C++20 Calender and timezone library

  • 2018/03/27 01:50
  • C++
この記事は, 旧ブログから移植された記事です. よって, その内容として, 旧ブログに依存した文脈が含まれている可能性があります. 予めご了承下さい.

本エントリは, 元々 P0355R5 を参考にまとめを行った記事であるが, その後 P0355R7

  • sunといった曜日を表すリテラルが全てSunday, mayといった月を表すリテラルが全てMayといった形式に変更され, またこれらとstd::chrono::last_spec型のlast1std::literals::chrono_literals名前空間下からstd::chrono名前空間下に移動された.
  • system_clock::to_time_tsystem_clock::from_time_tを Deprecated としていたが, Deprecated でなくなった.

といった他に, 細かい文面の改修や, constexprがつけられるなどの変更が加えられたため, 本エントリにおいても, それに従い該当箇所を改変している (差分を示すことも考えたが, ただ見難くなるように感じたため, そのようなことはしなかった). これらの変更には対応したつもりだが, 細かい厳密な記述に関しては, やはり P0355 の最新リビジョンを追って確かめてほしい(そして, 間違った箇所があれば指摘くださると嬉しい).

要旨

先週, 米国フロリダ州ジャクソンビルで ISO C++ 委員会によって, C++技術仕様(TS, 実験的機能ブランチ) と次の国際標準(IS) C++20 に関する作業が行われた. 同会議で Reddit で紹介されているように, C++20 にいくつかの機能が追加された. そのうちの 1 つである, Calender and timezone library に関する新機能, 概要のメモ.

  • 全機能の網羅性を主軸としたエントリではない(が, 独断と偏見により重要に感じた内容は結果的に網羅してしまっている部分もある)ため注意
  • 以下, 特に断らない限り, 全てのコード片においてusing namespace std::chrono;, using namespace std::chrono_literals;がされているとする.
  • 以下, 特に断らない限り, 各コードブロック以外で言及される内容といった記述については, 名前空間 std::chronoを省略する.

設計

以下の内容が追加される.

  • <chrono>に対するカレンダーおよびタイムゾーンライブラリをサポートするための最小限の拡張
  • Proleptic Gregorian calendar(civil calendar)
  • IANA Time Zone Database を基にしたタイムゾーンライブラリ
  • 分数秒, タイムゾーンの略語, UTC オフセットの完全サポートおよびstrftimeのようなフォーマッティング機能
  • IANA Time Zone Database でサポートされている, 閏秒を計算するための複数のクロック

カレンダー

本ライブラリ機能によって, 例えば 2016 年を次のように表現することができる.

auto y = std::chrono::year{2016};
auto y = 2016y;

このとき, yearpartial-calendar-type (部分カレンダー型) である. 他の部分カレンダー型と組み合わせることで, year_month_dayなどの full-calendar-type (フルカレンダー型) を生成することができる. フルカレンダー型は, 1 日の制度を持つタイムポイントであり, 部分カレンダー型であるyear, month, day で構成される. これらからyear_month_dayが構築されると, 内部で計算は一切起きず, 唯一起きることは, year_month_day が内部でそれらを格納するということだけである.

std::chrono::year_month_day ymd1{2016y, month{5}, day{29}};

この場合, すべての入力がコンパイル時定数であるので, constexpr にすることができる. さらに, operator/がオーバーロードされているため, 従来の日付の構文が利用できる.

constexpr std::chrono::year_month_day ymd1{2016y, month{5}, day{29}};
constexpr auto ymd2 = 2016y/May/29d;
constexpr auto ymd3 = May/29d/2016y;
constexpr auto ymd4 = 29d/May/2016y;
static_assert(ymd1 == ymd2);
static_assert(ymd2 == ymd3);
static_assert(ymd3 == ymd4);
static_assert(ymd1.year() == 2016y);
static_assert(ymd1.month() == May);
static_assert(ymd1.day() == 29d);
ここで, Mayは月を表現するmonth型のオブジェクトであり, 他にJan, Feb, Mar, Apr, Jun, Jul, Aug, Sep, Oct, Nov, Decが, 次のように定義される(クリックで展開).

カレンダーライブラリは, 例えばスカラ型で直接日付を指定するといったことはなく, 明示的な型指定による表現によって実現する. なお, 部分カレンダー型(year, month に加えてday型)は, すべて Strong ordering2 を満たし, 加えて以下のメンバ関数をもつ.

  • デフォルトコンストラクタ, unsigned型(year型のみint型)の値を受け付けるコンストラクタ
  • 各型の単位においてそれを前後に進める, {前|後}置{イン|デ}クリメント演算子
  • 各型の単位で計算を行う+, -の二項演算子
  • 二項演算子と同様の計算を行い自身に代入する+=, -= の複合代入演算子
  • unsigned型(year型のみint型)の値への明示的な変換(constexpr explicit operator unsigned() const noexcept;)
  • 指定された日付が各単位で適切であるかどうかをチェックするok

