閉じる

【C言語】 mktimeの正しい使い方|タイムゾーン・夏時間の落とし穴と対処法

C言語で日付や時刻を扱うとき、mktimeを正しく理解しているかどうかで、バグの出やすさが大きく変わります。

特にタイムゾーンや夏時間(DST)が関わると、意図しない時間に変換されてしまうことが少なくありません。

本記事では、mktimeの基本仕様から、タイムゾーン・夏時間の落とし穴、そしてそれらを回避するための実践的なテクニックまで、丁寧に解説していきます。

mktimeとは何かを理解する

mktimeの基本仕様と目的

mktimeは、C標準ライブラリに含まれる関数で、ローカル時刻を表すstruct tmを、カレンダー時刻time_tに変換するための関数です。

宣言はtime.hヘッダで定義されています。

関数プロトタイプは次のようになります。

C言語
#include <time.h>

time_t mktime(struct tm *timeptr);

mktimeの主な目的は、次の2つです。

  1. 人が読みやすいローカル日時(年・月・日・時・分・秒)を、機械が扱いやすい連続した秒数(time_t)に変換すること
  2. struct tm内の日付・時刻を「正規化」すること(例えば、13月を翌年1月に直すなど)

mktimeはローカルタイムゾーンを前提に動作し、TZ環境変数やOSのタイムゾーン設定を参照します。

そのため、同じソースコードでも、実行環境が異なると結果が変わる可能性があります。

time_tとstruct tmの関係

C言語で時刻を扱う基本的な型はtime_tstruct tmです。

time_tは、ある基準時刻(多くの環境では1970-01-01 00:00:00 UTC)からの経過秒数を表す整数型(または整数互換の型)です。

内部表現は環境依存で、32ビットまたは64ビット整数として実装されていることが多いです。

struct tmは、次のような構造体です。

C言語
#include <time.h>

struct tm {
    int tm_sec;   // 秒(0〜60: うるう秒を考慮)
    int tm_min;   // 分(0〜59)
    int tm_hour;  // 時(0〜23)
    int tm_mday;  // 日(1〜31)
    int tm_mon;   // 月(0〜11: 0が1月、11が12月)
    int tm_year;  // 年(1900年からの年数: 2025年なら125)
    int tm_wday;  // 曜日(0〜6: 0が日曜)
    int tm_yday;  // 年内通算日(0〜365: 0が1月1日)
    int tm_isdst; // 夏時間フラグ(後述)
};

mktimeは「struct tm → time_t」の変換を行い、逆方向「time_t → struct tm」の変換はlocaltimegmtimeが担当します。

この関係をまとめると、次のようになります。

役割型・関数特徴
機械向け時刻time_t基準時刻からの経過秒数
人間向け時刻構造体struct tm年月日・時分秒などを分解して保持
ローカル→time_tmktimeタイムゾーン・DSTを考慮して変換
time_t→ローカルlocaltimeローカルタイムゾーンでstruct tmを生成
time_t→UTCgmtimeUTCでstruct tmを生成

ローカル時刻とUTCの違い

UTC(Coordinated Universal Time)は、世界標準の時刻の基準となるタイムスケールです。

ネットワークやログ、分散システムなどの世界では、UTCを基準に扱うことが一般的です。

一方、ローカル時刻は、特定の地域のタイムゾーンと夏時間(DST)の規則を適用した時刻です。

例えば、日本標準時(JST)はUTC+9時間で、夏時間はありません。

ここで重要なのは、同じtime_tの値でも、タイムゾーンが違えばstruct tmに変換したときの表示が変わるという点です。

mktimeはローカル時刻をtime_tに変換する関数なので、UTCのつもりで使うと誤差が生じる可能性があります。

mktimeの正しい使い方

struct tmの初期化と設定のポイント

mktimeを正しく使うためには、struct tmをきちんと初期化したうえで必要なフィールドを設定することが重要です。

未初期化のままmktimeに渡すと、環境依存で不定な結果になります。

典型的な初期化方法は次の通りです。

C言語
#include <stdio.h>
#include <time.h>
#include <string.h>

