C++
C++20

C++20でサマータイムを謳歌しよう

前置き

サマータイムを導入するかしないかで巷が賑わっています。C++20ではstd::chronoの拡張としてDate and time utilities - cppreference.comが導入されました。Date and time utilitiesは、Time Zone Databaseを利用してサマータイムにも対応しているのでC++でサマータイムを謳歌してみようと思います。

日本でも過去、1948年から1952年までの4年間サマータイムを導入していたことがあります。
夏時刻法 - Wikipediaの記事によると、1948年は、5月2日0:00時に1時間時刻を進めて、9月の第2土曜日の深夜24:00に1時間時計の針を戻したそうです。

他の記事(サマータイム導入には反対だが「日本夏時間」を試してみた - Qiita)で
言及されているように、Time Zone Databaseには、この4年間のサマータイムは記録されているようで、今回は過去のサマータイムの記録からC++でどのようにサマータイムを扱うべきか見ていこうと思います。

タイムゾーンやサマータイムについてはタイムゾーン呪いの書 - Qiitaで詳しい説明がされているのでご覧ください。

Date and time utilitiesの基礎

サマータイムの扱い方を見る前にDate and time utilitiesの基本的な使い方を見ていきます。このライブラリには5つの基本的なコンセプトがあります。

  • calendar: 日付を表現する型。
  • sys_time: Unix時間を表現する型。タイムゾーンとは独立。(java.time.Instant相当)
  • local_time: ローカル時間(あるタイムゾーンでの時間)を表現する型。タイムゾーンとは独立。(java.time.LocalDateTime相当)
  • time_zone: タイムゾーン。
  • zoned_time: 時間とタイムゾーンの組。(java.time.ZonedDateTime相当)

calendar

calendarは日付を表現することができ、日付の比較等が行うことができます。

void basic_calendar() {
    using namespace std::literals;
    using namespace date;

    constexpr year y{ 2018_y };
    constexpr month m{ aug };
    constexpr day d{ 9_d };
    constexpr year_month_day ymd{ 2018_y / aug / 9_d };
    static_assert(ymd == y / m / d);
    static_assert(2020_y / aug / 9_d != ymd);
    static_assert(2018_y <= 2020_y);
}

sys_time & local_time

sys_time & local_timeは、ほぼ同じものですが、sys_timeはUnix時間を、local_timeはローカルの時間を表現しています。両者とも同じ型同士は比較可能ですが、sys_timelocal_timeはタイムゾーン情報が無いため比較できません。また、sys_time/local_timeともに実体はクラス・テンプレートで時間精度でパラメータ化されています。

void basic_sys_or_local_time() {
    using namespace std::literals;
    using namespace date;

    constexpr auto hour_precision = sys_days{ 2018_y / aug / 9_d } + 2h; // hour単位の精度。
    std::cout << hour_precision << "\n"; // 2018-08-09 0200
    constexpr auto second_precision = sys_days{ 2018_y / aug / 9_d } + 2h + 2s; // second単位の精度。
    std::cout << second_precision << "\n"; // 2018-08-09 02:00:02
    constexpr auto hour_precision_local = local_days{ 2018_y / aug / 9_d } + 2h;
    std::cout << hour_precision_local << "\n"; // 2018-08-09 0200
    constexpr auto second_precision_local = local_days{ 2018_y / aug / 9_d } + 2h + 2s;
    std::cout << second_precision_local << "\n"; // 2018-08-09 02:00:02
    static_assert(hour_precision != second_precision);
    // static_assert(hour_precision != second_precision_local); sys_timeとlocal_timeは比較できず、コンパイル・エラー。
}

time_zone

time_zoneは、タイムゾーン情報を扱うためのIF群です。Timezone databaseファイルを指定したりリロードする機能も提供されています。

void basic_time_zone() {
    date::tzdb const& tz = date::get_tzdb(); // Timezone database
    date::time_zone const* const japan_time_zone = date::locate_zone("Asia/Tokyo"); // 日本の標準タイムゾーン。
}

zoned_time

