Temporal入門その3 ～ Temporalオブジェクトと文字列の変換（フォーマット等）

# Temporalとは

Temporalとは、JavaScriptに導入が予定されている(2023年時点)、Date型に代わる新しい日付処理APIです。
https://tc39.es/proposal-temporal/docs/index.html

https://tc39.es/proposal-temporal/docs/index.html

現在TC39のStage 3で、仕様策定に関する議論が行われています。 https://github.com/tc39/proposal-temporal

https://github.com/tc39/proposal-temporal

従来のDate型の問題点を解消しており、タイムゾーンやサマータイムに関する処理も行うことができるようになっています。

この記事では、[前回の記事](https://qiita.com/access3151fq/items/c73563351788b63e0782)と[前々回の記事](https://qiita.com/access3151fq/items/05dba729b35bc2d8b3d0)に引き続き、日付と時刻の処理を試していきます。

https://qiita.com/access3151fq/items/05dba729b35bc2d8b3d0

https://qiita.com/access3151fq/items/c73563351788b63e0782

この記事では、

- 文字列からTemporalオブジェクトへの変換
- Temporalオブジェクトから文字列への変換

を行う方法について解説したいと思います。

# 文字列からTemporalオブジェクトへの変換

[前々回の記事](https://qiita.com/access3151fq/items/05dba729b35bc2d8b3d0)で解説したように、Temporalいは日付や時刻を表すための型として`Temporal.Instant`、`Temporal.PlainXXX`系、`Temporal.ZonedDateTime`の3タイプの型があります。

それぞれの型が、どのような文字列表現と関連付けられているのかは、以下の公式ドキュメントの図が分かりやすいです。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/1021525c-b0b0-126c-f79c-9cfbe89acd35.png)
> https://tc39.es/proposal-temporal/docs/index.html#string-persistence-parsing-and-formatting

このような、Temporal仕様やISO8601に従った形式なら、各クラスの`.from()`静的メソッドで簡単にパースできます。

### from()によるパース

`.from()`静的メソッドで文字列をパースするには、以下のようにします。

```ts:.from()静的メソッドによるパース (公式ドキュメントより)
// Temporal.Instantの生成
// Temporal.Instantはタイムゾーンなどの情報を持たないため、それらの情報を与えても無視される
const instant1 = Temporal.Instant.from("2019-03-30T00:45Z");
console.log(instant1.toString()); // 2019-03-30T00:45:00Z
const instant2 = Temporal.Instant.from("2019-03-30T01:45+01:00");
console.log(instant2.toString()); // 2019-03-30T00:45:00Z
const instant3 = Temporal.Instant.from("2019-03-30T01:45:00+01:00[Europe/Berlin]");
console.log(instant3.toString()); // 2019-03-30T00:45:00Z

// Temporal.PlainDateTimeの生成
// PlainDateは年月日時分秒のみを保持するため、タイムゾーンの情報は捨てられる
const plainDateTime1 = Temporal.PlainDateTime.from("1995-12-07T03:24:30");
console.log(plainDateTime1.toString()); // => 1995-12-07T03:24:30
const plainDateTime2 = Temporal.PlainDateTime.from("1995-12-07T03:24:30+01:00[Europe/Brussels]");
console.log(plainDateTime2.toString()); // => 1995-12-07T03:24:30

// Temporal.PlainDateの生成
// PlainDateは年月日のみを保持するため、それ以外の情報は捨てられる
const plainDate1 = Temporal.PlainDate.from("2006-08-24");
console.log(plainDate1.toString()); // => 2006-08-24
const plainDate2 = Temporal.PlainDate.from("2006-08-24T15:43:27");
console.log(plainDate2.toString()); // => 2006-08-24
const plainDate3 = Temporal.PlainDate.from("2006-08-24T15:43:27+01:00[Europe/Brussels]");
console.log(plainDate3.toString()); // => 2006-08-24

// Temporal.PlainTimeの生成
// PlainTimeは時分秒のみを保持するため、それ以外の情報は捨てられる
const plainTime1 = Temporal.PlainTime.from("03:24:30");
console.log(plainTime1.toString()); // => 03:24:30
const plainTime2 = Temporal.PlainTime.from("1995-12-07T03:24:30");
console.log(plainTime2.toString()); // => 03:24:30
const plainTime3 = Temporal.PlainTime.from("1995-12-07T03:24:30+01:00[Europe/Brussels]");
console.log(plainTime3.toString()); // => 03:24:30

// Temporal.PlainYearMonthの生成
// PlainDateは年月のみを保持するため、それ以外の情報は捨てられる
const plainYearMonth1 = Temporal.PlainYearMonth.from("2019-06");
console.log(plainYearMonth1.toString()); // => 2019-06
const plainYearMonth2 = Temporal.PlainYearMonth.from("2019-06-24");
console.log(plainYearMonth2.toString()); // => 2019-06
const plainYearMonth3 = Temporal.PlainYearMonth.from("2019-06-24T15:43:27");
console.log(plainYearMonth3.toString()); // => 2019-06
const plainYearMonth4 = Temporal.PlainYearMonth.from("2019-06-24T15:43:27+01:00[Europe/Brussels]");
console.log(plainYearMonth4.toString()); // => 2019-06

// Temporal.PlainMonthDayの生成
// PlainMonthDayは月日のみを保持するため、それ以外の情報は捨てられる
const plainMonthDay1 = Temporal.PlainMonthDay.from("08-24");
console.log(plainMonthDay1.toString()); // => 08-24
const plainMonthDay2 = Temporal.PlainMonthDay.from("2006-08-24");
console.log(plainMonthDay2.toString()); // => 08-24
const plainMonthDay3 = Temporal.PlainMonthDay.from("2006-08-24T15:43:27");
console.log(plainMonthDay3.toString()); // => 08-24
const plainMonthDay4 = Temporal.PlainMonthDay.from("2006-08-24T15:43:27+01:00[Europe/Brussels]");
console.log(plainMonthDay4.toString()); // => 08-24

// Temporal.ZonedDateTimeの生成
const zonedDateTime = Temporal.ZonedDateTime.from("2003-09-12T05:08:34+09:00[Asia/Tokyo]");
console.log(zonedDateTime.toString()); // 2003-09-12T05:08:34+09:00[Asia/Tokyo]
```

先ほど紹介した通り、`.from()`に渡す文字列は、Temporal仕様やISO8601に従った形式でなければいけません。更に、必要な情報が与えられなかった場合（`ZonedDateTime`にタイムゾーン情報が与えられなかった場合など）は`RangeError`が発生します。

### 正規表現を使用してパースする

実際にTemporalを使用する場面では、ISO8601形式でない文字列もパースする必要があるかもしれません。
その場合は、[正規表現の名前付きキャプチャグループ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_expressions/Groups_and_backreferences)と組み合わせることで簡単にパースできます。

正規表現の名前付きキャプチャグループは、`(?<Name>x)`の形で、正規表現のマッチ結果に名前を付けることができる機能です。
例えば、`年/月/日 時:分`の形の文字列を名前付きキャプチャグループを使ってパースする正規表現は、次のようになります。

(`\d{4}`は数字4桁、`\d{1,2}`は数字1桁または2桁を表します。それぞれに`year`, `month`, ...という名前を付けています。)

```ts
const str = "2021/7/21 10:00";

const matches = str.match(
  /(?<year>\d{4})\/(?<month>\d{1,2})\/(?<day>\d{1,2}) (?<hour>\d{1,2}):(?<minute>\d{1,2})/u,
)?.groups;

console.log(matches);
// {
//  year: "2021",
//  month: "7",
//  day: "21",
//  hour: "10",
//  minute: "00"
// }
```

マッチ結果は`groups`プロパティから取得できます。これを、そのままTemporalの`.from()`静的メソッドに入れることができます。

```ts
const str = "2021/7/21 10:00";

const matches = str.match(
  /(?<year>\d{4})\/(?<month>\d{1,2})\/(?<day>\d{1,2}) (?<hour>\d{1,2}):(?<minute>\d{1,2})/u,
)?.groups;
console.log(matches);

// マッチ結果がnullの場合はパースに失敗している
if (!matches) {
  throw new Error("failed to parse date.");
}

console.log(Temporal.PlainDateTime.from(matches).toString()); // => 2021-07-21T10:00:00
```

このように、正規表現の名前付きキャプチャグループと組み合わせることで、ライブラリ等を使用することなく、任意の形式の日付と時刻をパースすることができます。

# Temporalオブジェクトから文字列への変換

Temporalオブジェクトから文字列への変換には、`.toString()`、`.toJSON()`、`.toLocaleString()`を使用する事ができます。

:::note
もちろん、`year`や`month`などのプロパティ読み取って手動で文字列結合する方法もあります。

```ts
const zonedDateTime = Temporal.ZonedDateTime.from({
  timeZone: "Asia/Tokyo",
  year: 2023,
  month: 4,
  day: 8,
  hour: 22,
  minute: 34,
  second: 18,
  millisecond: 4,
  microsecond: 2,
  nanosecond: 1,
});
console.log(zonedDateTime.year); // => 2023
console.log(zonedDateTime.month); // => 4
console.log(zonedDateTime.monthCode); // => "M04"
console.log(zonedDateTime.day); // => 8
console.log(zonedDateTime.hour); // => 22
console.log(zonedDateTime.minute); // => 34
console.log(zonedDateTime.second); // => 18
console.log(zonedDateTime.millisecond); // => 4
console.log(zonedDateTime.microsecond); // => 2
console.log(zonedDateTime.nanosecond); // => 1
```
:::

`.toString()`メソッドと`.toJSON()`メソッドは、`.from()`関数でもう一度パースできるような形式で日時を文字列化します。
(`.toJSON()`メソッドは`JSON.stringify()`関数でJSON文字列化した時に自動で呼ばれるメソッドです。)

日時のうち「年月日」の部分だけハイフン区切りで欲しい時は、`.toPlainDate()`で`Temporal.PlainDate`型に先に変換しておくという方法が考えられます。（同様に`.toPlainTime()`で「時分秒」を取り出すこともできます。）

```ts
console.log(zonedDateTime.toString()); // 2023-04-08T22:34:18.004002001+09:00[Asia/Tokyo]
console.log(zonedDateTime.toPlainDate().toString()); // 2023-04-08
console.log(zonedDateTime.toPlainTime().toString()); // 22:34:18.004002001
```

```ts
console.log(JSON.stringify(zonedDateTime)); // "2023-04-08T22:34:18.004002001+09:00[Asia/Tokyo]"
```

`.toLocaleString()`メソッドを使用すると、人間が読める形式で文字列化することができます。
このメソッドはオプションが多いのですが、使用できる値は[Intl.DateTimeFormat()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat)の引数と同じです。

```ts
console.log(zonedDateTime.toLocaleString("ja-jp")); // 2023/4/8 22:34:18 JST

// weekday, era, dayPeriodオプション
console.log(zonedDateTime.toLocaleString("ja-jp", {
  weekday: "narrow", // 曜日の表示形式
  era: "narrow", // 元号の表示形式(AD/西暦, R/令和)
  dayPeriod: "narrow",
})); // AD2023年4月8日土 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-jp", {
  weekday: "short",
  era: "short",
  dayPeriod: "short",
})); // 西暦2023年4月8日土 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-jp", {
  weekday: "long",
  era: "long",
  dayPeriod: "long",
})); // 西暦2023年4月8日土曜日 22:34:18 JST

// year, month等のオプション
console.log(zonedDateTime.toLocaleString("ja-jp", {
  // "2-digit": 0埋めして2桁で表示
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
})); // 23/04/08 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-jp", {
  // "numeric": 0埋めせず表示
  year: "numeric",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
})); // 2023/4/8 22:34:18 JST

// dateStyleオプションとtimeStyleオプション
console.log(zonedDateTime.toLocaleString("ja-jp", {
  dateStyle: "full", // 日付部分のスタイルを一括指定
  timeStyle: "full", // 時刻部分のスタイルを一括指定
})); // 2023/4/8土曜日 22時34分18秒 日本標準時
console.log(zonedDateTime.toLocaleString("ja-jp", {
  dateStyle: "long",
  timeStyle: "long",
})); // 2023/4/8 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-jp", {
  dateStyle: "medium",
  timeStyle: "medium",
})); // 2023/04/08 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-jp", {
  dateStyle: "short",
  timeStyle: "short",
})); // 2023/04/08 22:34:18 JST

// fractionalSecondDigitsとtimeZoneNameオプション
console.log(zonedDateTime.toLocaleString("ja-jp", {
  fractionalSecondDigits: 3, // 秒の小数部分を第何位まで表示するか
  timeZoneName: "shortGeneric", // タイムゾーンをどのように表示するか
})); // 2023/4/8 22:34:18.004 JST
console.log(zonedDateTime.toLocaleString("ja-jp", {
  fractionalSecondDigits: 1,
  timeZoneName: "long",
})); // 2023/4/8 22時34分18.0秒 日本標準時

// localeに"ja-JP-u-ca-japanese"を指定することで、西暦表示ではなく和暦表示できる
console.log(zonedDateTime.toLocaleString("ja-JP-u-ca-japanese")); // R5/4/8 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-JP-u-ca-japanese", {
  weekday: "narrow",
  era: "narrow",
  dayPeriod: "narrow",
})); // R5年4月8日土 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-JP-u-ca-japanese", {
  weekday: "short",
  era: "short",
  dayPeriod: "short",
})); // 令和5年4月8日土 22:34:18 JST
console.log(zonedDateTime.toLocaleString("ja-JP-u-ca-japanese", {
  weekday: "long",
  era: "long",
  dayPeriod: "long",
})); // 令和5年4月8日土曜日 22:34:18 JST
```

# まとめ

Temporalオブジェクトと文字列の相互変換について解説しました。

- ISO8601形式の文字列→Temporalオブジェクト：Temporalの各クラスの`.from()`静的メソッドを使用する
- 任意の形式の文字列→Temporalオブジェクト：正規表現の名前付きキャプチャグループでパースしてから`.from()`静的メソッドを使用すると楽
- Temporalオブジェクト→文字列：`.toString()`, `.toJSON()`, `.toLocaleString()`を使用する。

`%Y%m%d%H%M%S`のような形式が利用できるフォーマット関数が無いのは、何でも引数に取れてしまう`new Date()`コンストラクタへの反省とも言えるかもしれません。[Temporal.parse](https://tc39.es/proposal-temporal/docs/parse-draft.html)の草案ドラフトを読んで、Temporalの目的はそのような「便利な」APIを導入することではなく、厳密に型指定された日付操作を仕様に従って正しく処理することなのではないかと感じました。
おそらくそうした便利機能はライブラリに任せて、Temporalではタイムゾーンなどの厳密な処理に特化するという方針なのだと思います。