int main(void) {
    struct tm t;

    // 構造体をゼロクリア
    memset(&t, 0, sizeof(t));

    // 例えば、2025年5月1日 13:30:00 を設定する
    t.tm_year = 2025 - 1900; // 1900年からの年数
    t.tm_mon  = 5 - 1;       // 0が1月なので、5月は4
    t.tm_mday = 1;           // 日付
    t.tm_hour = 13;          // 時
    t.tm_min  = 30;          // 分
    t.tm_sec  = 0;           // 秒

    // tm_isdstは「情報なし」を意味する-1にしておくのが基本
    t.tm_isdst = -1;

    time_t tt = mktime(&t);

    printf("time_t: %lld\n", (long long)tt);
    printf("normalized: %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);

    return 0;
}

実行結果例(タイムゾーンがJSTの場合):

実行結果
time_t: 1746073800
normalized: 2025-05-01 13:30:00

ポイントは次の通りです。

  • 構造体全体をゼロクリアする(memsetまたは= {0}など)
  • tm_yearは西暦年−1900tm_monは0が1月という仕様に注意する
  • tm_isdstは-1にしておくことで、mktimeにDST判定を任せる

mktimeの戻り値とエラー処理

mktimeの戻り値はtime_tですが、変換に失敗した場合は(time_t)-1を返すと規定されています。

ただし、実際にその時刻が(time_t)-1で表される有効な時刻である可能性もあるため、戻り値だけでエラーを判定するのは不完全です。

実務上は次の2点を組み合わせて確認する方法がよく使われます。

  1. 戻り値が(time_t)-1かどうかを確認する
  2. mktimeに渡したstruct tmと、変換後のstruct tm(正規化結果)が整合しているかを確認する

簡易的なチェックコード例を示します。

C言語
#include <stdio.h>
#include <time.h>
#include <string.h>

int safe_mktime(struct tm *t, time_t *out) {
    // 入力を保存しておく
    struct tm original = *t;

    time_t tt = mktime(t);
    if (tt == (time_t)-1) {
        // まずはエラーの可能性として扱う
        return -1;
    }

    // ケースによっては、originalとtを比較して
    // ありえない日付になっていないかチェックしても良い
    // (ここでは簡略化)

    *out = tt;
    return 0;
}

int main(void) {
    struct tm t;
    memset(&t, 0, sizeof(t));
    t.tm_year = 1800 - 1900; // かなり昔の年
    t.tm_mon  = 0;
    t.tm_mday = 1;

    t.tm_isdst = -1;

    time_t tt;
    if (safe_mktime(&t, &tt) != 0) {
        printf("mktime failed or returned (time_t)-1\n");
    } else {
        printf("time_t = %lld\n", (long long)tt);
    }

    return 0;
}

実際には、対象時刻がtime_tの範囲内かどうかを事前に見積もることや、errnoなど追加情報を用いる実装(非標準拡張)も環境によって存在します。

正規化の仕組み

mktimeの便利な点として、日付・時刻の「正規化」を自動で行ってくれることがあります。

例えば、次のようなコードを考えます。

C言語
#include <stdio.h>
#include <time.h>
#include <string.h>

int main(void) {
    struct tm t;
    memset(&t, 0, sizeof(t));

    // あえて「変な」値を設定する
    t.tm_year = 2025 - 1900;
    t.tm_mon  = 0;   // 1月
    t.tm_mday = 32;  // 32日(存在しない)
    t.tm_hour = 25;  // 25時
    t.tm_min  = 70;  // 70分
    t.tm_sec  = 80;  // 80秒
    t.tm_isdst = -1;

    // 正規化のためだけにmktimeを呼び出す
    time_t tt = mktime(&t);

    printf("time_t: %lld\n", (long long)tt);
    printf("normalized: %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);

    return 0;
}

出力例(タイムゾーンに依存しない形で考えると):

実行結果
time_t: 1738445480   // 値は環境依存
normalized: 2025-02-02 02:11:20

mktimeは以下のような手順で正規化します。

  • 秒が60以上なら分へ繰り上げ、負なら繰り下げ
  • 分が60以上なら時へ繰り上げ、同様に繰り下げ
  • 時が24以上なら日へ、日がその月の日数を超えたら月へ、月が12以上なら年へ、というように繰り上げ
  • うるう年や月ごとの日数も考慮する

この正規化機能のおかげで、「N日後」や「N時間後」の計算を比較的簡単に行えるようになります。

日付計算でのmktimeの活用例

mktimeは日付・時刻の差分計算にもよく使われる関数です。

典型的なパターンとして、「指定日時からN日後・N日前を計算する」例を示します。

C言語
#include <stdio.h>
#include <time.h>
#include <string.h>

void add_days(struct tm *t, int days) {
    t->tm_mday += days; // 日にちを直接加算
    t->tm_isdst = -1;   // 変更後もDST判定を任せる
    mktime(t);          // 正規化(結果はtに反映される)
}

int main(void) {
    struct tm t;
    memset(&t, 0, sizeof(t));

    // 2025-01-31 10:00:00 を起点にする
    t.tm_year = 2025 - 1900;
    t.tm_mon  = 0;    // 1月
    t.tm_mday = 31;
    t.tm_hour = 10;
    t.tm_min  = 0;
    t.tm_sec  = 0;
    t.tm_isdst = -1;

    printf("start: %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);

    // 10日後を求める
    add_days(&t, 10);

    printf("after 10 days: %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);

    return 0;
}
実行結果
start: 2025-01-31 10:00:00
after 10 days: 2025-02-10 10:00:00

このように、一度mktimeに通して正規化してしまえば、月末をまたぐ複雑な計算も比較的簡潔に書けます

ただし、この方法もDST境界をまたぐときには「時刻」がずれる可能性があるため、次章以降で扱うDSTの話が重要になります。

mktimeとタイムゾーンの落とし穴

タイムゾーン依存の挙動とTZ環境変数

mktimeはローカルタイムゾーンのルールを元に、struct tmをtime_tに変換します。

このとき、どのタイムゾーンのルールを使うかは、一般にTZ環境変数とOSの設定によって決まります。

例えば、UNIX系システムでTZを切り替えると、mktimeの結果が変わります。

C言語
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>

void test_mktime(const char *tz) {
    // TZを設定
    if (tz) {
        setenv("TZ", tz, 1);
    } else {
        unsetenv("TZ");
    }
    tzset(); // 実際に反映

    struct tm t;
    memset(&t, 0, sizeof(t));
    t.tm_year = 2025 - 1900;
    t.tm_mon  = 0;
    t.tm_mday = 1;
    t.tm_hour = 0;
    t.tm_min  = 0;
    t.tm_sec  = 0;
    t.tm_isdst = -1;

    time_t tt = mktime(&t);

    printf("TZ=%s, time_t=%lld\n", tz ? tz : "(default)", (long long)tt);
}

int main(void) {
    test_mktime("Asia/Tokyo");
    test_mktime("America/New_York");
    return 0;
}

出力例(概念的な例):

実行結果
TZ=Asia/Tokyo, time_t=1735666800
TZ=America/New_York, time_t=1735688400

同じstruct tm(2025-01-01 00:00:00)でも、どのタイムゾーンの「2025-01-01 00:00:00」なのかによって、UTCの基準から見た秒数time_tが変わることがわかります。

ローカルタイムとmktimeの関係

mktimeが扱うのは常に「ローカル時刻」です。

つまり、構造体struct tmで指定した日時は、「現在のローカルタイムゾーンにおける日時」と解釈されます。

これと対になるのがlocaltimeです。

localtimeは、time_tローカル時刻に変換し、struct tmを返します。

両者を組み合わせると、次の関係になります。

  • struct tm(ローカル) → mktime → time_t
  • time_t → localtime → struct tm(ローカル)

この組を使うことで、ローカル時刻とtime_tの間を双方向に変換できますが、どちらもタイムゾーンや夏時間の影響を受ける点に注意が必要です。

タイムゾーンをまたぐ日時変換の注意点

異なるタイムゾーン間で日時を変換するときは、必ず一度time_t(またはUTC)を経由することが重要です。

間違いやすいパターンとして、単純に時差を加減算するケースがありますが、DSTや歴史的なタイムゾーン変更を見落とす危険があります。

概念的な手順は次のようになります。

  1. 変換元のタイムゾーン環境でmktimeを呼び、struct tm(ローカル)time_tに変換
  2. 変換先のタイムゾーン環境に切り替え、time_tstruct tm(ローカル)にlocaltimeなどで変換

このように、「絶対時刻」としてのtime_tは共通の土台であり、そこから各タイムゾーンにマッピングする、という発想が重要です。

OSや実行環境によるmktimeの違い

mktimeの挙動は、C標準で大まかな仕様が定められているものの、細かい部分は実装依存です。

例えば次のような点が環境ごとに異なります。

  • DST切り替え時のtm_isdstの解釈方法
  • 歴史的なタイムゾーンルール(過去のオフセット)の扱い
  • time_tの範囲(32ビットか64ビットか)
  • うるう秒の扱い(多くは実質的に無視)

そのため、タイムゾーン・DSTをまたいだ厳密な日時計算を行う場合、OSごとの差異がバグを生むことがあります。

クロスプラットフォームなコードを書く場合は、検証用テストケースを複数の環境で実行して結果を比較することが非常に重要です。

mktimeと夏時間(DST)の注意点と対処法

DST(夏時間)フラグtm_isdstの意味

struct tmtm_isdstフィールドは、DST(夏時間)が適用されているかどうかを示すために使われます。

意味は次の通りです。

意味
-1DST情報は不明。mktimeが自動的に判定する
0DSTは適用されていない標準時間であると指定
>0DSTが適用されている時間であると指定

実務では、基本的にtm_isdstは-1にしておき、mktimeに判定を任せるのが安全です。

特定の理由で明示的にDST/非DSTを指定したい場合は0または1を使いますが、誤った値を指定すると1時間ずれた結果になってしまうことがあるため注意が必要です。

不存在時刻・重複時刻とmktimeの挙動

DST切り替え時には、カレンダー上は指定できるが、実際には存在しない時刻や、2回存在する時刻が発生します。

例えば、ある地域で春に「2:00から3:00に一気に進む」ルールがあるとします。

この場合、2:30という時刻は実際には存在しません。

そのような「存在しない時刻」をmktimeに渡した場合の挙動は、環境によって異なりますが、一般的には次のような形になります。

  • 最も近い有効な時刻に丸める
  • または、DSTフラグを見て前後どちら側の時刻に解釈する

反対に、秋に「2:00に戻る」ルールがある地域では、1:30という時刻が2回出現します。

この「重複時刻」をどう解釈するかも、tm_isdstの値や実装に依存します。

ここからわかる通り、DST境界付近の時刻は、mktimeにとって最も扱いが難しい領域です。

DST境界でのバグ例とデバッグ方法

よくあるバグの例として、DST切り替えをまたいで「同じ時刻で日を進める」プログラムがあります。

例えば、「毎日9:00に実行される処理」のスケジュールを、次のように計算しているとします。

C言語
// 疑似コード: ローカル時間で毎日9:00を計算
struct tm t = { ... 9:00 ... };
for (i = 0; i < N; i++) {
    // tを使って何か処理
    t.tm_mday += 1;
    t.tm_isdst = -1;
    mktime(&t); // 正規化
}

このコードは、一見すると毎日9:00を保ちながら日付だけ進めているように見えますが、DST切り替えをまたいだタイミングで9:00ではなく8:00または10:00になってしまうことがあります。

理由は、mktimeがDSTの有無に応じて実際のUTCオフセットを変更するためです。

デバッグのコツとしては、次のような方法があります。

  • time_tの値と、struct tm(ローカル)の両方をログ出力する
  • UTCベース(ctimeやgmtime)でも確認する
  • TZを固定して、DSTのない地域(例: UTC)で再実行して差分を見る

これにより、問題がタイムゾーン・DSTに起因しているかどうかを切り分けやすくなります。

DST影響を避けるmktimeの実践的テクニック

DSTの影響を最小限にするために、次のようなテクニックが有効です。

テクニック1: UTC基準でtime_tを操作する

できるだけtime_tやUTCベースで計算し、最後にローカルに変換する方法です。

C言語
#include <stdio.h>
#include <time.h>

int main(void) {
    time_t now = time(NULL);

    // 10日後をUTC基準で計算
    time_t after = now + (time_t)10 * 24 * 60 * 60;

    // 結果をローカル時刻として表示
    struct tm *loc = localtime(&after);

    printf("10 days after (local): %04d-%02d-%02d %02d:%02d:%02d\n",
           loc->tm_year + 1900, loc->tm_mon + 1, loc->tm_mday,
           loc->tm_hour, loc->tm_min, loc->tm_sec);
    return 0;
}

この方法は「絶対的な経過時間」を扱う用途(例えば10日後の有効期限など)には有効ですが、「毎日9:00」のようなローカルの壁時計時刻を厳密に維持したい場合には別の工夫が必要です。

テクニック2: 壁時計時刻を優先した計算

「毎日9:00」というローカル時刻を優先する場合は、日付だけを操作して、時刻部分は固定するという方法があります。

C言語
#include <stdio.h>
#include <time.h>
#include <string.h>

void next_day_9am(struct tm *t) {
    // 日付だけ進める
    t->tm_mday += 1;

    // 時刻を明示的にリセット
    t->tm_hour = 9;
    t->tm_min  = 0;
    t->tm_sec  = 0;

    t->tm_isdst = -1;
    mktime(t); // 正規化
}

int main(void) {
    struct tm t;
    memset(&t, 0, sizeof(t));

    // 初回: 2025-03-25 09:00
    t.tm_year = 2025 - 1900;
    t.tm_mon  = 2; // 3月
    t.tm_mday = 25;
    t.tm_hour = 9;
    t.tm_isdst = -1;
    mktime(&t);

    for (int i = 0; i < 10; i++) {
        printf("%04d-%02d-%02d %02d:%02d:%02d (isdst=%d)\n",
               t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
               t.tm_hour, t.tm_min, t.tm_sec, t.tm_isdst);
        next_day_9am(&t);
    }

    return 0;
}

この方法は、「現地のカレンダー上で毎日同じ時刻にイベントを行う」ような用途に適しています。

mktime以外の選択肢とライブラリ活用方法

タイムゾーンやDSTを正確に扱いたい場合、標準Cのmktimeだけに頼るのは限界があることも多いです。

そのため、状況に応じて次のような選択肢を検討することが有効です。

POSIX拡張

  • timegm: 非標準ですが、UTCとしてstruct tmを解釈してtime_tに変換する関数です。タイムゾーンに左右されない変換が可能になります。
  • localtime_r: スレッドセーフなlocaltimeのバージョンです。

C++標準ライブラリ(chrono)

C++であれば、C++20以降のstd::chronoのタイムゾーン・カレンダー機能が非常に強力です。

標準でタイムゾーンデータベースを扱える実装も増えてきています。

外部ライブラリ

より高度なニーズには、次のようなライブラリがあります。

  • ICU(International Components for Unicode)の日時API
  • Howard Hinnant氏のdate/tzライブラリ(C++向け)
  • OS特有の日時API(WindowsのSystemTimeToTzSpecificLocalTimeなど)

これらを使うことで、歴史的なタイムゾーン変更や将来的なルール変更にも追従しやすくなります

まとめ

mktimeは、ローカル時刻struct tmtime_tに変換し、同時に日付・時刻を正規化してくれる便利な関数です。

しかし、タイムゾーンや夏時間(DST)の影響を強く受けるため、その挙動を正しく理解していないと、1時間のズレや存在しない時刻への変換といったバグを生みやすくなります。

struct tmの初期化、tm_isdstの扱い、タイムゾーン環境(TZ)の影響を意識しつつ、必要に応じてUTC基準の計算や外部ライブラリの利用も検討することで、より堅牢な日時処理を実現できます。

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

URLをコピーしました!