zoned_timeは、sys_time/local_timetime_zone情報を組み合わせたものです。zoned_time型のオブジェクトを生成するためには、make_zonedというファクトリ関数を使用します。make_zonedは、第1引数にタイムゾーン情報を、第2引数に時刻情報を取ります。zoned_timeはタイムゾーン情報を持つため、sys_timeから生成されたzoned_timelocal_timeから生成されたzoned_timeは比較することができます。

sys_timelocal_timeでは表現する時間が違うことに注意してください。sys_timeはUnix Timeです。そのためsys_timeで生成した時刻とlocal_time(日本標準時間)で生成した時刻には9時間のオフセットがあります。

void basic_zoned_time() {
    using namespace std::literals;
    using namespace date;

    auto z0 = make_zoned("Asia/Tokyo", sys_days{ 2018_y / aug / 9_d });   // Unix時刻の2018年8月9日00:00 == 日本時刻の2018年8月9日09:00
    auto z1 = make_zoned("Asia/Tokyo", local_days{ 2018_y / aug / 9_d }); // 日本時刻の2018年8月9日00:00。
    assert(z0 != z1);
    std::cout << z0 << "\n"; // 2018-08-09 09:00:00 JST
    std::cout << z1 << "\n"; // 2018-08-09 00:00:00 JST
}

Date and time utilitiesのより詳細な例は作者のGithub Wikiページでみることができます。Examples and Recipes · HowardHinnant/date Wiki

サマータイム突入時

いよいよC++でサマータイムを扱っていこうと思います。サマータイムに突入した1948年5月1日から2日にかけての時刻の変化を見ていこうと思います。以下のコードでは日本時間1948年5月1日23時から1時間づつ時間を足しています。

void begining_day_light_saving_time() {
    using namespace std::literals;
    using namespace date;

    auto z = make_zoned("Asia/Tokyo", local_days{ 1948_y / may / 1 } +23h);

    for (int i = 0; i < 4; ++i) {
        std::cout << z << " = " << format("%F %T %Z\n", z.get_sys_time());
        z = z.get_local_time() + 1h;
    }
}

このコードを実行すると23時の表示がされた後、date::nonexistent_local_time例外でプログラムが終了していまいます。例外のメッセージを読むと、日本時間で5月2日0時から5月2日の1時は存在しないギャップだと言っています。日本時間の1948年5月2日0時はサマータイムの開始時です。サマータイムの調整のため、この瞬間に日本時間は1時間早くなりました。そのため日本時間上、1948年5月2日0時から1時までは存在しません。date::nonexistent_local_time例外が投げられました。

実行結果
1948-05-01 23:00:00 JST = 1948-05-01 14:00:00 UTC

例外で終了。例外のメッセージは、
1948-05-02 00:00:00 is in a gap between
1948-05-02 00:00:00 JST and
1948-05-02 01:00:00 JDT which are both equivalent to
1948-05-01 15:00:00 UTC

Unix時間はエポックからの連続した時間でギャップが存在しません。次は、同じく日本時間5月2日23時からUnix時間を進めてみようと思います。今度は例外は発生しません。Unix時間が連続して変化しているのに対して、日本時間はJSTからJDTに変わるタイミングで時間が飛んでいることが確認できます。

void begining_day_light_saving_time_with_sys_time() {
    using namespace std::literals;
    using namespace date;

    auto z = make_zoned("Asia/Tokyo", local_days{ 1948_y / may / 1 } +23h);

    for (int i = 0; i < 4; ++i) {
        std::cout << z << " = " << format("%F %T %Z\n", z.get_sys_time());
        z = z.get_sys_time() + 1h; // Unix時間を進める。
    }
}
実行結果
1948-05-01 23:00:00 JST = 1948-05-01 14:00:00 UTC
1948-05-02 01:00:00 JDT = 1948-05-01 15:00:00 UTC // 1時間飛んでいる。
1948-05-02 02:00:00 JDT = 1948-05-01 16:00:00 UTC
1948-05-02 03:00:00 JDT = 1948-05-01 17:00:00 UTC

サマータイム離脱時

