閉じる

C言語のstruct tm入門(各メンバーの範囲と0始まりの落とし穴)

C言語で日付や時刻を個々の要素に分解して扱うときに使うのがstruct tmです。

年や月の表現に独特のルールがあり、特にtm_yearは1900年からの年数tm_monは0始まりという2点でつまずきがちです。

本記事では各メンバーの意味と範囲、0始まりの落とし穴と実務的な対策を、初心者向けに丁寧に解説します。

struct tmとは?

年/月/日/時/分/秒を表す構造体

struct tmは、カレンダー時刻を人間が読み書きしやすい形に分解した構造体です。

つまりという6つの基本要素をメンバーとして持ちます。

標準ライブラリの<time.h>経由で得られる時刻をlocaltimegmtimestruct tmに変換すると、これらのメンバーにアクセスできます。

まずは、現在時刻をstruct tmに変換して各フィールドを表示する最小限の例です。

ここでは表示時の補正として年に+1900、月に+1を行います。

C言語
// tm_overview.c
// 現在のローカル時刻をstruct tmに分解し、生の値と表示用に補正した値を並べて表示します。
#include <stdio.h>
#include <time.h>

static void print_tm_detail(const struct tm *t) {
    // 生の値 (struct tmが保持している内部表現)
    printf("[raw]  tm_year=%d (years since 1900), tm_mon=%d (0..11), tm_mday=%d, tm_hour=%d, tm_min=%d, tm_sec=%d\n",
           t->tm_year, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);

    // 表示用補正を施した値
    int year  = t->tm_year + 1900; // 1900年基準
    int month = t->tm_mon + 1;     // 0始まりを1始まりに
    printf("[disp] %04d-%02d-%02d %02d:%02d:%02d\n",
           year, month, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
}

int main(void) {
    time_t now = time(NULL);          // 今のUNIX時刻を取得
    // localtimeは説明対象外ですが、struct tmの実例を得るために使用します
    struct tm local = *localtime(&now);
    print_tm_detail(&local);
    return 0;
}
実行結果
[raw]  tm_year=125 (years since 1900), tm_mon=0 (0..11), tm_mday=15, tm_hour=09, tm_min=07, tm_sec=42
[disp] 2025-01-15 09:07:42

覚える2点

次の2点を確実に覚えるだけで、初学者の大半のバグを避けられます。

  • tm_yearは西暦そのものではなく「1900年からの年数」です。例えば2025年は125となります。
  • tm_monは「0始まり」で0が1月、11が12月です。表示時は+1してください。

覚え方として、tm_year + 1900tm_mon + 1を「出力のときに必ず足すルール」と体で覚えるのがおすすめです。

struct tmの各メンバーの範囲と意味

まず、6つのメンバーの概要を表にまとめます。

ここでの範囲は実装やうるう秒の扱いにより微妙な差があり得ますが、初心者が最初に押さえるべき値域として示しています。

メンバー意味範囲表示時の補正例(2025-01-15 09:07:42)
tm_year1900年からの経過年数実装依存だが通常は広いint範囲+1900125
tm_mon月(0始まり)0〜11+10
tm_mday1〜31そのまま15
tm_hour0〜23そのまま9
tm_min0〜59そのまま7
tm_sec0〜59そのまま42

tm_year(年): 1900年からの年数

意味

tm_year1900年からのオフセットです。

西暦を得たい場合はtm_year + 1900を計算します。

逆に西暦を代入する場合はyear - 1900に変換して格納します。

  • 2025年 → tm_year = 2025 - 1900 = 125
  • 1999年 → tm_year = 99

よくある誤り

  • 西暦をそのままtm_yearに入れてしまう誤り。表示すると3900年代などの不正な年になります。

tm_mon(月): 0〜11

意味

tm_mon0始まりの月です。

0が1月、11が12月に対応します。

表示にはtm_mon + 1が必要です。

  • 1月 → tm_mon = 0
  • 12月 → tm_mon = 11

よくある誤り

  • 1〜12の人間向けの月をそのまま代入してしまうこと。必ず-1して0〜11に正規化してください。

tm_mday(日): 1〜31

意味

tm_mdayは月内の日付です。

常に1始まりで、月ごとの上限は異なります。

例と注意

  • 1〜31の範囲ですが、実際にはtm_monと年に依存します。例: 2月なら28または29が上限です。

tm_hour(時): 0〜23

意味

tm_hourは24時間制の時です。

0が午前0時、23が午後11時です。

tm_min(分): 0〜59

意味

tm_minは分です。

0〜59の範囲で扱います。

tm_sec(秒): 0〜59

意味

tm_secは秒です。

基本は0〜59です。

実装や環境によりうるう秒で60が現れる可能性の議論がありますが、入門レベルでは0〜59を前提に実装・バリデーションを行うのが安全です。

0始まりの落とし穴と対策

表示時は年に+1900, 月に+1

出力フォーマットを1か所に集約すると、+1900や+1の入れ忘れを防げます。

次の補助関数はYYYY-MM-DD HH:MM:SS形式の文字列を生成します。

C言語
// fmt_tm.hints.c
// struct tmを人間向けに安全に表示する補助関数
#include <stdio.h>
#include <time.h>

void format_tm_ymdhms(const struct tm *t, char *buf, size_t bufsz) {
    int year  = t->tm_year + 1900; // 西暦に補正
    int month = t->tm_mon + 1;     // 1〜12に補正
    snprintf(buf, bufsz, "%04d-%02d-%02d %02d:%02d:%02d",
             year, month, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
}

int main(void) {
    time_t now = time(NULL);
    struct tm local = *localtime(&now);

    char s[32];
    format_tm_ymdhms(&local, s, sizeof s);
    printf("display: %s\n", s);
    return 0;
}
実行結果
display: 2025-01-15 09:07:42

入力の月(1〜12)は0〜11に変換して格納

人間が入力するYYYY-MM-DD HH:MM:SSstruct tmに格納する際は、月を-1して0〜11に正規化します。

日付の妥当性も、月末やうるう年を考慮してチェックします。

C言語
// parse_tm.c
// 文字列"YYYY-MM-DD HH:MM:SS"をstruct tmに詰める安全な例
#include <stdio.h>
#include <time.h>

static int is_leap(int year) { // yearは西暦
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

static int days_in_month(int year, int mon0) { // mon0は0〜11
    static const int base_days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
    if (mon0 == 1) { // 2月
        return base_days[1] + (is_leap(year) ? 1 : 0);
    }
    return base_days[mon0];
}

int parse_ymdhms_to_tm(const char *s, struct tm *out) {
    int Y, M, D, h, m, se;
    if (sscanf(s, "%d-%d-%d %d:%d:%d", &Y, &M, &D, &h, &m, &se) != 6) {
        return 0; // 形式不一致
    }
    if (Y < 1900 || M < 1 || M > 12) return 0;
    int mon0 = M - 1; // 0始まりへ
    int dim = days_in_month(Y, mon0);
    if (D < 1 || D > dim) return 0;
    if (h < 0 || h > 23) return 0;
    if (m < 0 || m > 59) return 0;
    if (se < 0 || se > 59) return 0; // 入門では0〜59を採用

    struct tm t = {0};
    t.tm_year = Y - 1900; // 1900年基準へ
    t.tm_mon  = mon0;     // 0〜11
    t.tm_mday = D;
    t.tm_hour = h;
    t.tm_min  = m;
    t.tm_sec  = se;
    t.tm_isdst = -1;      // サマータイム情報不明

    *out = t;
    return 1;
}

static void dump_tm_for_check(const struct tm *t) {
    printf("raw: year=%d, mon=%d, mday=%d, hour=%d, min=%d, sec=%d\n",
           t->tm_year, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
    printf("disp: %04d-%02d-%02d %02d:%02d:%02d\n",
           t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
}

int main(void) {
    struct tm t;
    if (parse_ymdhms_to_tm("2024-02-29 23:59:59", &t)) {
        puts("OK: 2024のうるう年は2/29が有効");
        dump_tm_for_check(&t);
    }
    if (!parse_ymdhms_to_tm("2025-02-29 12:00:00", &t)) {
        puts("NG: 2025はうるう年ではないため2/29は無効");
    }
    if (!parse_ymdhms_to_tm("2025-13-01 00:00:00", &t)) {
        puts("NG: 13月は無効。1〜12の範囲に直す");
    }
    return 0;
}
実行結果
OK: 2024のうるう年は2/29が有効
raw: year=124, mon=1, mday=29, hour=23, min=59, sec=59
disp: 2024-02-29 23:59:59
NG: 2025はうるう年ではないため2/29は無効
NG: 13月は無効。1〜12の範囲に直す

境界チェックの誤り(<=/<, 0/1)に注意

境界条件は1文字違うだけでバグになります。

例えば、月のチェックをM <= 12 && M >= 1とすべきところをM < 12としてしまうと12月が弾かれます。

秒や分は0 <= x <= 59、時は0 <= h <= 23、日付は1 <= D <= days_in_month(...)の形で、下限と上限を明確に書きましょう。

以下は、典型的な誤りと正しいチェックの対比です。

C言語
// bad_good_checks.c
// よくある境界チェックの誤りと正しい書き方
#include <stdio.h>

int main(void) {
    int M = 12; // 12月のつもり
    // 悪い例: "< 12"にしてしまい12が弾かれる
    if (M > 0 && M < 12) {
        puts("bad: 12月が無効になってしまう");
    }
    // 良い例: ">= 1 && <= 12"で両端を含める
    if (M >= 1 && M <= 12) {
        puts("good: 1〜12を正しく許可する");
    }
    return 0;
}
実行結果
bad: 12月が無効になってしまう
good: 1〜12を正しく許可する

月の配列インデックス利用でオフバイワン防止

配列のインデックスは0始まりです。

tm_monも0始まりなので、月関連のデータは「0〜11」の配列を用意するとズレにくくなります。

C言語
// month_index.c
// 月ごとの表示名や最大日数を0始まり配列で扱い、tm_monと自然に対応づける
#include <stdio.h>

static const char *MONTH_NAME[12] = {
    "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"
};

int main(void) {
    int tm_mon = 0; // 1月は0
    printf("tm_mon=%d -> month name=%s\n", tm_mon, MONTH_NAME[tm_mon]);

    tm_mon = 11; // 12月は11
    printf("tm_mon=%d -> month name=%s\n", tm_mon, MONTH_NAME[tm_mon]);
    return 0;
}
実行結果
tm_mon=0 -> month name=Jan
tm_mon=11 -> month name=Dec

このように、人間向け入力→-1→格納格納→+1→表示に加え、配列も0始まりで揃えると、オフバイワンの発生箇所が激減します。

初心者向けチェックリスト

西暦とtm_yearを混在させない

コード中でtm_yearを扱う関数と、西暦を扱う関数を混在させると混乱します。

関数の引数や戻り値の「単位」をコメントで明示し、tm_year + 1900の変換は境界でのみ行う方針にしましょう。

表示用の補正(+1900/+1)を共通化

表示処理のあちこちで+1900+1を書くと、書き漏れが発生します。

上のformat_tm_ymdhmsのように、出力フォーマッタを1か所に集約してください。

各フィールドの範囲をコメントに明記

構造体に値を詰める箇所では、その場で範囲をコメントしましょう。

// tm_mon: 0..11, 入力は1..12なので-1して格納

将来の自分や他の開発者が安心して保守できます。

境界値テスト(1,31,0,23,59)を用意

単体テストや手動テストでは、最小値と最大値を必ず含めます。

日付なら1日と月末、時なら0時と23時、分・秒なら0と59です。

2月29日や月をまたぐ直前直後も加えると、境界バグを早期に検出できます。

まとめ

struct tmの最大の落とし穴は「tm_yearが1900年基準」「tm_monが0始まり」の2点です。

表示では年に+1900、月に+1、入力では月を-1して格納、境界チェックは上下限を明示し、月の配列は0始まりに揃える。

この一連の習慣化で、カレンダー時刻の扱いは安定します。

まずは本記事のサンプルを手元で動かし、表示用関数とパーサを自分のプロジェクトに取り込むところから始めてみてください。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!