C言語で日付や時刻を個々の要素に分解して扱うときに使うのがstruct tm
です。
年や月の表現に独特のルールがあり、特にtm_yearは1900年からの年数、tm_monは0始まりという2点でつまずきがちです。
本記事では各メンバーの意味と範囲、0始まりの落とし穴と実務的な対策を、初心者向けに丁寧に解説します。
struct tmとは?
年/月/日/時/分/秒を表す構造体
struct tm
は、カレンダー時刻を人間が読み書きしやすい形に分解した構造体です。
つまり年、月、日、時、分、秒という6つの基本要素をメンバーとして持ちます。
標準ライブラリの<time.h>
経由で得られる時刻をlocaltime
やgmtime
でstruct tm
に変換すると、これらのメンバーにアクセスできます。
まずは、現在時刻をstruct tm
に変換して各フィールドを表示する最小限の例です。
ここでは表示時の補正として年に+1900、月に+1を行います。
// 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 + 1900
とtm_mon + 1
を「出力のときに必ず足すルール」と体で覚えるのがおすすめです。
struct tmの各メンバーの範囲と意味
まず、6つのメンバーの概要を表にまとめます。
ここでの範囲は実装やうるう秒の扱いにより微妙な差があり得ますが、初心者が最初に押さえるべき値域として示しています。
メンバー | 意味 | 範囲 | 表示時の補正 | 例(2025-01-15 09:07:42) |
---|---|---|---|---|
tm_year | 1900年からの経過年数 | 実装依存だが通常は広いint範囲 | +1900 | 125 |
tm_mon | 月(0始まり) | 0〜11 | +1 | 0 |
tm_mday | 日 | 1〜31 | そのまま | 15 |
tm_hour | 時 | 0〜23 | そのまま | 9 |
tm_min | 分 | 0〜59 | そのまま | 7 |
tm_sec | 秒 | 0〜59 | そのまま | 42 |
tm_year(年): 1900年からの年数
意味
tm_year
は1900年からのオフセットです。
西暦を得たい場合はtm_year + 1900
を計算します。
逆に西暦を代入する場合はyear - 1900
に変換して格納します。
- 2025年 →
tm_year = 2025 - 1900 = 125
- 1999年 →
tm_year = 99
よくある誤り
- 西暦をそのまま
tm_year
に入れてしまう誤り。表示すると3900年代などの不正な年になります。
tm_mon(月): 0〜11
意味
tm_mon
は0始まりの月です。
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
形式の文字列を生成します。
// 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:SS
をstruct tm
に格納する際は、月を-1して0〜11に正規化します。
日付の妥当性も、月末やうるう年を考慮してチェックします。
// 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(...)
の形で、下限と上限を明確に書きましょう。
以下は、典型的な誤りと正しいチェックの対比です。
// 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」の配列を用意するとズレにくくなります。
// 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始まりに揃える。
この一連の習慣化で、カレンダー時刻の扱いは安定します。
まずは本記事のサンプルを手元で動かし、表示用関数とパーサを自分のプロジェクトに取り込むところから始めてみてください。