# Temporal におけるカレンダーシステムのサポート

[ECMAScript Temporal](https://github.com/tc39/proposal-temporal/)は、ECMAScript における新しい日時 API です。ECMAScript は世界中のユーザーに向けて設計されているため、高品質な国際化サポートを設計目標としています。

このドキュメントでは、Temporal がなぜ / どのようにしてカレンダーシステムをサポートするのか、そして開発者が遭遇するかもしれないカレンダーに関する問題と、それを回避する方法について説明します。前半の節では、Temporal でカレンダーシステムをサポートするに至った経緯を説明します。後半の節では、過去に検討されたカレンダーシステムの設計について説明し、なぜそれらではなく現在のアプローチが選択されたのかを説明します。

## 背景とモチベーション

カレンダーシステムは、「特定の時刻」と「年、月、日といった人間が読みやすい数値」を対応付ける方法を提供します。

世界中で多く使用されているのはグレゴリオ暦です。ここでは太陽暦、月、日が使われ、これらの値は季節に同期します。しかし世界中には、その他のカレンダーシステムを存在しています。有名なものでは、ユダヤ暦、ヒジュラ暦、仏滅紀元、ヒンドゥー暦、中国暦、和暦、そしてエチオピア暦などです。

今日はグレゴリオ暦で"March 4, 2021"ですが、その他のカレンダーでは次のように表されます。

<!-- prettier-ignore-start -->

- 20 Adar 5781（ユダヤ暦）
- Rajab 20, 1442 AH（ウンム・アル＝クラーのヒジュラ暦）
- March 4, 2564 BE（タイの仏滅紀元）
- 令和3年3月4日 == March 4, 3 Reiwa（和暦）

<!-- prettier-ignore-end -->

グレゴリオ暦以外のカレンダーシステムは、いくつかの国、特にグレゴリオ暦が支配的な西洋諸国との文化的、経済的関係が弱い国において、日常生活ならびに宗教的、文化的、学術的用途で使用されています。例えば、日本、中国、イラン、アフガニスタン、サウジアラビア、台湾を含むいくつかの国では、グレゴリオ暦以外のカレンダーが、少なくとも一部の政府公式の手続きで使用されています。これらの公式な用途以外でも、世界の人口の大多数は、宗教的または文化的な休日の日付を決定するためにグレゴリオ暦以外のカレンダーシステムを使用する国に住んでいます。

### カレンダーのビジネスロジック（単なる文字列の地域化ではない）

カレンダーシステムは地域化（l10n：localization）では重要な役割を果たします。これらは、タイムスタンプや日時を人間にわかりやすい文字列へ変換することを可能にします。

しかし、カレンダーシステムはアプリケーションのビジネスロジックについても重要な役割を果たします。人間が関わるアプリケーションでは、日時の演算や操作を必要とする場合が多く、そのような処理にはカレンダーシステムが必要です。カレンダーに依存する処理の例は次のとおりです：

- 今日から 1 ヶ月後の日付を求めたい
- ある月の最初の日付を求めたい
- 誕生日や記念日を扱いたい（訳注：「何月何日」という概念そのものがカレンダーに依存している）
- ラマダーン、ペサハ、イースター、春節のような、宗教的または文化的な祝日の日付を求めたい

言い換えると、1 日（太陽日）よりも大きな単位をどのように決めるかは、その地域の文化によります。日付計算を最も普遍的に解決する方法は、月と年の概念を排除することですが、そのような日付 API は多くの場合不十分です。

### 他のプラットフォームに比べて、ECMAScript におけるカレンダーサポートへの優先度が高いのはなぜか

カレンダーシステムを Temporal へ統合しようとすると新たな問題へ対処しなければならなくなるのは明らかです。例えば"Unexpected Calendar Problem"（[以下を参照](#the-unexpected-calendar-problem)）が考えられます。つまり、あるカレンダーを想定して書かれたコードで、別のカレンダーの日時を処理しようとすると様々な問題を引き起こす可能性があります。

しかし、Temporal にカレンダーシステムを統合することによる利点は、これらの問題を上回る可能性があり、これはそれほど自明では無いかもしれません。これらの利点を理解する 1 つの方法は、カレンダーシステムに対して異なるアプローチを行っている Java や.NET と、ECMAScript エコシステムのニーズの違いを明確にすることです。いくつかの違いを以下に示します：

- [Java の日付/時刻 API の設計に関する注意事項](https://github.com/ThreeTen/threeten/wiki/Multi-calendar-system)で説明されているとおり、ほとんどのエンタープライズアプリケーションでは ISO カレンダーが使われています。しかし Java や.NET に比べて、ECMAScript には一般のコンシューマーやブラウザ向けのユースケースが多くあります。このようなケースでは、ISO 以外のカレンダーがより必要になると考えられます。
- ISO 以外のカレンダーへの需要が、新興国市場で高まっています。これらの市場でスマートフォンユーザーがさらに 10 億人増加するという予想を考えると、地域化に関する技術への関心が更に高まり、ISO 以外のカレンダーが必要になると考えられます。
- ECMAScript の標準ライブラリは Java や.NET のものよりも 1〜2 桁少なく、そのため ECMAScript アプリケーションは 100 以上の OSS ライブラリに依存することも珍しくありません。したがって現状の ECMAScript で国際化（i18n：internationalization）のサポートを実現するためには、プラットフォームと OSS ライブラリの両方の協力が必要であり、ライブラリ開発者に国際化に対応するようコードの修正を依頼することがよくあります。

現在の設計を選択することことは容易ではありませんでした。カレンダーシステムを Temporal に統合することは、ISO カレンダーのみを意図してコードを作成するよりも多くのバグを引き起こします。しかし、経済成長や人口増加、テクノロジーの普及に関する世界的な傾向を考慮すれば、現在の設計を採用することは、ECMAScript コミュニティの長期的な成功に繋がる合理的なトレードオフであると私達は考えています。

### "Intl ファースト"な設計

ECMAScript には国際化（i18n）をサポートする主要な機能があります。Intl ファーストな設計の 2 つの原則は、「1.言語または地域に固有の操作はデータ駆動であること」、「2.ユーザーの環境が、API へ明示的に入力されること」です。これらの原則は、世界中の幅広いエンドユーザーを対象にしたアプリケーションの開発者が、API を適切に使用するために役立ちます。

Temporal では、これらの原則を「カレンダー固有のロジックを抽象化すること」と「どのカレンダーを使用するかをオブジェクトコンストラクタで選択できるようにすること」によって守っています。その結果、グレゴリオ暦のみを使用する地域と、他のカレンダーを使用する地域のどちらでも利用可能な API を作成できます。

### Temporal のコード内でのカレンダーの使用

以下の節ではカレンダーシステムが Temporal のコード内にどのように現れるかを示します。より詳しい情報は[ドキュメント](../calendar.html)を参照してください。

Temporal では、日付のフィールドを持つすべてのオブジェクトは、カレンダーのフィールドも持ちます。例えば、以下の 2 つの実行結果は、どちらもユダヤ暦の「23 Adar I 5779」を表す`Temporal.PlainDate`インスタンスとなります。

```javascript
// bag of fieldsでユダヤ暦の日付オブジェクトを作成する
Temporal.PlainDate.from({
  year: 5779,
  monthCode: 'M05L',
  day: 23,
  calendar: 'hebrew'
});

// ISOカレンダーの日付オブジェクトを作成し、ユダヤ暦に変換する
Temporal.PlainDate.from('2019-02-28').withCalendar('hebrew');
```

内部的には、日付は常に ISO カレンダーによって表されています。つまり、もしインスタンスからカレンダーの情報が失われても、それが示す日付は変化しません。これは、Temporal の内部スロットと文字列表現のどちらにも言えます。したがって、以下のどちらの結果も同じく「23 Adar I 5779」のインスタンスとなります：

```javascript
// ISO文字列をパースする（IETF RFCはまだ提案中）
Temporal.PlainDate.from('2019-02-28[u-ca=hebrew]');

// コンストラクタによって内部データモデルの値を指定する
new Temporal.PlainDate(2019, 2, 28, 'hebrew');
```

Temporal インスタンスに対する操作は、すべてカレンダーシステムのコンテキストで行われます。例：

```javascript
date = Temporal.PlainDate.from('2019-02-28[u-ca=hebrew]');
date.with({ day: 1 }); // => 2019-02-06[u-ca=hebrew]
date.with({ day: 1 }).toLocaleString('en-US', { calendar: 'hebrew' }); // => '1 Adar I 5779'
date.year; // => 5779
date.monthCode; // => 'M05L'
date.month; // => 6
date.day; // => 23
date.inLeapYear; // => true
date.calendar.id; // => 'hebrew'
inFourMonths = date.add({ months: 4 });
inFourMonths.toLocaleString('en-US', { calendar: 'hebrew' }); // => '23 Sivan 5779'
inFourMonths.withCalendar('iso8601'); // => 2019-06-26
date.until(inFourMonths, { largestUnit: 'month' }); // => P4M
```

定義より、年、月、日から bag of fields（訳注：コンストラクタに与える日付の情報を含んだオブジェクトのこと）を生成するにはカレンダーシステムを指定する必要があります。

そのような際にカレンダーシステムが指定されていない場合、Temporal は ISO カレンダーが省略されているものとみなします。これは、Temporal がカレンダーの省略を想定する唯一の場所です。

```javascript
Temporal.PlainDate.from({ year: 2019, month: 2, day: 28 });
```

次の Temporal タイプはすべて内部にカレンダーを持っています：`Temporal.ZonedDateTime`、`Temporal.PlainDateTime`、`Temporal.PlainDate`、`Temporal.PlainYearMonth`、`Temporal.PlainMonthDay`

## 予期しないカレンダーの問題

Temporal の多くの日付操作においてカレンダーを意識することは、世界中の多くのアプリケーション利用者にとって重要です。しかし、これは新しい問題を引き起こします：もし、コードが特定のカレンダーを想定して作られている場合、予期していないカレンダーの日付を扱おうとするとバグが発生します。このバグは、悪意を持って利用されてしまうかもしれません。

この節では、私達が「予期しないカレンダーの問題（Unexpected Calendar Problem）」と呼んでいるものについて説明し、それをユーザーランドのコードで軽減する方法も説明します。

### 問題の定義

以下はこの問題の典型的な例です：一年の最後の日かどうかを判定する関数があります。これは、1 年が 12 ヶ月で、1 年の最後の月の長さが 31 日であることを想定しています。

```javascript
function isLastDayOfYear(date) {
  // Note: これは"良くない"例です！
  return date.month === 12 && date.day === 31;
}

// ISOカレンダーでは正常に動作します
isLastDayOfYear(Temporal.PlainDate.from('2019-12-31')); // => true
isLastDayOfYear(Temporal.PlainDate.from('2020-01-01')); // => false

// ISO以外のカレンダーでは動作しません
hebrewNewYearsEve = Temporal.PlainDate.from({
  year: 5780,
  monthCode: 'M12',
  day: 29,
  calendar: 'hebrew'
});
isLastDayOfYear(hebrewNewYearsEve); // => false
// （trueとなるべき）
```

### 予期しないカレンダーの問題を緩和する方法

この問題を回避する方法は 2 つあります。ベストプラクティスは、どのようなカレンダーにも対応できる、カレンダーセーフなコードを書くことです。もう 1 つの方法は、カレンダーのバリデーションや変換処理を実装することです。これにより、対応しているカレンダーの日付のみを処理できるようになります。

#### カレンダーセーフなコードを書く

予期しないカレンダーの問題を回避する最も良い方法は、ビルトインのすべてのカレンダーに対応する、カレンダーセーフなコードを書くことです。例えば上記のコードに関しては、ISO カレンダーの定数をハードコーディングする代わりに`monthsInYear`や`daysInMonth`を用いることで、簡単にカレンダーセーフなコードに書き換えられます。開発者は、[カレンダーセーフな Temporal コードを書くためのベストプラクティス](https://tc39.es/proposal-temporal/docs/calendar.html#writing-cross-calendar-code)にしたがうことで、すべてのビルトインカレンダーで動作するコードを書くことができます。

ドキュメントに記載がある以外にも、Temporal API そのものが、カレンダーセーフなコードを書きやすいように既に設計されています。例えば：

- ISO を含むすべてのビルトインカレンダーは、`year`、`month`、`day`、`monthCode`といった、日付に関する共通したフィールドを持っています。これらのフィールドに関する操作は、すべてのビルトインカレンダーで共通です。
- `month`プロパティは、一年を通して連続する（ギャップのない）月のインデックスを表し、これは ISO カレンダーの月の挙動と一致します。太陰太陽暦では、年ごとに月の構造が異なりますが、`month`プロパティは常に 1 年を通してのインデックスです。年に依存しない月の種類を表す情報は、`monthCode`文字列フィールドに分離されています。
- `year`プロパティは各カレンダーにおける「デフォルトの時代」からの経過を示す符号付きの値です。これは ISO カレンダーの挙動と一致し、例えば「紀元前では年を逆方向に数える」というようなバグを防ぎます。各カレンダーにおけるデフォルトでない時代（訳注：和暦では平成、令和など）に対する年数などの情報は`eraYear`と`era`フィールドに分離されています。
- カレンダー間の違いは、すべてのカレンダーで利用可能なプロパティによって公開されます。例：`monthsInYear`、`daysInMonth`、`inLeapYear`。これらを用いることで、カレンダー固有の定数（例：1 年間は"12"ヶ月）の使用を回避できます。
- 暗黙的なデフォルトのカレンダーシステムはありません。開発者はどのカレンダーシステムを用いるのかを明示的に指定しなければならず、ISO に関するショートカットのためのメソッド名には、わかりやすいように接尾辞がついています（例：`Temporal.now.zonedDateTimeISO()`。
- Temporal はユーザーの環境からカレンダーを推測しません。どのカレンダーシステムを使用するかは、アプリケーションやライブラリの開発者、または日付を入力するユーザーによって明示的に決定されます。

#### カレンダーのバリデーションと変更

Temporal を用いることでカレンダーセーフなコードを簡単に記述できますが、それでも開発者による作業が必要です。一部の開発者はこの作業を行いません。これは、ISO 以外のカレンダーの存在を知らない、または理解していないことが原因である場合があります。または、ISO カレンダーのみを使用するアプリケーションを書いている場合、わざわざカレンダーセーフのベストプラクティスに従わないかもしれません。あるいは、自身が書くコードではベストプラクティスが守られているが、それが依存するコードではそうでない場合があります。通常、ECMAScript アプリケーションにはたくさんの依存関係があり、時間と労力の関係上、それらと日時データを相互運用する必要があるかもしれません。

このような場合、開発者は入力される日時データについて、「意図したカレンダーシステムであることをバリデートする」か、「カレンダーシステムを意図したものに変更する」必要があります。バリデーションと変更のどちらが望ましいかは、ユースケースによります。

カレンダーシステムを変更する場合:

```javascript
date = date.withCalendar('iso8601');
```

カレンダーシステムをバリデートする場合:

```javascript
if (date.calendar.id !== 'iso8601') throw new Error('invalid calendar');
```

ここで言う「入力」とは外部データに限らないことに注意してください。例えば、依存している日付ライブラリから返されるデータなども「入力」です。ライブラリや入力ソースが特定のカレンダーシステムをサポートしていると明示しない限り、それらからのデータ（日時オブジェクトやそれに変換される文字列）はすべて「外部」であり、カレンダーシステムのバリデーションや変更のための処理が必要です。それらのデータをそのまま使用するのはカレンダーセーフではありません。

## 検討されていた代替案

カレンダーシステムを Temporal に統合することは、API 設計におけるもっとも挑戦的な部分でした。以下では、私達が現在の設計の前に検討していたカレンダー API の代替案を簡単に説明します。（現在の設計も含めて）検討された設計はどれも理想的なものではないことに注意してください。これらの設計は、場合によっては回避策が必要となる既知の欠陥を持っています。以下の選択肢と比較して、現状の設計が全体を通して最善の選択であると私達は信じています。

### 代替案 1: データモデルにカレンダーを含めない

現状の Temporal の設計に対して、カレンダーの情報を Temporal のインスタンスから分離して管理するという代替案があります。これにより、「予期しないカレンダーの問題」を回避できるという利点がありますが、API の利用者への負担が大きいという問題があります：

- カレンダーに固有のフィールド（`month`、`day`、`year`など）やプロパティ（`daysInMonth`、`inLeapYear`）を利用する際に、その都度パラメタが必要になります。つまり、これらの値を参照するのではなく、`date.calendarMonth('chinese')`のように Getter を介して値を取得することになります。
- 多くのメソッド（`add`、`subtract`、`round`、`until`、`since`、`with`、そしておそらく`toPlainMonthDay`、`compare`、`equals`、またその他いくつかのメソッド）を呼び出すたびにカレンダーを明示しなければなりません。これは、年、月、日などのカレンダーに固有なフィールドを処理するために必要です。メソッドの呼び出しは、たとえば次のようになります：`date.add({months: 2}, {calendar: 'chinese'})`
- 複数の操作をチェインする（例：`.add({months: 2}).with({day: 1})`）際にもメソッドごとにカレンダーを指定する必要があります。通常、連鎖するすべての操作では同じカレンダーが使用されます。それをメソッドの呼び出しごとに指定するのは開発者の負担になり、バグの原因にもなります。
- `Temporal.PlainMonthDay`を扱うのが難しくなります。なぜなら、誕生日や祝日のような月/日の値は本質的にカレンダー固有のものであり、対応する ISO の記法が無いため推論することも難しくなります。特に、ユダヤ暦や中国歴のような太陰太陽暦では年ごとに月の構成が変わります。（現状の Temporal では[`monthCode`](../plaindate.md#monthCode)という文字列によって年に固有の月を表せます。）例えば、「ユダヤ暦の 12 Adar I」という誕生日は、現状では次のようにモデル化できます。

  ```javascript
  Temporal.PlainMonthDay.from({ monthCode: 'M05L', day: 12, calendar: 'hebrew' });
  ```

  インスタンスがカレンダーを持たない場合、この誕生日をどのようにモデル化すればいいのかわかりません。

- 日付を返すコードは、単一の Temporal インスタンスを返せません。代わりに、複合オブジェクトか、シリアライズされた文字列を返す必要があります。例えば、ユーザーの操作の結果日付データを返す DatePicker は、`Temporal.PlainDate`を返すだけでは不十分であり、代わりにタプルや`{date, calendar}`といった複合オブジェクトを返す必要があります。このような複合オブジェクトは、カレンダーが必要なすべての Temporal のコードを複雑にしてしまいます。

私達は、この代替案によって生じる負担が、予期しないカレンダーの問題を回避するためのものに比べてとても大きいと考えています。

### 代替案 2: ISO プロパティとカレンダープロパティを分離する

予期しないカレンダーの問題に対して脆弱になることなく、カレンダーを Temporal のデータモデルに統合することが可能です。しかし、これによって Temporal API の数が増加してしまいます。この案を適用した場合の一例を以下に示します。

- `.isoMonth`と`.calendarMonth`の 2 つのフィールドを提供します（または、カレンダーの概念に慣れていない開発者のために`.isoMonth`と`.calendarMonth`を提供する？）
  - `calendar*`のメソッドのうち、どれをサポートしてどのような型を返すかは、各カレンダーに完全に依存します。すべてのカレンダーは`calendarDay`、`calendarMonth`、`calendarMonthCode`、`calendarYear`を提供しますが、`calendarEra`や`calendarEraYear`を提供するかはカレンダーしだいです。
  - property bag 形式について、ISO フィールドとカレンダーフィールドのいずれかのみが使われている必要があります。両方が"混ぜられている"場合はエラーとなります。例えば、`date.with({calendarYear: 5780, isoMonth: 12})`は例外を投げる必要があります。しかも、 DX の観点から、これは ESLint や TS で強制されるべきです。
  - カレンダーが明示されていない場合、`calendar*`へのアクセスはエラーとなります。
  - `dayOfYear`や`inLeapYear`といった、フィールドではないプロパティも複製されます。ここで`calendar`接頭辞が付かないプロパティは、単に ISO カレンダーでの結果を返すものになります。
- `Temporal.Duration`に`calendar`フィールドを追加します。
  - ISO カレンダーを使用するインスタンスは次のフィールドのみをサポートします：`days`、`weeks`、`months`、`years`
  - ISO 以外のカレンダーを使用するインスタンスは次のフィールドのみをサポートします：`calendarDays`, `calendarWeeks`, `calendarMonths`, and `calendarYears`.
  - 文字列のパースとシリアル化は、他の Temporal タイプと同様に、カレンダーの接尾辞を使って次のように表します：`'P2D[u-ca=chinese]'`。なお、接尾辞がついていないものは ISO カレンダーを用いているとみなします。
  - `until` と`since`が曖昧なため、出力について ISO カレンダーの Duration なのか、ISO 以外のカレンダーの Duration なのかを指定できるようにする必要があります。
    これには次の方法が考えられます：
    - メソッドを"iso"と"calender"という接頭辞によって区別します。例：`isoUntil`と`calendarUntil`、`untilISO`と`untilCalendar`
    - 常に両方の結果を返します。例：`foo.until(bar).isoDuration`または`foo.until(bar).calendarDuration`
    - オプションを用います。例：`{ resultFields: 'iso' | 'calendar' }`。ただし、このオプションがリテラルとして指定されていない場合に（訳注：例えばこのオプションの値が外部から入力されるものである場合に）、TS がメソッドの実行結果の型をうまく処理できるかは疑問です。
  - `withCalendar`メソッドはありません。代わりに ISO フィールドやカレンダーフィールドがありますが、どちらか一方しかありません。

私達はこれらの代替案を検討した結果、この案によってもたらされる複雑さにはあまり価値がないと判断しました。特に、フィールドやプロパティを 2 倍に増やすというアプローチには、余計なバグが増えたり、入力のバリデーションなどが煩雑になるという懸念があります。

### 代替案 3: サブクラス

カレンダーシステムを PlainDate の内部スロットに格納する代わりに、PlainDate を ISO カレンダーのみをサポートするように保ち、PlainHebrewDate や PlainChineseDate といったカレンダー固有のサブクラスを作成する案が検討されました。より詳しくは[サブクラスに関するディスカッション](https://github.com/tc39/proposal-temporal/blob/main/docs/calendar-subclass.md)を参照してください。

私達は、サブクラスのアプローチを行わないことにしました。なぜなら：

- もしサブクラスにカレンダー固有のセットメソッドがある場合、カレンダーをまたいだコードが書きづらくなり、ポリモーフィズムの利点が失われてしまいます。一方、各サブクラスが PlainDate と同じメソッドを持つ場合、それは PlainDate にカレンダーを格納するためのプラガブルなスロットがあるのと同じです。
- 考えられるカレンダーシステムの数は膨大（かつ、理論的には無制限）であり、たくさんのサブクラスを作らなくてはならなくなります
- 各サブクラスは独立したデータモデルを定義しなければならず、仕様が膨大になってしまいます

私達は最終的に、このアプローチが他のアプローチに比べて高コストであり、かつ明確な利点がないと判断しました。また、カレンダーに固有なすべての操作を Temporal.Calendar に整理することで、より見通しの良いモデルが実現することもわかりました。

### 代替案 4: カレンダーを持たない日付タイプを新たに作成する

このアプローチでは、カレンダーシステムを持たずに日時を表す新しいタイプを作成します。そして、カレンダーの情報が必要ない処理においては、カレンダーを持たないクラスをできるだけ使用するようにします。このアプローチについては、[Option 5 in calendar-draft.md](https://github.com/tc39/proposal-temporal/blob/main/docs/calendar-draft.md#new-non-calendar-types-option-5)で詳しく説明しています。

私達は、次の理由によってこのアプローチに反対しました：

1. 作成するタイプにカレンダーの情報が無いとすると、年や月に関する処理をサポートできませんでした。
2. 作成するタイプが ISO カレンダーのように振る舞うとすると、それは ISO カレンダーを使用した PlainDate と同じことになります。

特に(2)については、動的型付け言語である ECMAScript にとってよく当てはまることに注意してください。ただし、静的型付け言語では、ECMAScript では不可能なコンパイル時の型チェックを行えるかもしれません。特に TypeScript のようなクライアントは、カレンダーシステムのコンパイル時の型チェックにおいて、カレンダーシステムに対応する型パラメタを用いることで PlainDate の型を効率的に生成できる可能性があることに注意してください。その場合、個別のデータ型は必要ありません。
