時刻計算とカレンダー計算
C++言語はC++11<chrono>
ライブラリ導入によって時刻計算(例:6時間後の時刻は?)をサポートしており、最新のC++20ではカレンダー計算(例:7日後の日付は?)もサポートするよう機能拡張されました。1
#include <chrono>
#include <iostream>
using namespace std::chrono;
using namespace std::chrono_literals;
using std::cout;
int main()
{
// 基点時刻=2021-12-15 01:23:45
const auto tp = sys_days{2021y/12/15} + 1h + 23min + 45s;
// 6時間後の時刻は?
cout << tp << '\n'; // 2021-12-15 01:23:45
cout << (tp + hours{6}) << '\n'; // 2021-12-15 07:23:45
// 7日後の日付は?
const sys_days dp = floor<days>(tp); // 日(days)単位に切り捨て
cout << dp << '\n'; // 2021-12-15
cout << (dp + days{7}) << '\n'; // 2021-12-22
それぞれ正しく計算できていますね。この調子で1ヶ月後や1年後の日付も求めてみましょう。
// 1ヶ月後の日付は?
cout << (dp + months{1}) << '\n'; // 2022-01-14 10:29:06
// 1年後の日付は?
cout << (dp + years{1}) << '\n'; // 2022-12-15 05:49:12
}
...あれ、妙な値が出力されてしまいました。1ヶ月後は明らかに日付がズレていますし、1ヶ月後・1年後ともなぜか時分秒フィールドまで出力されています。
Q. もしかしてC++標準ライブラリのバグ?
A. いいえ、C++標準ライブラリ仕様通りです。
時刻計算 != カレンダー計算
C++標準ライブラリ<chrono>
では「時刻計算」と「カレンダー計算」が、型システム(type system)を利用して区別されます。
- 時刻計算は、特定の「時刻(
time_point
)」から指定の「期間(duration
)」だけ時計の針を進める/戻す操作です。 - カレンダー計算は、特定の「年月日(
year_month_day
)」から指定の年(years
)・月(months
)だけ日付を加算/減算する操作です。
時刻計算は時間という連続した一次元軸上での加減算のため、不定な計算結果が得られることはありません。
一方のカレンダー計算は文化的に定めた暦の上での加減算であり、結果として実在しない年月日が求まるケースがあります(例:2022-01-31の1カ月後は2022-02-31)。C++標準ライブラリのカレンダー計算では非実在日付の表現を許容しており、年月日(year_month_day
)から時刻(time_point
)へ型変換することで実在する年月日へのマッピングを行います(例:2022-02-31は実際には2022-03-03)。
時刻計算の説明
days{7}
はduration<Int, ratio<24*3600, 1>>{7}
つまり 7×(24×3600)/1 秒間という期間を表しており、日単位に丸めた基点時刻dp
からその期間分だけ(日単位で)時計の針を進めています。2
const auto tp = sys_days{2021y/12/15} + 1h + 23min + 45s;
const sys_days dp = floor<days>(tp); // 日(days)単位に切り捨て
cout << dp << '\n'; // 2021-12-15
cout << (dp + days{7}) << '\n'; // 2021-12-22
つづいてmonths{1}
やyears{1}
の定義を確認してみます。
-
month{1}
はduration<Int, ratio<146097*24*3600, 400*12>>{1}
、ratio
部は約分されてduration<Int, ratio<2629746, 1>>{1}
つまり 1×(2629746/1) 秒間を表します。 -
years{1}
はduration<Int, ratio<146097*24*3600, 400>>{1}
、ratio
部は約分されてduration<Int, ratio<31556952, 1>>{1}
つまり 1×(31556952/1) 秒間を表します。
日単位に丸めた基点時刻dp
に対して、秒精度をもつこれらの期間だけ時計の針を進めた結果が得られています。<chrono>
ライブラリの時刻計算では精度を上げるキャストは暗黙に行われるため、ここでは基点時刻 2021-12-15 00:00:00 に対する加算となります。3
// duration_cast<seconds>(months{1}).count() == 2629746
cout << (dp + months{1}) << '\n'; // 2022-01-14 10:29:06
// duration_cast<seconds>(years{1}).count() == 31556952
cout << (dp + years{1}) << '\n'; // 2022-12-15 05:49:12
(謎のマジックナンバー146097
や400
の根拠が気になる方は https://zenn.dev/yohhoy/scraps/24e4db4be3e1566f6197 を参照ください。ヒント:グレゴリオ暦の閏(うるう)年周期)
カレンダー計算の説明
カレンダー計算では年月日year_month_day
に対する年years
または月months
の加減算のみがサポートされます。仮想的な日付から実在する日付にマッピングするため、日単位のシステム時刻sys_days
へキャストを忘れないようにご注意ください。
const auto tp = sys_days{2021y/12/15} + 1h + 23min + 45s;
const year_month_day ymd = floor<days>(tp);
cout << sys_days{ymd} << '\n'; // 2021-12-15
cout << sys_days{ymd + months{1}} << '\n'; // 2022-01-15
cout << sys_days{ymd + years{1}} << '\n'; // 2022-12-15
時刻計算ではmonths
やyears
の型情報にエンコードされた期間長さ(period
)も考慮されていましたが、カレンダー計算ではオブジェクトが保持する値(count
)のみを利用します。4
参考
本記事で紹介した以外のカレンダー計算例として、記事「C++20標準ライブラリ ヘッダ Tips」も参考にください。
-
C++20
<chrono>
ライブラリの拡張により、日付や時刻といった値もstd::cout
へストリーム出力可能となります。...というのが建前で、2021年12月現在はGCC/Clangともに本機能をまだ提供しません。MSVCはサポートしているかも[要出典]。本記事コードの動作確認には https://gist.github.com/yohhoy/4f4c4262ef05fff084538672885ff359 を利用しました。 ↩ -
クラステンプレート
duration<Rep, Period>
の第1テンプレート引数Rep
には、十分なビット幅を持つ符号付き整数型が用いられます。具体的な型は処理系定義とされるため、本記事中ではInt
で略記します。 ↩ -
厳密には
duration::period == ratio<N,D>
部分は時刻計算時に両オペランド間で通分されるため、duration<Int, ratio<54,1>>
やduration<Int, ratio<216,1>>
という型が導出されます。gcd(86400, 2629746)==54
,gcd(86400, 31556952)==216
↩ -
months
やyears
はduration
の別名(alias)として定義されます。つまり1ヶ月後や1年後の日付はそれぞれymd + duration<Int, ratio<2629746,1>>{1}
、ymd + duration<Int, ratio<31556952,1>>{1}
とも記述可能です。...こんなコード書く人は居ませんよね。 ↩