Temporal入門その2 ～ 実際に日付と時刻を操作してみる

# 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/05dba729b35bc2d8b3d0)に引き続き、日付と時刻の処理を試していきます。

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

# 日付と時刻の操作

[前回の記事](https://qiita.com/access3151fq/items/05dba729b35bc2d8b3d0)では、日付や時刻を表すために`Temporal.Instant`、`Temporal.PlainXXX`系、`Temporal.ZonedDateTime`の3タイプの型があると解説しました。
ただし、細かい違いはあるものの、**これらの操作方法は基本的に同一になるように設計されています**。（例えば、`equals`メソッドは全ての型に実装されていて、使い方も共通。）

以下では例として`Temporal.ZonedDateTime`を使用して解説しますが、一部のケースを除いて他の型でも同じように使用できます。

### 年月日の取得

`year`(年)、`month`(月)、`day`(日)、`hour`(時)、`minute`(分)、`second`(秒)などの情報はプロパティから取得することができます。

```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
```

### 曜日など詳しい情報を取得する

`Temporal.ZonedDateTime`や`Temporal.PlainXXX`を使用している場合、カレンダー情報を基に日付に関する詳しい情報を取得できます。

例えば、何曜日かについては`zonedDateTime.dayOfWeek`で取得します。

```ts
// 何曜日か（月曜日が1、日曜日が7）
console.log(zonedDateTime.dayOfWeek); // => 6
// その年の何日目か
console.log(zonedDateTime.dayOfYear); // => 98
// ISO週番号方式において、その年の何週目か
// 詳しくは=> https://casualstartup.hatenablog.jp/entry/20130908/iso_weeknum
console.log(zonedDateTime.weekOfYear); // => 14
// ISO週番号方式において何年か
// 詳しくは=> https://casualstartup.hatenablog.jp/entry/20130908/iso_weeknum
console.log(zonedDateTime.yearOfWeek); // => 2023
// 1週間は何日か（通常は7日）
console.log(zonedDateTime.daysInWeek); // => 7
// (その月の) 1か月は何日あるか
console.log(zonedDateTime.daysInMonth); // => 30
// (その年の) 1年間は何日あるか
console.log(zonedDateTime.daysInYear); // => 365
// (その年の) 1年間は何か月あるか
console.log(zonedDateTime.monthsInYear); // => 12
// その年はうるう年か
console.log(zonedDateTime.inLeapYear); // => false
// (その日の) 1日は何時間か（通常24時間だがサマータイムによって差あり）
console.log(zonedDateTime.hoursInDay); // => 24
// その日の起点時刻（＝0時00分）を取得する
// サマータイムによって0時00分にならないこともある
console.log(zonedDateTime.startOfDay()); // => Temporal.ZonedDateTime { 2023-04-08T00:00:00+09:00[Asia/Tokyo] }
```


### 日付の足し算、引き算

Temporalでは、日時同士の以下のような計算をサポートしています。

- 日時 + 時間 = 日時 (`.add()`メソッド)
- 日時 - 時間 = 日時 (`.subtract()`メソッド)
- 日時 - 日時 = 時間 (`.until()`メソッドと`.since()`メソッド)

これらを利用することで、「翌日の同じ時刻」「翌月の同じ日」などを求めることができます。

```ts:足し算 (日時 + 時間 = 日時)
zonedDateTime.add({ days: 1 }); // 1日後の日付が返る
zonedDateTime.add({ weeks: 2 }); // 2週間後の日付
zonedDateTime.add({ months: 3 }); // 3か月後の日付
zonedDateTime.add({ years: 4 }); // 4年後の日付
```


```ts:引き算 (日時 - 時間 = 日時)
zonedDateTime.subtract({ days: 1 }); // 1日前の日付が返る
zonedDateTime.subtract({ weeks: 2 }); // 2週間前の日付
zonedDateTime.subtract({ months: 3 }); // 3か月前の日付
zonedDateTime.subtract({ years: 4 }); // 4年前の日付
```


```ts:引き算 (日時 - 日時 = 時間)
const timeA = Temporal.ZonedDateTime.from({
  timeZone: "Asia/Tokyo",
  year: 2022,
  month: 3,
  day: 7,
  hour: 21,
  minute: 33,
  second: 17,
});
const timeB = Temporal.ZonedDateTime.from({
  timeZone: "Asia/Tokyo",
  year: 2023,
  month: 4,
  day: 8,
  hour: 22,
  minute: 34,
  second: 18,
});

// timeB - timeA を計算する
const duration1 = timeA.until(timeB);
console.log(duration1.years);
console.log(duration1.months);
console.log(duration1.weeks);
console.log(duration1.days);
console.log(duration1.hours);
console.log(duration1.minutes);
console.log(duration1.seconds);
console.log(duration1.milliseconds);
console.log(duration1.microseconds);
console.log(duration1.nanoseconds);

// timeA - timeB を計算する
const duration2 = timeA.since(timeB); 
console.log(duration2.years);
console.log(duration2.months);
console.log(duration2.weeks);
console.log(duration2.days);
console.log(duration2.hours);
console.log(duration2.minutes);
console.log(duration2.seconds);
console.log(duration2.milliseconds);
console.log(duration2.microseconds);
console.log(duration2.nanoseconds);
```

`A.since(B)`と`A.until(B)`は符号が逆になっているだけで、同じ操作です。

`.since()`と`.until()`の返り値は`Temporal.Duration`型です。
`Temporal.Duration`型を人間が読める形にフォーマットする`Intl.DurationFormat`という関数が提案されていますが、現状では使えるブラウザは少ないです。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat

### 一部の値を変更して新しい日付を生成する

日付時刻のうち、一部分だけ書き換えたい場合があります。

例えば、その月の初めの日（1日）を求めたい時は、元のオブジェクトの`day`の部分だけを`1`に書き換えることで可能です。

そのような処理は、`.with()`メソッドで可能です。

```ts
// 元の日付から、dayの値のみが1に変更された新しいオブジェクトを返す
console.log(zonedDateTime.with({ day: 1 })); // 2023-04-01T22:34:18.004002001+09:00[Asia/Tokyo]
// 元の日付から、hourとminuteの値のみが0に変更された新しいオブジェクトを返す
console.log(zonedDateTime.with({ hour: 0, minute: 0 })); // 2023-04-08T00:00:18.004002001+09:00[Asia/Tokyo]
```

Temporalはイミュータブルなデータ構造のため、`.with()`メソッドは新しいインスタンスを返します（元のオブジェクトは変更されません）。

同様のメソッドに、`withPlainTime`, `withPlainDate`, `withTimeZone`, `withCalendar`が存在します。

```ts
zonedDateTime.withTimeZone('Europe/London'); // => London時間で言うと何時か
```

### 日付の丸め処理

`.round()`メソッドを使用すると、好きな単位に日付を四捨五入、切り捨て、切り上げできます。

```ts
// smallestUnitで、どの単位で丸めるかを指定できる
console.log(zonedDateTime.round({ smallestUnit: "second" }));

// roundingIncrementで、丸める大きさを指定できる
// この場合は20分単位に丸める
console.log(zonedDateTime.round({
  roundingIncrement: 20,
  smallestUnit: "minute"
}));

// roundingModeで、四捨五入するか切り上げるか切り捨てるかを選べる
// - "ceil": 切り上げ
// - "floor": 切り捨て
// - "halfCeil": 四捨五入（ちょうど時刻の中間地点で、丸める先が選べない場合は、切り上げられる）
// - "halfFloor": 四捨五入（ちょうど時刻の中間地点で、丸める先が選べない場合は、切り捨てられる）
// - "halfEven": 四捨五入（ちょうど時刻の中間地点で、丸める先が選べない場合は、偶数丸め）
// デフォルトは"halfExpand"
console.log(zonedDateTime.round({
  roundingIncrement: 20,
  smallestUnit: "minute",
  roundingMode: "floor",
}));
```

なお、`roundingIncrement`の値は、86400秒＝24時間を均等に分割できる値でなければならないそうです。（15分はOKだが7秒はNG）

### 日時のソート、比較

まず、日時と日時が等しいか調べる方法です。
Temporalの日時はクラスのインスタンスなので、`===`演算子で比較しても正しい結果はえられません。

```ts
const timeA = Temporal.ZonedDateTime.from({
  timeZone: "Asia/Tokyo",
  year: 2023,
  month: 4,
  day: 8,
});
const timeB = Temporal.ZonedDateTime.from({
  timeZone: "Asia/Tokyo",
  year: 2023,
  month: 4,
  day: 8,
});
// ❌ 正しく比較できない
console.log(timeA === timeB); // 同じ日時を表しているが、false
```

日時と日時が等しいか調べるには、`.equals()`メソッドを使用します。

```ts:日時と日時が等しい値か調べる
console.log(timeA.equals(timeB)); // => true
```

日時と日時を大小比較するには、各クラスのstatic methodである`compare`関数を使用します。

`compare`関数の返り値は`-1`か`0`か`1`です。
`compare(timeA, timeB)`のように引数を渡した場合の戻り値は、

- timeAよりtimeBが後の場合、`-1`
- timeAとtimeBが同じ日時の場合、`0`
- timeAよりtimeBが後の場合、`1`

のようになります。

```ts:日時の大小比較
// 以下の関数は全て、-1か0か1を返す

// timeAよりtimeBが後の場合、-1
// timeAとtimeBが同じ日時の場合、0
// timeAよりtimeBが後の場合、1

Temporal.Instant.compare(timeA, timeB); // Instantを大小比較する
Temporal.PlainDateTime.compare(timeA, timeB); // PlainDateTimeを大小比較する
Temporal.PlainDate.compare(timeA, timeB); // PlainDateを大小比較する
Temporal.PlainTime.compare(timeA, timeB); // PlainTimeを大小比較する
Temporal.PlainYearMonth.compare(timeA, timeB); // PlainYearMonthを大小比較する
// Temporal.PlainMonthDay.compare(timeA, timeB);
Temporal.ZonedDateTime.compare(timeA, timeB); // ZonedDateTimeを大小比較する
```

なお、`Temporal.PlainMonthDay.compare`は今のところ存在しません。
将来的には[proposal-temporal-v2](https://github.com/js-temporal/proposal-temporal-v2/issues/18)で導入される可能性があります。

配列に入った日時をソートすることもできます。
配列内の日時をソートするには、`Array.prototype.sort`に渡す比較関数に先ほどの`compare`関数を渡すだけです。

```ts:日時が入った配列をソート
const array = [.....]; // 日時が入った配列
array.sort(Temporal.Instant.compare); // Instantの配列をソートする
array.sort(Temporal.PlainDateTime.compare); // PlainDateTimeの配列をソートする
array.sort(Temporal.PlainDate.compare); // PlainDateの配列をソートする
array.sort(Temporal.PlainTime.compare); // PlainTimeの配列をソートする
array.sort(Temporal.PlainYearMonth.compare); // PlainYearMonthの配列をソートする
// array.sort(Temporal.PlainMonthDay.compare);
array.sort(Temporal.ZonedDateTime.compare); // ZonedDateTimeの配列をソートする
```

### Unix Timeの取得

Unix Timeは`epochMilliseconds`などのプロパティから取得できます。

マイクロ秒とナノ秒では、値はBigInt型になます。

```ts
console.log(zonedDateTime.epochSeconds); // => 1680960858 (秒)
console.log(zonedDateTime.epochMilliseconds); // => 1680960858004 (ミリ秒)
console.log(zonedDateTime.epochMicroseconds); // => 1680960858004002n (マイクロ秒)
console.log(zonedDateTime.epochNanoseconds); // => 1680960858004002001n (ナノ秒)
```




### 元号の取得

`zonedDateTime.era`は通常は`undefined`ですが、`Temporal.ZonedDateTime`の生成時に`[u-ca=japanese]`を指定することで、「Heisei」や「Reiwa」などの元号を取得できます。

```ts
const zonedDateTime = Temporal.ZonedDateTime.from("2023-02-23T03:24:30[Asia/Tokyo][u-ca=japanese]");
console.log(zonedDateTime.era); // => "reiwa"
console.log(zonedDateTime.eraYear); // => 5
```

# まとめ

Temporalを使って日付と時刻を操作する方法について解説しました。

Temporalを使用すると、日付と時刻に対して以下のような操作を行うことができます。

- 年月日の取得 (`.year`や`.month`などのプロパティ)
- 曜日など詳しい情報の取得 (`.dayOfWeek`などのプロパティ)
- 日付の足し算、引き算 (`add`, `subtract`, `until`, `since`メソッド)
- 一部の値を変更して新しい日付を生成する (`with`メソッド)
- 日付の丸め処理 (`round`メソッド)
- 日時のソート、比較 (`equals`メソッド、`compare`関数)
- Unix Timeの取得 (`epochMilliseconds`プロパティなど)
- 元号の取得 (`era`プロパティなど)

この記事に続いて、[Temporalオブジェクトと文字列の間の変換方法についての記事](https://qiita.com/access3151fq/items/ec27a14271c99a379af0)も書きましたので、合わせてご覧ください。

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