🕒時刻計算と📅カレンダー計算は違うよ。全然違うよ。

# 時刻計算とカレンダー計算
C++言語はC++11[`<chrono>`ライブラリ](https://cpprefjp.github.io/reference/chrono.html)導入によって時刻計算（例：6時間後の時刻は？）をサポートしており、最新のC++20ではカレンダー計算（例：7日後の日付は？）もサポートするよう機能拡張されました。[^chrono-io]

[^chrono-io]: C++20`<chrono>`ライブラリの拡張により、日付や時刻といった値も`std::cout`へストリーム出力可能となります。...というのが建前で、2021年12月現在はGCC／Clangともに本機能をまだ提供しません。MSVCはサポートしているかも［要出典］。本記事コードの動作確認には https://gist.github.com/yohhoy/4f4c4262ef05fff084538672885ff359 を利用しました。

```cpp
#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年後の日付も求めてみましょう。

```cpp
  // 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`)**](https://cpprefjp.github.io/reference/chrono/time_point.html)」から指定の「[期間(`duration`)](https://cpprefjp.github.io/reference/chrono/duration.html)」だけ時計の針を進める／戻す操作です。
* カレンダー計算は、特定の「[**年月日(`year_month_day`)**](https://cpprefjp.github.io/reference/chrono/year_month_day.html)」から指定の年(`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`からその期間分だけ（日単位で）時計の針を進めています。[^duration-rep]

[^duration-rep]: [クラステンプレート`duration<Rep, Period>`](https://cpprefjp.github.io/reference/chrono/duration.html)の第1テンプレート引数`Rep`には、[十分なビット幅を持つ符号付き整数型](https://cpprefjp.github.io/reference/chrono/duration_aliases.html)が用いられます。具体的な型は処理系定義とされるため、本記事中では`Int`で略記します。

```cpp
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 に対する加算となります。[^duration-precision]

[^duration-precision]: 厳密には`duration::period == ratio<N,D>`部分は時刻計算時に両オペランド間で通分されるため、`duration<Int, ratio<54,1>>`や`duration<Int, ratio<216,1>>`という型が導出されます。`gcd(86400, 2629746)==54`, `gcd(86400, 31556952)==216`

```cpp
// 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`へキャストを忘れないようにご注意ください。

```cpp
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`)のみを利用します。[^months-years]

[^months-years]: `months`や`years`は`duration`の別名(alias)として定義されます。つまり1ヶ月後や1年後の日付はそれぞれ`ymd + duration<Int, ratio<2629746,1>>{1}`、`ymd + duration<Int, ratio<31556952,1>>{1}`とも記述可能です。...こんなコード書く人は居ませんよね。


## 参考
本記事で紹介した以外のカレンダー計算例として、記事「[C++20標準ライブラリ <chrono>ヘッダ Tips](https://qiita.com/yohhoy/items/8e751b019b369693f0f3)」も参考にください。