次は、1948年9月第2土曜日の深夜24:00、サマータイム終了時の近辺の挙動を見てみます。日本時間、9月第2土曜日(11日)の22時から、1時間づつ時間を進めてみます。Date and time utilsでは、第2土曜(sat[2])のようなカレンダーの記法もサポートしています。

void finishing_day_light_saving_time() {
    using namespace std::literals;
    using namespace date;

    auto z = make_zoned("Asia/Tokyo", local_days{ 1949_y / sep / sat[2] } +22h);

    for (int i = 0; i < 4; ++i) {
        std::cout << z << " = " << format("%F %T %Z\n", z.get_sys_time());
        z = z.get_local_time() + 1h;
    }
}

このコードを実行すると、また例外でプログラムが終了します。今度はdate::ambiguous_local_timeという先ほどとは異なる例外です。例外のメッセージを読むと、日本時間の"1948年9月11日23時"という表現はあいまいであると言っています。日本時間の1948年9月11日24時はサマータイムの終了の瞬間です。サマータイムの時刻調整のため深夜24時に時計の針は1時間戻されました。日本時間11日の23~24時の間は、サマータイム終了前と終了後で2回同じ時刻を刻んでいることになります。

実行結果
1948-09-11 22:00:00 JDT = 1948-09-11 12:00:00 UTC

例外で終了。例外のメッセージ。
1948-09-11 23:00:00 is ambiguous.  It could be
1948-09-11 23:00:00 JDT == 1948-09-11 13:00:00 UTC or
1948-09-11 23:00:00 JST == 1948-09-11 14:00:00 UTC

同じサマータイムの終了近辺でUnix時間を進めて確認します。サマータイムの終了時点(JDTからJSTに切り替わる時点)時計の針が23:00時に戻っていることが確認できます。

void finishing_day_light_saving_time_with_sys_time() {
    using namespace std::literals;
    using namespace date;

    auto z = make_zoned("Asia/Tokyo", local_days{ 1948_y / sep / sat[2] } +22h);

    for (int i = 0; i < 4; ++i) {
        std::cout << z << " = " << format("%F %T %Z\n", z.get_sys_time());
        z = z.get_sys_time() + 1h;
    }
}
実行時間
1948-09-11 22:00:00 JDT = 1948-09-11 12:00:00 UTC
1948-09-11 23:00:00 JDT = 1948-09-11 13:00:00 UTC
1948-09-11 23:00:00 JST = 1948-09-11 14:00:00 UTC // 23時からもう一度時計を進める。
1948-09-12 00:00:00 JST = 1948-09-11 15:00:00 UTC

最後に

今回はC++20でサマータイムを扱う方法を見てきました。

ローカル時間local_timeを使用して時刻の調整を行うと、ローカル時間のギャップや曖昧さのため例外が発生することが分かりました。また、以下のようにサマータイムの開始・終了を跨いだ時間の計算では、sys_timeを使用した計算とlocal_timeを使用した計算では計算結果が異なります。速度など単位時間当たりの計算でlocal_timeを使用している場合は意図しない値になる可能性があるので注意が必要です。

void diff_across_day_light_saving() {
    using namespace std::literals;
    using namespace date;

    auto beforeJDT = make_zoned("Asia/Tokyo", local_days{ 1948_y / may / 1 } + 23h);
    auto afterJDT = make_zoned("Asia/Tokyo", local_days{ 1948_y / may / 2 }  + 1h);

    std::cout << date::format("%H:%M\n", afterJDT.get_sys_time() - beforeJDT.get_sys_time()); // 1時間差。
    std::cout << date::format("%H:%M\n", afterJDT.get_local_time() - beforeJDT.get_local_time()); // 2時間差。
}

まとめると時刻に対する演算ではsys_timeを使用して、ユーザへの表示ではlocal_timeを使用するのが良いようです。

Date and time utilsは表現力が高く、型安全なライブラリです。サマータイムも扱えることが分かったので、サマータイム導入を隠れ蓑にしてC++20のツールチェインを導入していきましょう!

参考リンク