C言語で スケジュール管理アプリ(ミニ)を作ろう!第1回 ~予定の追加と一覧表示~

スポンサーリンク
C言語

はじめに

これまで本ブログでは、C言語を学ぶ企画として、構造体とファイルI/Oを使った「ミニ日記アプリ」シリーズと、「基本情報技術者試験・アルゴリズム問題」シリーズを進めてきた。
今回から、新しい題材として「スケジュール管理アプリ(ミニ)」を作っていこう。

日付付きの予定を登録・一覧表示・削除できる、シンプルなCLI(コマンドラインインターフェース)アプリである。最終的には祝日データとの連携も行い、Go・Kotlin版との比較も予定している。

シリーズ全体の計画

段階を追って機能を増やしていく方針は、ミニ日記アプリシリーズと同じである。

  • Ver.1:予定の追加・一覧表示(メモリ上のみ)今回
  • Ver.2:ファイルへの保存・読み込み
  • Ver.3:予定の編集・削除
  • Ver.4:日付順ソート(基本情報技術者試験アルゴリズムシリーズのバブルソートを応用)
  • Ver.5:祝日データとの連携

今回のVer.1では、まずメモリ上に予定を保持し、追加と一覧表示だけができるところまでを目標とする。

データ構造

1件の予定を、年・月・日とタイトルを持つ構造体として表現する。

typedef struct {
    int year;
    int month;
    int day;
    char title[MAX_TITLE];
} Schedule;

予定を複数保持するため、固定長の配列を用意する。ミニ日記アプリのVer.1と同様、ファイルへの保存はまだ行わないので、プログラムを終了すると内容は消える。

#define MAX_SCHEDULES 100
#define MAX_TITLE 100
#define MAX_DATE_LINE 64

Schedule schedules[MAX_SCHEDULES];
int schedule_count = 0;

メニュー表示

操作は「追加」「一覧表示」「終了」の3つのみとした。

void print_menu(void) {
    printf("\n=== スケジュール管理アプリ (Ver.1) ===\n");
    printf("1: 予定を追加\n");
    printf("2: 予定一覧を表示\n");
    printf("0: 終了\n");
    printf("番号を入力してください: ");
}

予定の追加

年月日と予定の内容を受け取る。当初は scanf("%d %d %d", ...) で年・月・日を直接読み取る仕様であったが、20260801 のようにスペース無しでまとめて入力されると、年しか読み取れず月・日が未入力のまま次の入力とずれてしまう問題が発生した。

そこで年月日の行はいったん fgets で1行分そのまま読み取り、sscanf で解析する方式に変更した。解析は次の2通りの形式を順に試す。

  • 半角スペース区切り:2026 8 1
  • 8桁連続表記:20260801%4d%2d%2d で年4桁・月2桁・日2桁に分割)
/* scanf直後に残った入力(改行など)を読み捨てる。 */
void clear_stdin(void) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

void add_schedule(void) {
    if (schedule_count >= MAX_SCHEDULES) {
        printf("これ以上予定を追加できません。\n");
        return;
    }

    Schedule *s = &schedules[schedule_count];
    char date_line[MAX_DATE_LINE];

    printf("年月日を入力してください(例: 2026 8 1 または 20260801): ");

    /*
     * 年月日は1行分をfgetsでまとめて読み取り、sscanfで解析する。
     * scanf("%d %d %d", ...)を直接使うと、"20260801"のように
     * スペース無しでまとめて入力された場合に年しか読み取れず、
     * 月・日が未入力のまま次の入力(予定の内容のはず)とずれてしまう。
     *
     * そこで次の2通りの形式を順に試す。
     * 1) "2026 8 1" のような半角スペース区切り
     * 2) "20260801" のような8桁連続表記(年4桁 + 月2桁 + 日2桁)
     */
    fgets(date_line, sizeof(date_line), stdin);

    int result = sscanf(date_line, "%d %d %d", &s->year, &s->month, &s->day);
    if (result != 3) {
        result = sscanf(date_line, "%4d%2d%2d", &s->year, &s->month, &s->day);
    }

    /*
     * %dは先頭の"-"も符号として受け付けてしまうため、
     * "2026-08-01"のようなハイフン区切りを渡すと、
     * ハイフンを負の符号と誤認識して month=-8, day=-1 のように
     * 「解析には成功したが値がおかしい」状態になることがある。
     * そのため件数チェックに加えて、月日の値そのものが
     * 妥当な範囲かどうかも確認する。
     */
    if (result != 3 || s->month < 1 || s->month > 12 || s->day < 1 || s->day > 31) {
        printf("入力形式が正しくありません。「2026 8 1」または「20260801」の形式で入力してください。\n");
        return;
    }

    printf("予定を入力してください: ");
    fgets(s->title, MAX_TITLE, stdin);

    /* fgetsで読み込んだ文字列の末尾の改行を取り除く。 */
    s->title[strcspn(s->title, "\n")] = '\0';

    schedule_count++;
    printf("予定を追加しました。\n");
}