year型は, これに加えて, is_leap, min, max メンバ関数をもつ. is_leapは, 指定された年が閏年であるか判定できる. min, maxは内部型の最小値と最大値を返す. また, 上記の通りyといったリテラル接尾語が定義される.
またday型は, dといったリテラル接尾語が定義される.

フルカレンダー型は, sys_daysという型へ変換できる. これは, 次のように定義されている.

constexpr year_month_day::operator sys_days() const noexcept;
フルカレンダー型は,sys_days型に変換することで,system_clock::time_pointファミリとの間で変換でき, これにより完全な相互運用が可能である. sys_days 次のように定義される(クリックで展開).

加えて, sys_daysには次の特性がある.

  • sys_daysは, system_clock::time_pointがマイクロ秒, またはナノ秒のカウントだけであるのと同様に, system_clockの基点(エポック)からの日数を示す.
  • sys_daysは, 切り捨てエラーなしで暗黙的にsystem_clock::time_pointに変換される.
  • system_clock::time_pointは, 切り捨てエラーが含まれるため, 暗黙的にsys_daysに変換されない.
  • system_clock::time_point_cast またはfloorを使用した明示的な変換によってsystem_clock::time_pointからsys_daysへ変換することができる.

内部で保持する部分カレンダー型year, month, day をそれぞれy_, m_, d_ としたとき, year_month_dayからsys_daysへの変換時には(すなわち上記のoperator sys_days()の呼び出し),

  • year_month_day::ok()trueの場合, sys_daysの基点から*thisまでの日数を保持するsys_daysを返す
  • そうでない場合, y_.ok() && m_.ok() == true ならば sys_days{y_/m_/last} から days(duration<int32_t, ratio_multiply<ratio<24>, hours::period>>) の数だけsys_days{y_,m_,last}.day()からオフセットされたsys_daysを返す
  • そうでない場合, 未規定である

ここでyear_month_day::okは, y_.ok() && m_.ok() == true \land 1d \leq d_ \leq (y_/m_/last).day() であるとき true を, そうでない場合, false を返すメンバ関数である.

