固定長テキストデータを読み込む

どう書く?orgを眺めていて、固定長テキストデータを読み込む方法の問題を見つけた。

試しに書いてみたけど、アカウントを持っていないし、何より2008/03頃のお題なので今さら投稿するのも微妙なのでここに晒しておく。

特徴はこんな感じ。

  • 使用言語はC言語
  • エラー処理は考えない。
  • データが正しいかどうかチェックしない。
  • ファイルから読み込んだデータは文字列として保持する。
    • 「文字列」だから、末尾にNUL文字を付加する。
    • 上記より、データ格納用の構造体(メンバは全てchar型)に単純にfreadでデータを詰め込む書き方は不可能なので、sscanfでパースする。

sscanfでパースするアイデアは、自分の中では『プログラミング作法』から借りてきたと思っている。

『プログラミング作法』の中に、通信用パケットのpack/unpack用にprintfライクな可変長引数関数を定義する話が書いてある。個別のパケットごとにpack/unpack用の関数を実装するのではなく、pack/unpackするパケットの構造を記述する専用の記法を用意することでメンテナンス性や拡張性を向上させようというアイデアだ。

で、固定長テキストデータをパースする場合、結果を全て文字列として格納するならsscanfでも十分使える。もちろんデータの検証やデータ形式の変換をすることを考えるとsscanfでは不十分なケースも多々あるけど、その処理はsscanfで一旦文字列としてパースした後に個々のデータに対して実施しても問題はないはずだ。パースとデータの検証/データ形式の変換を同時進行で行わなくてはならない法律なんて存在しない*1

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


/* ---------------------------------------------------------------------- */
/* 汎用のデータ型 */
/* ---------------------------------------------------------------------- */

#if (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L)
	/* C99 の場合は、組み込みのデータ型を使用 */
	#include <stdbool.h>
#else
	#ifndef __cplusplus
		/**
		 * @brief  C++互換のブール型
		 */
		typedef  enum { false = 0, true = 1 }  bool;
	#endif
	/**
	 * @brief  C99互換のブール型
	 */
	#define  _Bool  bool
#endif


/* ---------------------------------------------------------------------- */
/* 汎用のマクロ関数 */
/* ---------------------------------------------------------------------- */

/** 配列の要素数を求める */
#define  NELEMS(ary)  (sizeof(ary) / sizeof((ary)[0]))


/* ---------------------------------------------------------------------- */
/* コンフィグレーション定数 */
/* ---------------------------------------------------------------------- */

/** ファイルパスの区切り文字 */
#define  PATH_SEP  '\\'

enum {
	MAX_USER_INFO = 500,        /**<  */
	MAX_EATEN_MENU = 31,        /**<  */
	USER_INFO_SIZE = 34,        /**< struct USER_INFO に該当するデータのファイル上の大きさ(byte) */
	EATEN_MENU_SIZE = 1502      /**< struct EATEN_MENU に該当するデータのファイル上の大きさ(byte) */
};


/* ---------------------------------------------------------------------- */
/* データ型 */
/* ---------------------------------------------------------------------- */

struct EATEN_MENU {
	char date[3];
	char breakfast[501];
	char lunch[501];
	char dinner[501];
};

struct USER_INFO {
	char family[13];
	char name[13];
	char sex[2];
	char age[4];
	char year[5];
	char month[3];
	struct EATEN_MENU eaten_menu[MAX_EATEN_MENU];
};


/* ---------------------------------------------------------------------- */
/* 関数 */
/* ---------------------------------------------------------------------- */

/** ファイル名からディレクトリ部分を取り除いた部分の先頭位置を返す */
static const char *basename(const char *name)
{
	const char *bn;

	assert(name != NULL);

	bn = strrchr(name, PATH_SEP);
	return (bn == NULL) ? name : bn;
}

/** ファイルから固定長のテキストデータを読み込む */
static _Bool read_file(struct USER_INFO *ui, size_t size, FILE *in)
{
	static char buf[EATEN_MENU_SIZE];

	struct USER_INFO *ui_end;
	struct EATEN_MENU *em, *em_end;

	assert(ui != NULL && size > 0 && in != NULL);

#if 0
	/* uiの実体はゼロクリアされていなければならない
	 * 現状はstatic変数なので、初期化時にゼロクリアされているはず
	 */
	(void) memset(ui, 0, sizeof(*ui) * size);
#endif

	for (ui_end = ui + size; ui < ui_end; ++ui) {
		if (fread((void *) buf, USER_INFO_SIZE, 1, in) != 1) {
			return false;
		}
		(void) sscanf(buf, "%12c%12c%1c%3c%4c%2c",
		              ui->family, ui->name, ui->sex, ui->age, ui->year, ui->month);

		for (em = ui->eaten_menu, em_end = em + NELEMS(ui->eaten_menu); em < em_end; ++em) {
			if (fread((void *) buf, EATEN_MENU_SIZE, 1, in) != 1) {
				return false;
			}
			(void) sscanf(buf, "%2c%500c%500c%500c",
			              em->date, em->breakfast, em->lunch, em->dinner);
		}
	}

	return true;
}

/** 全データを出力する */
static void print_data(struct USER_INFO *ui, size_t size, FILE *out)
{
	struct USER_INFO *ui_begin, *ui_end;
	struct EATEN_MENU *em, *em_end;

	assert(ui != NULL && out != NULL);

	for (ui_begin = ui, ui_end = ui + size; ui < ui_end; ++ui) {
		(void) fprintf(out, "%s%s:%s:%s:%s:%s:%s\n",
		               (ui == ui_begin) ? "" : "%%\n",
		               ui->family, ui->name, ui->sex, ui->age, ui->year, ui->month);

		for (em = ui->eaten_menu, em_end = em + NELEMS(ui->eaten_menu); em < em_end; ++em) {
			(void) fprintf(out, "%s:%s:%s:%s\n",
			               em->date, em->breakfast, em->lunch, em->dinner);
		}
	}
}

/** メインルーチン */
int main(int argc, char *argv[])
{
	static struct USER_INFO user_info[MAX_USER_INFO];

	FILE *in;
	_Bool file_read;

	if (argc > 2) {
		(void) fprintf(stderr, "Usage: %s [FILE]\n", basename(argv[0]));
		return EXIT_FAILURE;
	} else if (argc <= 1) {
		in = stdin;
	} else if ((in = fopen(argv[1], "r")) == NULL) {
		perror(argv[1]);
		return EXIT_FAILURE;
	} else {
		/*EMPTY*/
	}

	file_read = read_file(user_info, NELEMS(user_info), in);
	if (in != stdin) {
		(void) fclose(in);
	}

	if (!file_read) {
		fprintf(stderr, "%s: read data failed\n", basename(argv[0]));
		return EXIT_FAILURE;
	}

	print_data(user_info, NELEMS(user_info), stdout);

	return EXIT_SUCCESS;
}

今回は「%c」が大活躍している。読み込むテキストデータの形式上、空白文字が無視される「%s」が使えない部分があるのだけど、「%c」は空白文字を無視せずに読み込む。その代わり文字列末尾にNUL文字が付加されないので注意する必要がある。

「%c」とか、今回は使わなかったけど正規表現の文字クラスみたいな「%[...]」や「%[^...]」を使えば、結構面白そうなことができそうな気がする。もっとも、現実にはPerlPythonRubyでテキスト処理する方が楽なので、sscanfを使う機会自体が少ない。

*1:もちろん同時進行で進めた方がよい場合ならあるけど。