Intl.RelativeTimeFormat()を使って日本語で相対時刻を取得する

JavaScriptの標準組み込みオブジェクトに`Intl`オブジェクトがあります。国際化APIの名前空間で、`locale`に依存した数値のフォーマットや分かち書きを行うプロパティを持ちます。

この記事ではそんなプロパティの1つである`RelativeTimeFormat`について解説します。

## RelativeTimeFormat
`RelativeTimeFormat`は相対時刻を`locale`に応じて扱うためのオブジェクトです。

```ts
const rtf = new Intl.RelativeTimeFormat('ja-JP');

// 2 日後
console.log(rtf.format(2, 'day'));

// 9 か月前
console.log(rtf.format(-9, 'month'));
```
`RelativeTimeFormat`を初期化するタイミングで扱いたい`locale`を指定します。
```ts
const rtf = new Intl.RelativeTimeFormat('ja-JP');
```
`RelativeTimeFormat`には第2引数でオプションをオブジェクトとして渡せます。
```ts
const rtf = new Intl.RelativeTimeFormat(
  'ja-JP',
  {
    localeMatcher: 'lookup',
    numeric: 'auto',
    style: 'short',
  },
);
```
オプションは`localeMatcher`と`numeric`と`style`の３つです。

`localeMathcer`は第一引数の`locale`が配列で複数渡ってきた時の優先順位の付け方を決めます。デフォルトでは`best fit`で他に`lookup`を指定できます。
`best fit`はリクエストやランタイムを用いて最適な`locale`を決定し、`lookup`を指定した場合は[`Lookup`](https://datatracker.ietf.org/doc/html/rfc4647#section-3.4)のアルゴリズムによって決定されます。

`numeric`はメッセージを表示する形式を指定できます。デフォルトは`always`でどのような数値であっても一貫した形式でフォーマットするのですが、`auto`にすると明日や昨日のように特定の数値を別の一般的な言い方に置き換えます。
```ts
const rtf1 = new Intl.RelativeTimeFormat('ja-JP');
const rtf2 = new Intl.RelativeTimeFormat(
  'ja-JP',
  {
    numeric: 'auto',
  },
);

// 2 日後
console.log(rtf1.format(2, 'day'));

// 明後日
console.log(rtf2.format(2, 'day'));
```

`style`は単位の長さを指定します。デフォルトは`long`で`short`と`narrow`が存在します。
`locale`が`ja-JP`の場合は`long`と`short`に違いがありません(例えば`locale`が`en`の場合は`long`では`month`、`short`では`mo`のように表示されます)。`narrow`は数値と単位の間の空白を埋めた形で出力します。

```ts
const rtf1 = new Intl.RelativeTimeFormat('ja-JP');
const rtf2 = new Intl.RelativeTimeFormat(
  'ja-JP',
  {
    style: 'short',
  },
);
const rtf3 = new Intl.RelativeTimeFormat(
  'ja-JP',
  {
    style: 'narrow',
  },
);

// 2 日後
console.log(rtf1.format(2, 'day'));

// 2 日後
console.log(rtf2.format(2, 'day'));

// 2日後
console.log(rtf3.format(2, 'day'));
```

このように作成された`RelativeTimeFormat`は`format`メソッドを用いて相対時刻を表示します。第1引数に相対日付の数値を、第2引数には単位を渡します。

第1引数の数値は正であれば未来との相対時刻を、負であれば過去との相対日付と判断します。

```ts
const rtf = new Intl.RelativeTimeFormat('ja-JP');

// 1 年後
console.log(rtf.format(1, 'year'));

// 1 年前
console.log(rtf.format(-1, 'year'));
```

第2引数は`year`、`quarter`、`month`、`week`、`day`、`hour`、`minute`、`second`とその複数形を渡します。それぞれその名の通りのフォーマットをしてくれます。

```ts
const rtf = new Intl.RelativeTimeFormat('ja-JP');

// 1 年後
console.log(rtf.format(1, 'year'));

// 1 四半期後
console.log(rtf.format(1, 'quarter'));

// 1 か月後
console.log(rtf.format(1, 'month'));

// 1 週間後
console.log(rtf.format(1, 'week'));

// 1 日後
console.log(rtf.format(1, 'day'));

// 1 時間後
console.log(rtf.format(1, 'hour'));

// 1 分後
console.log(rtf.format(1, 'minute'));

// 1 秒後
console.log(rtf.format(1, 'second'));
```

## 部品単位で相対時刻を取得する
`RelatveTimeFormat`オブジェクトの`formatToParts`メソッドを使えば数値と単位を配列の要素に分けて表示できます。

```ts
const rtf = new Intl.RelativeTimeFormat('ja-JP');

/**
 * [
 *   {
 *     type: 'integer',
 *     unit: 'day',
 *     value: 2,
 *   },
 *   {
 *     type: 'literal',
 *     value: ' 日後',
 *   },
 * ]
 */
console.log(rtf.formatToParts(2, 'day'));

/**
 * [
 *   {
 *     type: 'integer',
 *     unit: 'day',
 *     value: 9,
 *   },
 *   {
 *     type: 'literal',
 *     value: ' か月前',
 *   },
 * ]
 */
console.log(rtf.formatToParts(-9, 'month'));
```
引数は`format`メソッドと同じです。単位には空白が含まれていることに注意です。

## localeが対応していることを確認する
`RelativeTimeFormat`オブジェクトに渡す`locale`は静的メソッドである`supportedLocalesOf`を用いて確認できます。引数は`RelativeTimeFormat`オブジェクトのコンストラクタと同じです。

```ts
// ['ja', 'ja-JP']
console.log(
  Intl.RelativeTimeFormat.supportedLocalesOf(
    ['ja', 'JP', 'ja-JP']
  )
);
```

## 有効なオプションを確認する
`resolvedOptions`は`RelativeTimeFormat`オブジェクトを作成したときに有効となった設定を表示します。
```ts
// {locale: 'ja', style: 'long', numeric: 'always', numberingSystem: 'latn'}
console.log((new Intl.RelativeTimeFormat('ja')).resolvedOptions());

// {locale: 'ja-JP', style: 'long', numeric: 'always', numberingSystem: 'latn'}
console.log((new Intl.RelativeTimeFormat(['ja-JP', 'en'])).resolvedOptions());

const rtf = new Intl.RelativeTimeFormat(
  'ja-JP',
  {
    localeMatcher: 'lookup',
    numeric: 'auto',
    style: 'short',
  },
);
// {locale: 'ja-JP', style: 'short', numeric: 'auto', numberingSystem: 'latn'}
console.log(rtf.resolvedOptions());
```
`localeMatcher`はおそらく`locale`の決定後は使われないことから表示されません。
`numberingSystem`だけ見慣れませんが、これは`locale`に依存する[命数法](https://en.wikipedia.org/wiki/Numeral_system)を渡しています。

```ts
const locale = new Intl.Locale(
  'ja-JP',
  { numberingSystem: 'arab' }
);
// ['arab']
console.log(locale.getNumberingSystems());

// {locale: 'ja-JP-u-nu-arab', style: 'long', numeric: 'always', numberingSystem: 'arab'}
console.log((new Intl.RelativeTimeFormat(locale)).resolvedOptions())
```