TemporalでのPlainYearMonthやPlainMonthDateのフォーマット方法について

## はじめに

:::note warn
Temporalは現状 TC39 proposal stage 3 のものを使用しています
今後APIが変わる可能性もあるのでご注意ください
:::

Temporalのpolyfill[^polyfill]に実装されている`Intl`を拡張したモジュールを使って`PlainMonthDay`のフォーマットをしようとした時に、以下のようなエラーが出てハマった部分があったので説明します。

[^polyfill]: https://github.com/js-temporal/temporal-polyfill

```log
intl.js:400 Uncaught RangeError: cannot format PlainMonthDay with calendar iso8601 in locale with calendar Gregory
    at extractOverrides (intl.js:400:1)
    at DateTimeFormatImpl.format (intl.js:143:1)
    at App (App.tsx:76:1)
```

## Temporalとは

TC39 proposalのTemporalについてはこちらの記事で説明していますので、ご覧ください。

https://qiita.com/kyntk/items/6a5a30357c285dfd3387

## Intlについて

IntlはES2017[^ES2017]で導入された、国際API 名前空間オブジェクトで、Temporalのpolyfillで実装されているIntlは、これを拡張してTemporalの日時を扱えるようにしているものです。

[^ES2017]: https://402.ecma-international.org/8.0/#intl-object

ECMAScriptのIntlには、`Intl.ListFormat()`や`Intl.NumberFormat()`などがありますが、Temporalでは`Intl.DateTimeFormat()`が拡張されています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Intl

## `Intl.DateTimeFormat()`の基本的な使い方について

基本的な`Intl.DateTimeFormat()`の使い方はこちらをご確認ください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat

一部引用すると、以下のように使えます。

```js
const date = new Date(Date.UTC(2020, 11, 20, 3, 23, 16, 738));
// Results below assume UTC timezone - your results may vary

console.log(new Intl.DateTimeFormat('en-US').format(date));
// expected output: "12/20/2020"

console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'long' }).format(date));
// Expected output "Sunday, 20 December 2020 at 14:23:16 GMT+11"
```

## PlainYearMonthやPlainMonthDayのフォーマットについて

`Intl.DateTimeFormat().format()`でフォーマットできるオブジェクトは以下のように型定義されています。

```ts
type Formattable =
  | Date
  | Temporal.Instant
  | Temporal.ZonedDateTime
  | Temporal.PlainDate
  | Temporal.PlainTime
  | Temporal.PlainDateTime
  | Temporal.PlainYearMonth
  | Temporal.PlainMonthDay;
```

そこで、タイトルにあるように、`PlainMonthDay`をformatしてみると、以下のようなエラーが発生します。

```ts
new Intl.DateTimeFormat().format(
  Temporal.Now.plainDateISO().toPlainMonthDay()
)

/*
intl.js:400 Uncaught RangeError: cannot format PlainMonthDay with calendar iso8601 in locale with calendar Gregory
    at extractOverrides (intl.js:400:1)
    at DateTimeFormatImpl.format (intl.js:143:1)
    at App (App.tsx:76:1)
*/
```

これがエラーになる原因については、こちらで説明されていますが、これはTemporalの機能のCalendarに関係してきます。

https://github.com/tc39/proposal-temporal/issues/2075#issuecomment-1059492503

### `Temporal.Calendar`について

カレンダーという概念についてはこちらで説明されています。

https://tc39.es/proposal-temporal/docs/ja/calendar-review.html

日時を扱う際に、世界中で多く使われているグレゴリオ暦以外の暦を扱うことができ、例えばユダヤ暦、中国暦、また和暦もあります。
これらの暦の使い方として、ドキュメントにある例を引用すると「グレゴリオ暦の`March 4, 2021`をユダヤ暦では`20 Adar 5781`と表現する」などのような変換をするものがあります。

Temporalでは内部的にはISO8601カレンダーを使用していますが、表示の際に他のカレンダーを使用することができます。

### あらためて、PlainMonthDayのフォーマットについて

このカレンダーについての考え方を元に、[先程のissue](https://github.com/tc39/proposal-temporal/issues/2075#issuecomment-1059492503)で説明されている内容を確認してみます。

> Dates can be converted between calendars.

> The same is not true for Temporal.PlainMonthDay or Temporal.PlainYearMonth. The 5th month of the Chinese calendar year 2022 has no relationship to the 5th month of the Hebrew year 2022. Similarly, the 5th day or the 8th month of the Gregorian calendar is unrelated to the 5th day of the 8th month of the Chinese or Hebrew or Islamic calendar. There is no way to convert them.

このように、ある暦の2022年の5ヶ月目は別の暦の2022年の5ヶ月目と全く関係がないため、変換ができません。

> For this reason, localized formatting of Temporal.PlainMonthDay or Temporal.PlainYearMonth requires that the calendar of the Temporal.PlainMonthDay or Temporal.PlainYearMonth instance must exactly match the calendar of the locale. Because the default calendar is iso8601 (a calendar that no locale uses), the caller must opt into using a specific calendar option when formatting these temporal types.

そのため、`Temporal.PlainMonthDay`や`Temporal.PlainYearMonth`をローカライズフォーマットしたい時は、「インスタンスの暦」と「ロケールの暦」を一致させる必要があるようです。

```ts
// インスタンスの暦を和暦に設定
const yearMonth = Temporal.Now.plainDate('japanese').toPlainYearMonth()

// ロケールの暦を和暦に設定
console.log(yearMonth.toLocaleString('ja', { calendar: 'japanese' }))

// -> R4/03
```

また、`Intl.DateTimeFormat`を使った場合も同様です。

```ts
const yearMonth = Temporal.Now.plainDate('japanese').toPlainYearMonth()

console.log(
  new Intl.DateTimeFormat('ja', {
    calendar: 'japanese',
    year: 'numeric',
  }).format(yearMonth)
)
// 令和4年
```

## まとめ

`Temporal.PlainMonthDay`や`Temporal.PlainYearMonth`をローカライズフォーマットしたい時は、「インスタンスの暦」と「ロケールの暦」を一致させる必要がある

## 参考

- [ECMAScript® 2021 Internationalization API Specification](https://402.ecma-international.org/8.0/#intl-object)
- [Intl - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Intl)
- [`Intl.DateTimeFormat().format` can't format `Temporal.PlainMonthDay` · Issue #2075 · tc39/proposal-temporal](https://github.com/tc39/proposal-temporal/issues/2075#issuecomment-1059492503)
- [Principles to resolve issues with MonthDay and YearMonth? · Issue #874 · tc39/proposal-temporal](https://github.com/tc39/proposal-temporal/issues/874)
- [Temporal documentation](https://tc39.es/proposal-temporal/docs/ja/calendar-review.html)