はじめに
前回のVer.1では、予定をメモリ上に追加・一覧表示するところまでを実装した。
ただしメモリ上にしか保持していないため、プログラムを終了すると内容はすべて消えてしまった。
今回のVer.2では、プログラム終了時に予定をファイルへ保存し、次回起動時に読み込むようにする。ミニ日記アプリシリーズで扱った、構造体とファイルI/Oの組み合わせがここで活かせる。
対応するGitHubリポジトリは以下の通りである。
- Rocky-Seven/c-language-studies 内の
schedule-mini-app/ver2_save_and_load/
Ver.1からの変更点
- プログラム終了時(メニュー
0)に、登録されている予定をすべてschedules.txtに自動保存する。 - プログラム起動時に、
schedules.txtがあれば自動的に読み込む。ファイルが無い場合(初回起動時)は、何もせず空の状態で始まる。 - 予定の追加・一覧表示そのものの操作方法(メニュー
1``2``0)はVer.1と変わらない。
保存・読み込みのタイミングを「終了時」「起動時」に固定し、メニューに保存・読み込み専用の項目は追加していない。これにより、Ver.1のときと同じ感覚でそのまま使い続けられるようにしている。
保存形式
1件の予定を、カンマ区切りの1行として保存する。
年,月,日,予定例えば、前回の例で登録した2件の予定は、次のように保存される。
2026,8,1,夏休み計画
2026,8,15,お盆休みファイルへの保存
schedule_count の分だけループし、fprintf でカンマ区切りの行を書き出すだけのシンプルな実装である。
c
/* 現在保持している予定をすべてファイルに書き出す。1件につき1行、"年,月,日,予定"の形式。 */
void save_schedules(void) {
FILE *fp = fopen(SCHEDULE_FILE, "w");
if (fp == NULL) {
printf("ファイルへの保存に失敗しました。\n");
return;
}
for (int i = 0; i < schedule_count; i++) {
Schedule *s = &schedules[i];
fprintf(fp, "%d,%d,%d,%s\n", s->year, s->month, s->day, s->title);
}
fclose(fp);
}fopen を "w" モードで開くと、既存のファイルがあれば中身を空にしてから書き込む。そのため、保存のたびにファイル全体を最新の内容で上書きする形になる。
ファイルからの読み込み
読み込み側は、保存形式に対応する sscanf で1行ずつ解析する。
c
/*
* ファイルから予定を読み込み、現在メモリ上にある内容を置き換える。
* ファイルが存在しない場合(初回起動時など)は、何もせず空の状態で始める。
*/
void load_schedules(void) {
FILE *fp = fopen(SCHEDULE_FILE, "r");
if (fp == NULL) {
return;
}
schedule_count = 0;
int skipped = 0;
char line[MAX_LINE];
while (fgets(line, sizeof(line), fp) != NULL) {
if (schedule_count >= MAX_SCHEDULES) {
skipped++;
continue;
}
Schedule *s = &schedules[schedule_count];
/*
* 保存形式「年,月,日,予定」に対応するsscanf。
* 予定の部分は改行の手前まで(%[^\n])読み取ることで、
* 予定の内容に読点や空白が含まれていても正しく読み込める。
* 予定が空文字の行(データが壊れている場合など)は読み取りに失敗するため、
* その行はスキップする。
*/
int result = sscanf(line, "%d,%d,%d,%99[^\n]", &s->year, &s->month, &s->day, s->title);
/*
* add_schedule()と同様に、月日の範囲もあわせて確認する。
* ファイルを手動で編集した場合などに、壊れた日付
* (例: 月が13、日が40など)が紛れ込む可能性があるため。
*/
if (result == 4 && s->month >= 1 && s->month <= 12 && s->day >= 1 && s->day <= 31) {
schedule_count++;
} else {
skipped++;
}
}
fclose(fp);
if (skipped > 0) {
printf("注意: 読み込めなかったデータが%d件ありました。\n", skipped);
}
}%99[^\n] は「改行が来るまで、最大99文字を読み取る」という指定である。予定の内容にカンマや空白が含まれていても、改行までをまとめて1つの文字列として読み取れるため、%s のように空白で区切られてしまう心配がない。
fopen が NULL を返す場合(ファイルが存在しない場合)は、読み込みを行わずそのまま関数を抜ける。これにより、初回起動時にエラーで止まることなく、空の状態からアプリを開始できる。
【設計上の注意点】空の予定をはじく理由
実装を進める中で、予定の内容を空のまま追加できてしまうと、保存・読み込みのサイクルでその1件だけが消えてしまう問題が生じた。
予定が空文字だと、保存後のファイルは次のような行になる。
2026,9,1,この行を %99[^\n] で読み取ろうとすると、カンマの直後がそのまま改行のため、読み取るべき文字が1文字も無い。%[^\n] のような文字集合の指定は、最低1文字にマッチしないと変換が失敗する仕様のため、この行だけ sscanf の戻り値が3になり、読み込み失敗として扱われてしまう。
保存・読み込みの仕組み側で対応することもできるが、それよりも「空の予定はそもそも登録できないようにする」方がシンプルで分かりやすい。そのため add_schedule 側に、予定が空文字の場合は追加を中止するチェックを追加した。
c
if (s->title[0] == '\0') {
printf("予定の内容が入力されていません。追加を中止します。\n");
return;
}壊れたデータへの対応
schedules.txt を手動で編集した場合など、想定外の行が混ざる可能性がある。例えば次のような行である。
2026,13,40,壊れた行
おかしなテキスト1行目は数値としては読み取れるものの、月が13、日が40と、実在しない日付になっている。2行目はカンマ区切りの数値になっていない。どちらも load_schedules 内のチェック(sscanf の戻り値、および月日の範囲)に引っかかるため、読み込み対象から除外され、除外した件数がまとめて表示される。
注意: 読み込めなかったデータが2件ありました。コンパイルと実行
bash
gcc -Wall -Wextra -o schedule schedule.c
./schedule1回目の起動で予定を2件追加し、0 で終了する。
=== スケジュール管理アプリ (Ver.2) ===
1: 予定を追加
2: 予定一覧を表示
0: 終了
番号を入力してください: 1
年月日を入力してください(例: 2026 8 1 または 20260801): 2026 8 1
予定を入力してください: 夏休み計画
予定を追加しました。
...
番号を入力してください: 0
終了します。同じフォルダに schedules.txt が作成されていることを確認する。
bash
cat schedules.txt2026,8,1,夏休み計画
2026,8,15,お盆休みもう一度起動し、2 で一覧表示すると、前回終了時の内容がそのまま表示される。
--- 予定一覧 ---
1: 2026-08-01 夏休み計画
2: 2026-08-15 お盆休み次回予告
次回のVer.3では、登録した予定の編集・削除に対応する。何番目の予定を対象にするかを指定する操作が新たに必要になるため、一覧表示に振っている番号(インデックス)の扱いがポイントになる予定である。


コメント