fgets で読み込んだ文字列の末尾には改行コードが含まれるため、strcspn を使って改行を探し、そこにヌル文字を上書きすることで取り除いている。

なぜ「拒否」ではなく「両対応」にしたか

当初は、スペース区切り以外の入力(20260801など)はエラーメッセージを出して拒否するだけにしていた。しかし日付をまとめて8桁で入力する書き方はごく自然な形式であり、単に「使えない」とするより、対応できるようにする方が親切である。

%4d%2d%2d は、先頭から4桁・2桁・2桁という固定幅で数値を読み取る指定である。sscanf はこの指定に対して、先頭の空白を読み飛ばしたうえで、指定した桁数以内の数字を読み取ろうとする。そのため 20260801 のような区切りのない入力にも対応できる。

一方で、2026-8-1 のようなハイフン区切りや、数字以外の文字を含む入力までは対応しておらず、その場合はエラーメッセージを表示して追加処理を中止する。

なお、当初は件数チェック(result != 3)だけを行っていたが、これだけでは 2026-08-01 のようなハイフン区切りを弾ききれなかった。%d は先頭の - を負の符号として扱う仕様のため、sscanf(date_line, "%d %d %d", ...)2026-08-01 を渡すと、year=2026, month=-8, day=-1 という形で解析自体は成功してしまう。その結果、一覧表示が 2026--8--1 のように、区切り文字の - と負の符号の - が並んでしまう表示崩れが起きていた。

そのため、件数チェックに加えて s->month が1〜12、s->day が1〜31の範囲に収まっているかも確認するように変更した。これにより、ハイフン区切りのような「解析には成功するが値としておかしい」入力も、正しくエラーとして検出できるようになっている。

予定の一覧表示

登録順にそのまま表示するだけの、シンプルな実装である。日付順への並び替えはVer.4で対応する。

void list_schedules(void) {
    if (schedule_count == 0) {
        printf("登録されている予定はありません。\n");
        return;
    }

    printf("\n--- 予定一覧 ---\n");
    for (int i = 0; i < schedule_count; i++) {
        Schedule *s = &schedules[i];
        printf("%d: %04d-%02d-%02d  %s\n", i + 1, s->year, s->month, s->day, s->title);
    }
}

メインループ

main 関数では、メニュー表示と選択肢に応じた処理の呼び出しを繰り返す、単純な while ループとしている。メニュー番号の入力についても、数値以外が入力された場合にバッファを読み捨てないと同じ入力を延々と読み続けてしまうため、scanf の戻り値を確認している。

int main(void) {
    int choice;

    while (1) {
        print_menu();

        /* メニュー番号が数値以外の場合も、バッファを読み捨てて無限ループを防ぐ。 */
        if (scanf("%d", &choice) != 1) {
            clear_stdin();
            printf("正しい番号を入力してください。\n");
            continue;
        }
        clear_stdin();

        switch (choice) {
            case 1:
                add_schedule();
                break;
            case 2:
                list_schedules();
                break;
            case 0:
                printf("終了します。\n");
                return 0;
            default:
                printf("正しい番号を入力してください。\n");
        }
    }

    return 0;
}

全コード

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

#define MAX_SCHEDULES 100
#define MAX_TITLE 100
#define MAX_DATE_LINE 64

/* 1件の予定を表す構造体。年・月・日と、予定の内容を保持する。 */
typedef struct {
    int year;
    int month;
    int day;
    char title[MAX_TITLE];
} Schedule;

/* 予定を保持する配列。Ver.1ではファイル保存は行わず、メモリ上にのみ保持する。 */
Schedule schedules[MAX_SCHEDULES];
int schedule_count = 0;

/* scanf直後に残った入力(改行など)を読み捨てる。 */
void clear_stdin(void) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