constexpr std::chrono::system_clock::time_point tp = std::chrono::sys_days{2016y/May/29d}; // Convert date to time_point
static_assert(tp.time_since_epoch() == 1'464'480'000'000'000us);
constexpr auto ymd = std::chrono::year_month_day{std::chrono::floor<days>(tp)}; // Convert time_point to date
static_assert(ymd == 2016y/May/29d);
constexpr auto tp = std::chrono::sys_days{2016y/May/29d} + 7h + 30min; // 2016-05-29 07:30 UTC
static_assert(year_month_day{sys_days{2017y/January/0}}  == 2016y/December/31);
static_assert(year_month_day{sys_days{2017y/January/31}} == 2017y/January/31);
static_assert(year_month_day{sys_days{2017y/January/32}} == 2017y/February/1);
上述した, daysの他に, weeks, months, years 次のように定義される(クリックで展開).
  • days, weeks, months, years型は, それぞれ少なくとも $$40000 年の範囲をカバーする.
  • hoursのリテラル接尾語がh, minutesのリテラル接尾語がmin, というように, 今までのリテラル接尾語はduration型へ対応していたが, 新規に追加される y, dといったリテラル接尾語は, years, daysに対応するリテラル接尾語ではなく, 上述したように, year, dayの部分カレンダー型に対応するリテラル接尾語である.
  • 1 年を, 365.2425 日(グレゴリオ暦の平均長)と定義し, 1 月を, 30.436875 日(112)(\dfrac{1}{12}) と定義するため, システム時刻(time_point)を利用した算出結果と, year_month_day を利用した算出結果は異なる.
constexpr auto date1 = sys_days{1997y/May/30d} - months{5}; // 1996-12-28 19:34:30
constexpr auto date2 = sys_days{1997y/December/29d} - years{1}; // 1996-12-28 18:10:48

現実のカレンダーの利用方法として, たとえば「2016 年の 5 月 29 日」を, 「2016 年の 5 月第 5 日曜日」ということもよくあり, これを表現することもできる.

constexpr std::chrono::system_clock::time_point tp = std::chrono::sys_days{Sunday[5]/May/2016}; // Convert date to time_point
static_assert(tp.time_since_epoch() == 1'464'480'000'000'000us);
constexpr auto ymd = std::chrono::year_month_weekday{std::chrono::floor<days>(tp)}; // Convert time_point to date
static_assert(ymd == Sunday[5]/std::chrono::May/2016);
static_assert(2016y/May/29d == std::chrono::year_month_day{Sunday[5]/May/2016});

constexpr auto wdi = Sunday[5]; // wdi is the 5th Sunday of an as yet unspecified month
static_assert(wdi.weekday() == Sunday);
static_assert(wdi.index() == 5);
static_assert(std::is_same<decltype(Sunday), std::chrono::weekday>::value);
static_assert(std::is_same<decltype(wdi.index()), std::chrono::weekday_indexed>::value);
ここで, Sundayweekday型であり, 日曜日を表現するリテラルとして定義され, 他にもMonday, Tuesday, Wednesday, Thursday, Friday, Saturday 次のように定義される(クリックで展開).

weekday型は, Strong equality3 を満たし, 加えて, 次のメンバ関数を持つ.

  • デフォルトコンストラクタ, unsigned, sys_days, 後に取り上げているlocal_days型のオブジェクトを受け付けるコンストラクタ
  • 曜日を前後に進める{前|後}置{イン|デ}クリメント演算子
  • weekday, days型のオブジェクトを受け付けて曜日の計算を行う+, -の二項演算子
  • 二項演算子と同様の計算を行い自身に代入する+=, -= の複合代入演算子
  • 指定された曜日が適切であるかどうかをチェックするok
  • operator []

operator []は, unsigned型を引数として呼び出すと, weekday_indexed型のオブジェクトが返され, last_spec型のオブジェクトを引数として呼び出すと, weekday_last型のオブジェクトが返される.

  • weekday_indexed型は, 月の第 1, 第 2, 第 3, 第 4 または第 5 曜日を表すために使用され, 上記の通り, weekdayメンバ関数, indexメンバ関数を持つ他, okメンバ関数を持つ.
  • weekday_last型は, 月の最後のweekdayを表すために使用され, weekdayメンバ関数, okメンバ関数を持つ,
  • last_spec型は, 最終日を表すために使用され, 同型のオブジェクトlastchrono名前空間下に定義される.

例えば次のようにして, ある月の最終日, 最終 XX 曜日などを表現することができる.

auto today = std::chrono::year_month_day{std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now())};
auto last_day = today.year()/today.month()/last; // last day of this month
auto last_Sunday = today.year()/today.month()/Sunday[last]; // last Sundayday of this month

static_assert(std::is_same<decltype(Sunday[5]), std::chrono::weekday_indexed>::value);
static_assert(std::is_same<decltype(Sunday[last]), std::chrono::weekday_last>::value);

他に, 年を未指定とし, 特定の月日を表す, month_day, 月の最終日を表すmonth_day_last, NN 番目の曜日を表すmonth_weekday, 月の最終曜日を表すmonth_weekday_lastと, 日を未指定とし, 特定の年月を表す, year_month, 前述したyear_month_day, 特定の年月の最終日を表すyear_month_day_last, 特定の年月の NN 番目の曜日を表すyear_month_weekday, 特定の年月の最終曜日を表すyear_month_weekday_last が提供される.

constexpr auto md = February/1d;
static_assert(std::is_same<decltype(md), std::chrono::month_day>::value);
constexpr auto mdl = February/last;  // mdl is the last day of February of an as yet unspecified year
static_assert(mdl.month() == February);
static_assert(std::is_same<decltype(mdl), std::chrono::month_day_last>::value);
constxpr auto mw = February/Sunday[5];
static_assert(std::is_same<decltype(mw), std::chrono::month_weekday>::value);
constexpr auto mwl = February/Sunday[last];
static_assert(std::is_same<decltype(mwl), std::chrono::month_weekday_last>::value);
constexpr auto ym = 2016y/February;
static_assert(std::is_same<decltype(ym), std::chrono::year_month>::value);
constexpr auto ymd = 2016y/February/1d;
static_assert(std::is_same<decltype(ymd), std::chrono::year_month_day>::value);
constexpr auto ymdl = 2016y/February/last;
static_assert(std::is_same<decltype(ymdl), std::chrono::year_month_day_last>::value);
constexpr auto ymw = 2016y/February/Sunday[1];
static_assert(std::is_same<decltype(ymw), std::chrono::year_month_weekday>::value);
constexpr auto ymwl = 2016y/February/last;
static_assert(std::is_same<decltype(ymwl), std::chrono::year_month_day_last>::value);

フルカレンダー型, 部分カレンダー型, sys_days型の全てで, operator <<のオーバーロードによる IO ストリームへの出力機能が提供される他, 非メンバ関数として, to_stream, from_stream が提供される. これらはそれぞれ, 指定されたフォーマットの通りに出力する機能と, 指定されたフォーマットを使用して入力ストリームから解析する機能を持つ.

std::cout << std::chrono::sys_days{Sunday[5]/May/2016} << std::endl; // 2016-05-29
std::chrono::to_stream(std::cout, "%b/%d/%Y %A %T", std::chrono::sys_days{2016y/May/29d} + 30min); // May/29/2016 Sunday 00:30:00

auto is = std::istringstream{"2016-5-26"};
auto tp = std::chrono::sys_days{};
std::chrono::from_stream(in, "%F", tp);
if (!is.fail()) std::cout << tp << std::endl; // 2016-05-26

また, time_of_dayクラスが提供される. これは, hours, minutes, seconds, duration<Rep, Period> の 4 つに対する特殊化が行われており, それぞれ午前 0 時からの時間, 時間:分, 時間:分:秒, 時間:分:秒:XX といった書式設定ができる.

std::chrono::time_of_day<std::chrono::hours> todh(1h);
todh.make12();
std::cout << todh << '\n'; // 1am
todh.make24();
std::cout << todh << '\n'; // 0100

std::chrono::time_of_day<std::chrono::minutes> todm(1h + 30min);
todm.make12();
std::cout << todm << '\n'; // 1:30am
todm.make24();
std::cout << todm << '\n'; // 01:30

std::chrono::time_of_day<std::chrono::seconds> tods(1h + 30min + 30s);
tods.make12();
std::cout << tods << '\n'; // 1:30:30am
tods.make24();
std::cout << tods << '\n'; // 01:30:30

std::chdono::time_of_day<std::chrono::milliseconds> todms(1h + 30min + 30s + 30ms);
todms.make12();
std::cout << todms << '\n'; // 1:30:30.030am
todms.make24();
std::cout << todms << '\n'; // 01:30:30.030

タイムゾーン

タイムゾーンライブラリは, IANA Time Zone Database のパーサーとして提供される4. IANA Time Zone Database には, UTC からのオフセットと地域の省略名5が含まれており, さらに該当する場合, 夏時間(サマータイム)のルールも含まれる. これを表現した, tzdb, またバージョンごとのtzdbのリストとなっているtzdb_listを介して, 任意のtzdbにアクセスすることができる. tzdb_listはシングルトンであり, 非メンバ関数get_tzdb_listからその参照を得て利用する. 関連する宣言を下記に抜粋する(クリックで展開).
  • local_timelocal_tという空の擬似クロック型が指定されており, これは当然 C++ の Clock ライブラリコンセプトを満たしていないが, 未定義のタイムゾーンに関するローカル時刻であることを示す.
  • sys_info構造体は, time_zonesys_time, またはlocal_timeの組み合わせ, およびzoned_timeから取得することができる. 実質的には, time_zonesys_timeのペアであり, 低レベル API を表現する. sys_timeからlocal_timeへの通常の変換では, 暗黙的にこの構造体が使用される.
    • begin, endフィールドは, time_zoneおよびtime_pointについてoffsetabbrev[[begin, end)) であることを示す.
    • offsetフィールドは, 関連するtime_zoneおよびtime_pointに有効な UTC オフセットを示す(offset = local_time - sys_time).
    • saveフィールドは, 通常local_timesys_timeの変換では必要のない“余分な”情報であるが, サマータイムの対応で必要となる. save != 0minの場合, この sys_info はサマータイムの時間帯にあると判断する. offset - saveによって, このtime_zoneがサマータイムに対応できていない可能性を導出できる. しかし, この情報は正式なものではなく, そのような情報を確実に取得する唯一の方法は, save == 0minであるsys_infoを返すtime_pointと, 確認したいtime_zoneを照会することである.
    • abbrevフィールドは, 関連するtime_zoneおよびtime_pointに使用される現在の略語を示す. 略語は, time_zone間で一意でないため, 略語をtime_zoneと UTC のオフセットに確実にマッピングすることはできない
    • IO ストリームに対応している. zoned_time zt = { "Asia/Tokyo", system_clock::now() }; std::cout << zt.get_info() << '\n';
  • local_info構造体は, 低レベル API を表す. local_timeからsys_timeへの通常の変換では, 暗黙的にこの構造体が使用される.
    • local_timeからsys_timeへの変換が唯一(サマータイムでない)で, result == unique である場合, firstが正しいsys_infoがセットされ, secondが 0 で初期化される.
    • 変換が存在しない(result == noexistent)6場合, firstlocal_timeの直前で終了するsys_infoがセットされ, secondlocal_timeの直後に開始するsys_infoがセットされる.
    • 変換が曖昧(result == ambiguous)7な場合, firstlocal_timeの直後に終了するsys_infoがセットされ, secondlocal_timeの直前で開始するsys_infoがセットされる.
    • IO ストリームに対応している. std::cout << get_tzdb().current_zone()->get_info(local_days{2016y/May/29d}) << '\n';
  • time_zone構造体は, 特定の地域の全てのタイムゾーン遷移を表現する. データベースの初期化の過程で, 現在地のタイムゾーン, およびタイムゾーンの情報をなんらかの方法89で構築する. Strong ordering10 を満たす.
    • nameメンバ関数によって, time_zoneの名前11を取得できる.
    • get_infoメンバ関数によって, sys_info, local_infoを取得できる.
    • to_sysメンバ関数によって, sys_time, local_timeを取得できる.
      • time_zone::to_sys(local_time<Duration> tp) const;: 少なくともsecondsと同じぐらいのsys_timeであり, 引数の精度がさらに高ければそれに合わせられる. tpからsys_timeへの変換が曖昧12である場合, ambiguous_local_time例外をスローする13. tpからsys_timeへの変換が存在しない14場合, nonexistent_local_time例外をスローする15.
      • time_zone::to_sys(local_time<Duration> tp, choose z) const;: 少なくともsecondsと同じぐらいのsys_timeであり, 引数の精度がさらに高ければそれに合わせられる. tpからsys_timeへの変換が曖昧16があいまいである場合, z == choose::earliestの場合は, サマータイム以前のsys_timeを返し, z == choose::latestの場合は, サマータイム以後のsys_timeを返す. tp が 2 つの UTC time_pointの間に存在しない時間を表す場合, 2 つの UTC time_pointは同じになり, UTC time_pointが返される.
      • time_zone::to_local(sys_time<Duration> tp) const;: tp と自身のtime_zoneに関連づけられたlocal_timeを返す.
  • tzdbは, 前述した通り, タイムゾーンデータベースを表現する.
    • versionは, そのデータベースバージョンを表す. zones, links, leapsは, 検索の高速化のために, 構築時に昇順ソートされる.
    • locate_zoneメンバ関数から, 与えられたstring_viewオブジェクトとname()が等価であるtime_zoneが見つかった場合, そのtime_zoneへのポインタを取得できる. そうでない場合, 与えられたstring_viewlink.name()(ここで, linkは後述しているtime_zoneの代替名を表現するクラスである)が等価であるlinkが見つかった場合, zone.name() == link.target()time_zoneポインタが取得できる. そうでない場合, runtime_error例外を送出する. 例外送出以外でこの関数から処理が返るとき, 返される戻り値は必ず有効なtime_zoneへのポインタである.
    • current_zoneメンバ関数から, コンピューターに設定されたローカルタイムゾーンを取得できる.
  • tzdb_listは, tzdbのアトミックポインターをもつ, tzdbのシングルトンリストである. 複数のバージョンのデータベースを, 同リストを介して一度に使用することができる. 例:for (auto&& v : get_tzdb_list()) { std::cout << v << '\n'; }
    • frontメンバ関数によって, 先頭tzdbの参照を得ることができる. これは, reload_tzdb非メンバ関数に対してスレッドセーフである.
    • erase_afterメンバ関数によって, 与えられたイテレータの後に参照するtzdbを消去する. 消去された要素の次の要素を指すイテレータが返される. そのような要素が存在しない場合, メンバ関数endを呼び出し, その結果を返す. なお, ここで消去されたtzdbを参照することを除いて, ポインター, 参照, イテレータは無効にならない. また, メンバ関数beginを呼び出し, それによって参照されるtzdbを消去することはできない.
    • beginメンバ関数によって, コンテナ内の最初のtzdbを参照するイテレータ取得できる. cbeginメンバ関数はbeginメンバ関数のconst版である.
    • endメンバ関数によって, コンテナ内の最後のtzdbより 1 つ後ろの位置を参照するイテレータを取得できる. cendメンバ関数はendメンバ関数のconst版である.
    • get_tzdb_list非メンバ関数によって, 同リストの参照を得ることができる. 同メンバ関数への呼び出しがデータベースへの最初のアクセスである場合, データベースを初期化する. この呼び出しによってデータベースが初期化された場合, tzdbを一つ持つtzdb_listが構築される. 同メンバ関数を一度に複数のスレッドから呼び出しても競合せず, スレッドセーフである. 何らかの理由で有効なリストの参照を返せず, 1 つ以上の有効なtzdbを含む場合, runtime_error例外を送出する.
    • get_tzdb非メンバ関数によって, 同リストの先頭tzdbの参照を得ることができる(get_tzdb_list().front()).
    • locate_zone非メンバ関数によって, 次の値を得ることができる. なお, これがデータベースへの最初のアクセスである場合, データベースを初期化する. get_tzdb().locate_zone(tz_name);
    • current_zone非メンバ関数によって, 次の値を得ることができる. get_tzdb().current_zone();
ローカルタイムゾーンデータベースは、アプリケーションがデータベースに最初にアクセスするとき, たとえばcurrent_zone()を介して実装によって提供される. アプリケーションが実行されている間, 実装はタイムゾーンデータベースの更新を選択することがある. このアップデートは, アプリケーションによって次に挙げる関数を呼び出さない限り, アプリケーションに影響を与えることはない. この潜在的に更新されたタイムゾーンデータベースは, リモートタイムゾーンデータベースと呼ぶ. 次のように定義される(クリックで展開).
  • reload_tzdb非メンバ関数は, 最初にリモートタイムゾーンデータベースのチェックを行い, ローカルデータベースとリモートデータベースのバージョンが同じである場合はなにもしない. それ以外の場合, リモートデータベースは, get_tzdb_list非メンバ関数によってアクセスされるtzdb_listの先頭にプッシュされる. いずれの場合も, get_tzdb_list().front() が返される. この関数は, get_tzdb_list().front()get_tzdb_list().erase_after()に対してスレッドセーフである. 何らかの理由で有効なtzdbの参照が戻されない場合, runtime_error例外が送出される.
  • remote_version非メンバ関数は, 最新のリモートデータベースバージョンの文字列(std::string)を返す. リモートバージョンが利用できない場合, 空の文字列が返される. 空でない場合, これをget_tzdb_list().versionと比較して, ローカルデータベースとリモートデータベースが同等かどうかをチェックできる.
zoned_traits, zoned_timeを利用することで, sys_days, local_daysといったtime_pointtzdbデータベースと関連付けることができる. 次のように定義される(クリックで展開).

zoned_traitsによって, zoned_timeのデフォルトコンストラクタの動作をカスタマイズすることができる.

  • zoned_traits<const time_zone*>::default_zone(); は, std::chrono::locate_zone("UTC") を返す.
  • zoned_traits<const time_zone*>::locate_zone(string_view name); は, std::chrono::locate_zone(name) を返す.
zoned_timeは, Durationの精度で, time_zonetime_pointの論理区切りを表す. Strong equality17を満たす. 次のように定義される(クリックで展開).
  • invariant なzoned_time<Duration>は常に有効なtime_zoneを参照し, 曖昧でない時間を表す.
  • デフォルコンストラクタは, zone_zoned_traits::default_zone()で初期化し, tp_をデフォルト構築してzoned_timeを構築する.
  • コピーコンストラクタは, 関連するtime_zoneを転送する. Durationnoexceptコピーコンストラクタである場合, zoned_time<Duration>noexceptコピーコンストラクタである.
  • zoned_time(const sys_time<Duration>& st): zone_zoned_traits::default_zoneで初期化し, tp_stで初期化してzoned_timeを構築する.
  • zoned_time(TimeZonePtr z): std::move(z)zone_を初期化し, zoned_timeを構築する. このとき, zは有効なtime_zoneを指していなければならない.
  • zoned_time(string_view name): zoned_traits::locate_zone(name)zone_を初期化し, tp_をデフォルト構築してzoned_timeを構築する.
  • zoned_time(const zoned_time<Duration2, TimeZonePtr>& y) noexcept: x == yとなるzoned_time, xを構築する.
  • zoned_time(TimeZonePtr z, const sys_time<Duration>& st): zone_std::move(z)で初期化し, tp_stで初期化してzoned_timeを構築する. このとき, zは有効なtime_zoneを指していなければならない.
  • zoned_time(string_view name, const sys_time<Duration>& st): {zoned_traits<TimeZonePtr>::locate_zone(name), st}と同等の構築を行う.
  • zoned_time(TimeZonePtr z, const local_time<Duration>& tp): zone_std::move(z)で初期化し, tp_zone_->to_sys(t)で初期化してzoned_timeを構築する. このとき, zは有効なtime_zoneを指していなければならない.
  • zoned_time(string_view name, const local_time<Duration>& tp): {zoned_traits<TimeZonePtr>::locate_zone(name), tp}と同等の構築を行う.
  • zoned_time(TimeZonePtr z, const local_time<Duration>& tp, choose c): zoned_std::moev(z)で初期化し, tpzone_->to_sys(t, c)で初期化してzoned_timeを構築する. このとき, zは有効なtime_zoneを指していなければならない.
  • zoned_time(string_view name, const local_time<Duration>& tp, choose c): {zoned_traits<TimeZonePtr>::locate_zone(name), tp, c}と同等の構築を行う.
  • zoned_time(TimeZonePtr z, const zoned_time<Duration2, TimeZonePtr2>& y): zone_std::move(z)で初期化し, tp_z.tp_で初期化してzoned_timeを構築する. このとき, zは有効なtime_zoneを指していなければならない.
  • zoned_time(TimeZonePtr z, const zoned_time<Duration2, TimeZonePtr2>& y, choose): {z, y}と同等の構築を行う. このとき, zは有効なtime_zoneを指していなければならない. chooseパラメータを渡すことができるが, これによって挙動が変わることはない.
  • zoned_time(string_view name, const zoned_time<Duration>& y): {zoned_traits<TimeZonePtr>::locate_zone(name), y}と同等の構築を行う.
  • zoned_time(string_view name, const zoned_time<Duration>& y, choose c): {locate_zone(name), y, c}と同等の構築を行う. chooseパラメータを渡すことができるが, これによって挙動が変わることはない.
  • operator=(const local_time<Duration>& lt): 代入後, get_local_time() == ltとなるよう代入し*thisを返す. この代入は, get_time_zoneの戻り値には影響しない.
  • operator sys_time<duration>() const: get_sys_time()を返す.
  • operator local_time<duration>() const: get_local_time() を返す.
  • get_time_zoneメンバ関数によって, zone_のポインタを取得できる.
  • get_local_timeメンバ関数によって, 構築時に設定されたタイムゾーンでのlocal_timeオブジェクトを取得できる(retunr zone_->to_local(tp_);).
  • get_sys_timeメンバ関数によって, 構築時に設定されたタイムゾーンでのsys_timeオブジェクトを取得できる(return tp_;).
  • get_infoメンバ関数によって, 構築時に設定されたタイムゾーンでのsys_infoオブジェクトを取得できる(return zone_->get_info(tp_);).

同ライブラリを利用して, 例えば次のように, ある日時の東京の時間帯を得ることができる18.

auto tp1 = std::chrono::sys_days{2016y/May/29d} + 7h + 30min + 6s + 153ms; // 2016-05-29 07:30:06.153 UTC
std::chrono::zoned_time zt1 = {"Asia/Tokyo", tp1};
std::cout << zt1 << '\n'; // 2016-05-29 16:30:06.153 JST

auto tp2 = std::chrono::local_days{2016y/May/29d} + 7h + 30min + 6s + 153ms; // 2016-05-29 07:30:06.153 JTC
auto zt2 = std::chrono::zoned_time{"Asia/Tokyo", tp2};
std::cout << zt << '\n'; // 2016-05-29 07:30:06.153 JST
leapは, タイムゾーンデータベースの初期化時に構築され, タイムゾーンデータベースに格納されるクラスであり, 主に閏秒を扱うクラスである. 同クラスは, Strong ordering19を満たす. 次のように定義される(クリックで展開).
  • dateメンバ関数によって, date_を取得できる. date_には閏秒挿入の日付が格納されている.
  • 閏秒挿入の全ての日付をfor (auto& l : get_tzdb().leaps) std::cout << l.date() << '\n';で確認できる.

またタイムゾーンデータベースの構築時に作成される, time_zoneの代替名を表現するlinkというクラスも提供される.

Clock

新たに utc_clock, tai_clock, gps_clock, file_clock の 4 つのクロック, また, そのtime_point型のエイリアス(utc_time, utc_seconds, tai_time, tai_seconds, gps_time, gps_seconds) が提供される.

  • utc_clock は, 協定世界時(UTC)を表現するクロックであり, 1970 年 1 月 1 日木曜日午後 00:00:00 分からの時間を測定する. これには, 閏秒が含まれる.
  • tai_clockは, 国際原始時計(TAI)を表現するクロックであり, 1958 年 1 月 1 日 00:00:00 からの時間を測定し, この日の UTC(1957-12-31 23:59:50 UTC)よりも 10 秒前にオフセットされている. これには, 閏秒が含まれない20.
  • gps_clockは GPS 時刻を表現するクロックであり, UTC 1980 年 1 月 6 日 00:00:00 からの時間を測定する. 閏秒は含まれない21.
  • file_clockは, C++20 で追加されたエイリアス, using file_time_type = std::chrono::time_point<std::chrono::file_clock>;で利用されるファイルクロックである22.

試用

同ライブラリを利用した任意月のカレンダーを出力するプログラムは, 既にあったのだが, 特別何か別のものは思いつかないので, とりあえず, 任意年の全ての月のカレンダーを出力するプログラムを書いて試用.

#include <algorithm>
#include <array>
#include <chrono>
#include <iostream>
#include <iomanip>
#include <utility>

namespace ns {

template <class> struct weeks_init;
template <std::size_t... s>
struct weeks_init<std::index_sequence<s...>> {
    constexpr weeks_init() = default;
    constexpr std::array<std::chrono::weekday, sizeof...(s)> operator()() const noexcept { return {{ std::chrono::weekday{s}... }}; }
};
constexpr auto weeks = weeks_init<std::make_index_sequence<7>>()();

inline void print_weeks(std::ostream& os)
{
    namespace sc = std::chrono;
    std::copy(std::begin(weeks), std::end(weeks), std::ostream_iterator<sc::weekday>(os, "  "));
    os << '\n';
}

} // namespace ns

struct print_calendar_year {
    explicit constexpr print_calendar_year(std::chrono::year y) noexcept
        : y_(y) {}

    friend std::ostream& operator<<(std::ostream& os, const print_calendar_year& this_)
    {
        using namespace std::chrono_literals;
        namespace sc = std::chrono;
        constexpr int width = 5;

        for (unsigned i = 1u, uweek = static_cast<unsigned>(sc::weekday{sc::sys_days{this_.y_/sc::January/1d}}); i <= 12u; ++i) {
            auto lastday = (this_.y_/sc::month{i}/sc::last).day();
            os << std::setw(ns::weeks.size() * width / 2) << sc::month{i} << '\n';
            ns::print_weeks(os << std::setw(width));

            unsigned k = 0;
            for (; k < uweek; ++k) os << std::setw(width) << " ";
            for (sc::day d{1}; d <= lastday; ++d) {
                os << std::setw(width) << static_cast<unsigned>(d);
                if (++k > 6) {
                    k = 0;
                    os << '\n';
                }
            }
            if (k) os << '\n';
            uweek = k;
        }
        return os;
    }
private:
    std::chrono::year y_;
};

int main()
{
    std::cout << print_calendar_year{std::chrono::year{2000}} << std::endl;
}

実行結果.

タイムゾーンに関するサンプルは, 元の実装のドキュメントで多く取り上げられている. フライトタイムの計算や, IANA タイムゾーンデータベースを利用しないカスタムタイムゾーンを作成する例などが掲示されている.

感想

  • とてもよく作り込まれていて, 使いやすそうに感じる.
  • C++ にこのような高レベル API が導入されるのは, 少し新鮮.

  1. yohhoy さんにlastの属する名前空間に関する追加情報を頂いた. ありがとうございます.↩︎

  2. この一つ前の ISO C++ 委員会による国際会議で C++20 に追加された P0515 Consistent comparison で挙げられている comparison category types での呼称を用いている. 参照: Consistent/three-way comparison↩︎

  3. この一つ前の ISO C++ 委員会による国際会議で C++20 に追加された P0515 Consistent comparison で挙げられている comparison category types での呼称を用いている. 参照: Consistent/three-way comparison↩︎

  4. タイムゾーンライブラリの型とその関係性を示したを, 作者のドキュメントページから見ることができる.↩︎

  5. List of tz database time zones↩︎

  6. サマータイムの開始と終了で, 存在しないローカル時間(nonexistent_local_time)と重複するローカル時間(ambiguous_local_time)という概念が生じる.↩︎

  7. サマータイムの開始と終了で, 存在しないローカル時間(nonexistent_local_time)と重複するローカル時間(ambiguous_local_time)という概念が生じる.↩︎

  8. 元の実装を見ると, 現在地のタイムゾーン取得においては, Linux および Mac では特定ファイル(/usr/share/zoneinfo, /usr/share/zoneinfo/uclibc) を読み込み, Windows ではレジストリ値を読み込んでいる. レジストリ値から取得されたネイティブな現在のタイムゾーン名が標準のものと一致する保証はなく, 特に Windows の場合, 得られる名前は必ず標準と異なるものであるため, 標準の名前と関連づけるマッピングが行われる. 元の実装では, Windows の場合ではタイムゾーンデータベースの取得の際に, xml ファイル取得している.↩︎

  9. 元の実装を見ると, OS のタイムゾーンデータベースを利用せず, リモート API があるときは OS 依存またはサードパーティ製のライブラリを利用してデータを取得し, そうでないとき OS のタイムゾーンデータベースを利用する. 元の実装で OS のタイムゾーンデータベースを試用する際には, DUSE_OS_TZDB=1をセットしてビルドする. Windows 環境がないので筆者にはわからないが, Windows では OS のタイムゾーンデータベースを利用できないようだ.↩︎

  10. この一つ前の ISO C++ 委員会による国際会議で C++20 に追加された P0515 Consistent comparison で挙げられている comparison category types での呼称を用いている. 参照: Consistent/three-way comparison↩︎

  11. List of tz database time zones↩︎

  12. サマータイムの開始と終了で, 存在しないローカル時間(nonexistent_local_time)と重複するローカル時間(ambiguous_local_time)という概念が生じる.↩︎

  13. 2016-11-06 01:30:00 EDT は, 2016-11-06 05:30:00 UTC と 2016-11-06 06:30:00 UTC になりうる. try { auto zt = zoned_time{"America/New_York", local_days{Sunday[1]/November/2016} + 1h + 30min}; } catch (const ambiguous_local_time&) { }↩︎

  14. サマータイムの開始と終了で, 存在しないローカル時間(nonexistent_local_time)と重複するローカル時間(ambiguous_local_time)という概念が生じる.↩︎

  15. 2016-03-13 02:30:00 EDT は, 2016-03-13 02:00:00 EST と 2016-03-13 03:00:00 EDT の間にあるため存在しない. どちらも, 2016-03-13 07:00:00 UTC と等価である. try { auto zt = zoned_time{"America/New_York", local_days{Sunday[2]/March/2016} + 2h + 30min}; } catch (const noexistent_local_time&) {}↩︎

  16. サマータイムの開始と終了で, 存在しないローカル時間(nonexistent_local_time)と重複するローカル時間(ambiguous_local_time)という概念が生じる.↩︎

  17. この一つ前の ISO C++ 委員会による国際会議で C++20 に追加された P0515 Consistent comparison で挙げられている comparison category types での呼称を用いている. 参照: Consistent/three-way comparison↩︎

  18. 元の実装を見ると, OS のタイムゾーンデータベースを利用せず, リモート API があるときは OS 依存またはサードパーティ製のライブラリを利用してデータを取得し, そうでないとき OS のタイムゾーンデータベースを利用する. 元の実装で OS のタイムゾーンデータベースを試用する際には, DUSE_OS_TZDB=1をセットしてビルドする. Windows 環境がないので筆者にはわからないが, Windows では OS のタイムゾーンデータベースを利用できないようだ.↩︎

  19. この一つ前の ISO C++ 委員会による国際会議で C++20 に追加された P0515 Consistent comparison で挙げられている comparison category types での呼称を用いている. 参照: Consistent/three-way comparison↩︎

  20. 閏秒が UTC に挿入される度に, UTC は TAI の 1 秒遅れとなる. 1961 年発祥の旧 UTC, 1972 年の特別調整, 1972 年から 2017 年 1 月まで行われた27 回の閏秒調整の過程を踏み, UTC は現在 TAI に対して 37 秒遅れている. 参考: http://jjy.nict.go.jp/mission/page1.html↩︎

  21. tai_clock同様, UTC に閏秒が挿入されるたびに UTC の 1 秒後を表現することとなる. 2017 年時点で, UTC は GPS の 18 秒前 にある. 余談: GPS が UTC との差分を計算する方法に関して.↩︎

  22. file_time_typeは C++17 時点で既にエイリアスとして追加されているが, using file_time_type = std::chrono::time_point</*trivial-clock*/>;と記されており, 具体的なクロック型は明記されていなかった.↩︎