JavaScriptの新しい標準日付ライブラリ Temporalまとめ

2023/04/07に公開

JavaScriptの標準ライブラリに入っているDateはタイムゾーンを扱いづらいところがあって、悩みのタネになることがあります。

そこでTemporalです。Temporalには下記のような特徴があり、タイムゾーン問題を始めDateで悩みのタネであった課題を多く解決しています。

  • 日付と時刻を操作する簡単で使いやすい API を提供します
  • DST を考慮した演算と、すべてのタイムゾーンをサポートします
  • オブジェクトは特定の日時や時刻を明確に表します
  • 厳格に定義された文字列をパースします
  • グレゴリオ暦以外のカレンダーをサポートします

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

さらにプロポーザルはStage 3まで進んでいて、将来標準ライブラリ入りする可能性も高いです。

特にサーバサイドJavaScript環境では、今後Temporalをベースに日時を処理する、というのが主流になるのではないかと思ったので、そのAPIや使い方について調べました。

Temporalはドキュメントが充実しており、読めばだいたいのことがわかります。
https://tc39.es/proposal-temporal/docs/ja/

ポリフィル

既にいくつか実装があるので、それを動かして確認すると良いです。

各クラスの役割

オブジェクトの関係図を見ると理解しやすいです。


https://tc39.es/proposal-temporal/docs/ja/index.html#object-%E3%81%AE%E9%96%A2%E4%BF%82%E5%9B%B3

TemporalのコアコンセプトはWall-Clock TimeとExact Timeの区別にあります。
Wall-Clock Timeはカレンダーを持っていて(PlainTimeを除く)、現地時間を知っているものです。
Exact Timeはエポックからの経過時間(UNIX時間)を知っているものです。

日時を表現するクラスについてざっくりまとめます。

クラス名 説明
PlainXX 日時のみを持つ。UNIX時間やタイムゾーンの情報は持っていない
ZonedDateTime UNIX時間とタイムゾーンを両方持ってる。Exact TimeとWall-Clock Timeの両方に属する
Instant UNIX時間しか知らない。タイムゾーンを持ってないので、このクラス単体では日付や時間は得られない。そういうことをしたければZonedDateTimeを使う

これに加えて、ZonedDateTimeとPlainXX(PlainTime除く)は「カレンダー」というものを持ってるのですが、これについては後述します。

残りのTimeZone、Calendar、 Durationは補助的な役割のクラスで、ざっくりまとめると下記のようになります。

クラス名 説明
Duration 時刻の加算・減算をするときなどに使う
TimeZone タイムゾーンを指定するときに使える。例えばIsntantからZonedDateTimeの変換をするときなど。タイムゾーンは文字列(IANA Name)で指定することもできる
Calendar 後述

このあたりは、文字列表現から見るとイメージしやすいかもしれません。


https://tc39.es/proposal-temporal/docs/ja/index.html#%E6%96%87%E5%AD%97%E5%88%97%E3%81%AB%E3%82%88%E3%82%8B%E6%B0%B8%E7%B6%9A%E6%80%A7

カレンダーについて

例えば、中国暦やユダヤ暦など一部の暦だと、うるう年は1年が13ヶ月になる、というようなiso8601のカレンダーと異なる特殊なケースがあります。そういった特殊な事情に対応するための抽象化がカレンダーです。

Intl.DateTimeFormat で使えるカレンダーがそのまま使えます。

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

例えばjapaneseカレンダーだと、iso8601カレンダーをベースに和暦に対応している、といった感じです。

const { Temporal, Intl, toTemporalInstant } = require('@js-temporal/polyfill');

// 2023/04/01
// (iso8601カレンダーだとera, eraYearはundefinedになる)
const { era, eraYear } = Temporal.Now.zonedDateTime("japanese", "Asia/Tokyo");

// reiwa
// 5
console.log(era, eraYear);

フォーマットについて

各オブジェクトの toLocaleString が使えます。
toLocaleString の引数にはとIntl.DateTimeFormatで使う localeとoptionsを渡せます。
ただ、@js-temporal/polyfill だと、試した範囲でフォーマットが正常に動作してないように見えるところがありました。under constructionと書いてるので、まだ実装できてない箇所があるのかもしれません。

import { Temporal } from "@js-temporal/polyfill";

// こっちだとうまく動く
// import { Temporal, Intl, toTemporalInstant } from "temporal-polyfill";


const now = Temporal.Now.instant();
const fmtArgs = [
  ["ja-JP-u-ca-japanese"],
  {
    era: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
  }
];

// actual: “令和5/4/1”
// expected: “令和5年2月6日”
console.log(now.toLocaleString(...fmtArgs));

// UNIX時間に変換してIntl.DateTimeFormat.format を直接呼ぶと期待通りに動く
// "令和5年4月1日"
console.log(new Intl.DateTimeFormat(...fmtArgs).format(now.epochMilliseconds));

Discussion