yohhoyの日記

技術的メモをしていきたい日記

C++標準ライブラリのカレンダー(Calendar)

C++2a(C++20)標準ライブラリ <chrono> ヘッダに追加される カレンダー(Calendar) サポートについてざっくりメモ。

本記事では簡単のため名前空間std::chronoを省略する。またタイムゾーン(Time Zone)サポートには言及しない。

まとめ:

  • 型安全(Type safety): 年(year), 月(month), 日(day), 年月日(year_month_day)を型システム上で表現する。数値からの暗黙変換は禁止。
    • 例: 2020年7月24日*1year_month_day{2020, 7, 24} ではなく year_month_day{year{2020}, month{7}, day{24}} のように記述する。
  • 既存chronoライブラリとの統合: 年月日(year_month_day) と システム時刻(system_clock::time_point) は相互変換可能。年月日はフィールド単純保持、システム時刻は実在するTime pointを表す。*2
    • 例1: auto tp = sys_days{2020y/July/24} + 9h + 30min + 15s; year_month_day ymd{floor(tp)};
    • 例2: 2018年3月32日は2018年4月1日 sys_days{2018y/3/32} == sys_days{2018y/4/1}
  • 日付リテラルの表記順は 年/月/日, 月/日/年[アメリカ式], 日/月/年[イギリス式] をサポート。*3
    • 例: 2020年7月24日は 2020y/July/24, 2020y/7/24, July/24/2020, 24d/July/2020, 24d/7/2020*4
  • 曜日(weekday), 最終日(last_spec)を用いて、“ある月の最終日”、“ある月の第N X曜日”、“ある月の最終X曜日”も表現可能。*5
    • 例: 2020年7月の最終日 2020y/7/last == 2020y/7/31、2020年7月の第1月曜日 2020y/7/Monday[1] == 2020y/7/6、2020年7月の最終月曜日 2020y/7/Monday[last] == 2020y/7/27
  • 日付計算サポート: N年(years), N月(months), N週(weeks), N日(days) 前/後を計算可能。
    • 注意: 一年years{1}は 146097/400 日(≒365.24日)、一ヶ月months{1}は 1/12 年(≒30.43日) のDurationと定義される。年月日(year_month_day)またはシステム時刻(Time point)のいずれの型で計算するかで結果が異なる。
    • 例: 3ヶ月前(2020-04-24) 2020y/7/24 - months{3}、16日後(2020-08-09) sys_days{2020y/7/24} + days{16} *6
  • IOストリーム出力(operator<<)、書式指定出力(to_stream)、書式指定入力(from_stream)を追加提供。
    • 例: std::cout << 2020y/7/24; は "2020-07-24" を出力。
  • グレゴリオ歴(Gregorian calendar)のみサポート。要件を満たせば独自の暦との相互運用可。
    • (当然ながら)国・地域・文化で異なる “祝日” のサポートはない。

おまけ:任意月のカレンダーを出力するサンプルプログラム。

// C++2a
#include <chrono>
#include <iostream>
#include <iomanip>

void print_calendar(std::ostream& os, const std::chrono::year_month& ym)
{
  using namespace std::chrono;

  unsigned weekday_offset{ weekday{sys_days{ym/1}}.c_encoding() };  // P1466R3
  unsigned lastday_in_month{ (ym/last).day() };

  os << "      " << ym << "\n"
     << "Su Mo Tu We Th Fr Sa\n";
  unsigned wd = 0;
  while (wd++ < weekday_offset)
    os << "   ";
  for (unsigned d = 1; d <= lastday_in_month; ++d, ++wd)
    os << std::setw(2) << d << (wd % 7 == 0 ? '\n' : ' ');
}

int main()
{
  using namespace std::chrono_literals;
  print_calendar(std::cout, 2018y/3);
}
/*
      2018/Mar
Su Mo Tu We Th Fr Sa
             1  2  3
 4  5  6  7  8  9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
*/

2020-10-23追記:おまけプログラムにP1466R3を反映。std::chrono::weekdayから整数値(unsigned)への変換では、日曜始まりの範囲[0, 6]を返すc_encodingまたは月曜始まりの範囲[1, 7]を返すiso_encodingのいずれかを明示する必要がある。

関連URL

*1:2020-07-24 は東京オリンピックの開会式予定日ですって。特例法で祝日にする(他祝日を変更)とかなんとか。

*2:本文中の floor<D> は名前空間 std::chrono で定義されるDuration変換関数(C++17で追加)。days は日単位を表すDuration型、sys_days はシステムクロックの日単位Time point型(いずれもC++2aで追加)。

*3:ユーザ定義リテラル名前空間 std::literals::chrono_literals 以下で定義される。本文中の例では年(year)を返す operator""y 、日(day)を返す operator""d 、7番目の月(month)を表す定数 July を利用している。年月日区切りは除算演算子 / オーバーロードにより実現される。

*4:月は2桁表記 2020y/07/24 できなくもないが、8月(08)と9月(09)がill-formedな8進数リテラルとなってしまう。おすすめできない。

*5:本文中の例では、月曜日を表す定数 Monday と、 last_spec 型のタグ定数 last を利用している。いずれも名前空間 std::chrono 以下で定義される。

*6:月単位の加減算をTime point型で計算 sys_days{2020y/7/24} - months{3} すると、3ヶ月前は 2020-04-23 16:32:42 となってしまう。一方で、日単位の加減算は日単位Time point型(sys_days)へ変換してから計算する必要がある。