前置き
サマータイムを導入するかしないかで巷が賑わっています。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_time
とlocal_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_time
にtime_zone
情報を組み合わせたものです。zoned_time
型のオブジェクトを生成するためには、make_zoned
というファクトリ関数を使用します。make_zoned
は、第1引数にタイムゾーン情報を、第2引数に時刻情報を取ります。zoned_time
はタイムゾーン情報を持つため、sys_time
から生成されたzoned_time
とlocal_time
から生成されたzoned_time
は比較することができます。
sys_time
とlocal_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のツールチェインを導入していきましょう!