void print_menu(void) {
    printf("\n=== スケジュール管理アプリ (Ver.1) ===\n");
    printf("1: 予定を追加\n");
    printf("2: 予定一覧を表示\n");
    printf("0: 終了\n");
    printf("番号を入力してください: ");
}

void add_schedule(void) {
    if (schedule_count >= MAX_SCHEDULES) {
        printf("これ以上予定を追加できません。\n");
        return;
    }

    Schedule *s = &schedules[schedule_count];
    char date_line[MAX_DATE_LINE];

    printf("年月日を入力してください(例: 2026 8 1 または 20260801): ");

    /*
     * 年月日は1行分をfgetsでまとめて読み取り、sscanfで解析する。
     * scanf("%d %d %d", ...)を直接使うと、"20260801"のように
     * スペース無しでまとめて入力された場合に年しか読み取れず、
     * 月・日が未入力のまま次の入力(予定の内容のはず)とずれてしまう。
     *
     * そこで次の2通りの形式を順に試す。
     * 1) "2026 8 1" のような半角スペース区切り
     * 2) "20260801" のような8桁連続表記(年4桁 + 月2桁 + 日2桁)
     */
    fgets(date_line, sizeof(date_line), stdin);

    int result = sscanf(date_line, "%d %d %d", &s->year, &s->month, &s->day);
    if (result != 3) {
        result = sscanf(date_line, "%4d%2d%2d", &s->year, &s->month, &s->day);
    }

    /*
     * %dは先頭の"-"も符号として受け付けてしまうため、
     * "2026-08-01"のようなハイフン区切りを渡すと、
     * ハイフンを負の符号と誤認識して month=-8, day=-1 のように
     * 「解析には成功したが値がおかしい」状態になることがある。
     * そのため件数チェックに加えて、月日の値そのものが
     * 妥当な範囲かどうかも確認する。
     */
    if (result != 3 || s->month < 1 || s->month > 12 || s->day < 1 || s->day > 31) {
        printf("入力形式が正しくありません。「2026 8 1」または「20260801」の形式で入力してください。\n");
        return;
    }

    printf("予定を入力してください: ");
    fgets(s->title, MAX_TITLE, stdin);

    /* fgetsで読み込んだ文字列の末尾の改行を取り除く。 */
    s->title[strcspn(s->title, "\n")] = '\0';

    schedule_count++;
    printf("予定を追加しました。\n");
}

void list_schedules(void) {
    if (schedule_count == 0) {
        printf("登録されている予定はありません。\n");
        return;
    }

    printf("\n--- 予定一覧 ---\n");
    for (int i = 0; i < schedule_count; i++) {
        Schedule *s = &schedules[i];
        printf("%d: %04d-%02d-%02d  %s\n", i + 1, s->year, s->month, s->day, s->title);
    }
}

int main(void) {
    int choice;

    while (1) {
        print_menu();

        /* メニュー番号が数値以外の場合も、バッファを読み捨てて無限ループを防ぐ。 */
        if (scanf("%d", &choice) != 1) {
            clear_stdin();
            printf("正しい番号を入力してください。\n");
            continue;
        }
        clear_stdin();

        switch (choice) {
            case 1:
                add_schedule();
                break;
            case 2:
                list_schedules();
                break;
            case 0:
                printf("終了します。\n");
                return 0;
            default:
                printf("正しい番号を入力してください。\n");
        }
    }

    return 0;
}

コンパイルと実行

gcc -Wall -Wextra -o schedule schedule.c
./schedule

実行すると、以下のようにメニューが表示される。

=== スケジュール管理アプリ (Ver.1) ===
1: 予定を追加
2: 予定一覧を表示
0: 終了
番号を入力してください:

「1」を選んで日付と予定を入力し、「2」で一覧表示すると、登録した予定が順番に表示される。

--- 予定一覧 ---
1: 2026-08-01  夏休み計画
2: 2026-08-15  お盆休み

次回予告

次回のVer.2では、プログラムを終了しても予定が消えないよう、ファイルへの保存と読み込みに対応する。ミニ日記アプリシリーズで扱った、構造体とファイルI/Oの組み合わせがここで活きてくる。

私のGitHubリポジトリにおいてソースコードを公開している。

Rocky-Seven/c-language-studies 内の schedule-mini-app/ver1_add_and_list/

コメント

タイトルとURLをコピーしました