本連載について
はじめまして! サイボウズ フロントエンドエキスパートチームの左治木です。
本連載では、Webフロントエンドに関してもう一歩踏み込んだ知識について、サイボウズフロントエンドエキスパートチームのメンバーによって不定期で解説記事を掲載しています。前回までの記事では
JS Modern Features / JavaScriptの進歩
この記事からは
JavaScriptはWebの中核技術としてはもちろん、サーバサイドやモバイル、CLIツールの開発でも広く使われている言語です。JavaScriptの標準仕様は
この
初回となる今回は
新しい日時操作の仕様:Temporal
JavaScriptを利用している方の多くはJavaScriptで日時を扱ったことがあると思います。JavaScriptでは標準でDate
オブジェクトが存在し、日時データの保持や簡単な日付の計算・
一方でこのDate
オブジェクトはタイムゾーンサポートが足りなかったり、暗黙的な挙動が多かったりといった問題点を抱えていることも知られています。そのため代わりにday.
このような中、Dateオブジェクトに替わる新しい日時操作オブジェクトの仕様としてTemporalと呼ばれる組込みAPIがECMAScriptに提案されています。Temporalは現在Stage3の提案ですが、2024年からいくつかのランタイムやブラウザで試験的なサポートが始まるなど少しずつ試せる環境が増えてきている仕様です。
今回はこのTemporalについて、そのコンセプトや基本的な使い方、Intlとの関連、サポート状況などを紹介します。
既存のDateに対する不満
そもそもTemporalが提案された背景には、Dateオブジェクトに対する問題点や不満があります。これは具体的には以下のようなものです。
- ユーザーのシステムタイムゾーンとUTC以外のタイムゾーンはサポートされない
- 非グレゴリオ暦がサポートされていない
- 日時文字列のパース動作の信頼性が低い
- 日付オブジェクトの可変性の問題
- 日時計算のAPIが扱いにくい
タイムゾーンとカレンダーのサポート問題
1と2に関しては書いてあるとおりで、DateオブジェクトはシステムのタイムゾーンとUTCしかサポートしておらず、グレゴリオ暦以外のローカルな暦にも対応していません。
日時文字列のパース問題
3はnew Date()
で日時文字列を渡した場合、どう解釈されるかが分かりにくいという問題です。たとえばnew Date('2025-01-01')
とした場合、これはUTCの2025年1月1日0時0分0秒として解釈されます。しかし時刻まで含めたnew Date('2025-01-01T00:00:00')
とした場合、これはシステムのタイムゾーンでの2025年1月1日0時0分0秒として解釈されます。これは、タイムゾーンオフセットやZ
// システムのタイムゾーンがUTC+9の場合
new Date("2025-01-01"); // Wed Jan 01 2025 09:00:00 GMT+0900 (日本標準時)
new Date("2025-01-01T00:00:00"); // Wed Jan 01 2025 00:00:00 GMT+0900 (日本標準時)
また、ECMAScript仕様書としては書かれていないものですが、多くのランタイムでは"Wed Jan 01 2025"のようなRFC 2822形式の日時文字列や"2025/
// Chrome かつ システムのタイムゾーンがUTC+9の場合
new Date("Wed Jan 01 2025"); // Wed Jan 01 2025 00:00:00 GMT+0900 (日本標準時)
new Date("2025/01/01"); // Wed Jan 01 2025 00:00:00 GMT+0900 (日本標準時)
Date オブジェクトの可変性の問題
Dateオブジェクトのset系のメソッドはそのオブジェクトが持つ日時を破壊的に変更します。この挙動はset系のメソッドを日時の計算に使う場合、ミスを招く原因となります。たとえば以下のようなコードはオブジェクトの破壊的な変更を意識できていないコード例です。
const addOneWeek = (date) => {
date.setDate(date.getDate() + 7);
return date;
};
const today = new Date("2025-01-01"); //
const nextWeek = addOneWeek(today);
console.log(
`Today is ${today.toLocaleString()}, and one week from today will be ${nextWeek.toLocaleString()}`
);
// Today is 2025/1/8 9:00:00, and one week from today will be 2025/1/8 9:00:00
// todayを破壊的に変更してしまったため、nextWeekもtodayが同じ日時を指してしまっている
このような意図しない破壊的変更を防ぐためにはDateオブジェクトのset系のメソッドが新しいDateのインスタンスを返すようにするか、Dateオブジェクトの値自体を不変にすることが必要です。しかしながら既存の多くのコードはDateオブジェクトの破壊的な変更挙動に依存してしまっており、これらの挙動を変えることは困難です。
日時計算のAPIが扱いにくい問題
現在のDateオブジェクトには日時ライブラリで標準的にあるような日時の加算減算・
Temporalとそのコンセプト
このようにJavaScriptのDateオブジェクトには多くの問題点があり、Temporalはこれらの問題を解決することを目指しています。具体的には以下のような機能を追加することで上記の問題を解決します。
- Wall-Clock TimeとExact Timeの明確な分離
- タイムゾーンとカレンダーのサポート
- 豊富な計算API
- 明確な日時文字列
(タイムスタンプ) の定義とパース処理
Wall-Clock TimeとExact Timeの明確な分離
Temporalでは日時を表すデータ
具体的にはそれぞれ以下の日時データを表すクラスが
- ExactTime
Temporal.
: タイムスタンプデータInstant Temporal.
: タイムゾーンと日時がペアになったデータZonedDateTime
- WallClockTime
Temporal.
: タイムゾーンを持たない日時データPlainDateTime Temporal.
: タイムゾーンを持たない日付データPlainDate Temporal.
: タイムゾーンを持たない年月データPlainYearMonth Temporal.
: タイムゾーンを持たない月日データPlainMonthDay Temporal.
: タイムゾーンを持たない時刻データPlainTime
このようにWall-Clock TimeのClassは必ずPlain
で始まるので一目で区別がつくようなAPIになっているのも特徴です。
またWall-Clock Timeのデータは
このようにWall-Clock TimeとExactTimeの扱いをAPIレベルで明確に区別することで日時の取り扱いをより安全に行えます。
タイムゾーンとカレンダーのサポート
TemporalはDateオブジェクトと異なり、UTCやシステムのタイムゾーン以外のタイムゾーンをサポートしています。サポートされるTimeZoneは基本的にIANA Time Zone Databaseで管理されているタイムゾーンで、これらのタイムゾーンを指定しローカルな時間とUTC時刻・
またTemporalはユダヤ暦や中国の旧暦のようなグレゴリオ暦以外のカレンダーもサポートしています。これらの非グレゴリオ暦は1年が12ヵ月でなかったり、1ヵ月の日数がグレゴリオ暦と大きく異なるといった特徴があります。このような場合でもTemporalでは各暦での年月日から日時を指定したり、逆に特定のタイムスタンプからその暦での年月日を取得できます。
豊富な計算API
Temporalの日時を表すClassには日時計算のメソッドが用意されています。たとえば日時の加算減算を行うadd()/subtract()
メソッドや比較をするcompare()/equals()
メソッド、日時の差分計算のためのsince()/until()
メソッド、丸めのためのround()
メソッドなどが用意されています。
またこれらの計算が行いやすいように、日時を表すクラスとは別に日時の差分を表すTemporal.
明確な日時文字列(タイムスタンプ)の定義とパース処理
上記の問題点で挙げたとおり、既存のDateオブジェクトは日時文字列からパースする処理の信頼性が低いという問題がありました。またタイムゾーンやカレンダーの指定もサポートしていなかったため、そもそも解釈できる日時文字列にタイムゾーン情報やカレンダー情報を持たせることができませんでした。
Temporalではこれらの問題を解決するため、パースできる日時文字列を明確にRFC 9557(Internet Extended DateTime Format : IXDTF)形式のものと定義しています。
RFC 9557は既存のJavaScriptなどがサポートするRFC 3339/
2025-01-01T00:00:00+09:00[Asia/Tokyo][u-ca-japanese]
// [タイムゾーンID][u-ca=カレンダーID] のような形式の拡張部分
RFC 9557はRFC 3339/Temporal.
などのタイムゾーンを明確に含むべきデータを生成する際はタイムゾーン部分の情報がないとエラーになるという制約があります。このように、日時文字列のパースを厳格化することでパース処理が安定するようになっています。Temporal.
などのタイムゾーンを持たないデータを生成する際はタイムゾーン部分の情報は省略できますが、基本的にはタイムゾーン情報を含む形式の日時文字列で日時を指定することが推奨されます。
Temporalの基本的な使い方
Temporalの提案された背景やそのメリットを理解したところで、ここからは実際にTemporalのAPI見ていきましょう。
とはいえ、Temporalは非常に多くのAPIや機能を持つのですべてを解説することは難しいです。そこでここではよくありそうな日時操作のユースケースをもとに
サーバから取得した日時情報を表示する
サーバから取得した日時情報を表示する場合、取得した日時情報はUTCの日時フォーマットやタイムスタンプで表現されていることが一般的です。そのため、日時情報を表示するためにはユーザーの利用するローカルタイムゾーンに変換して表示する必要があります。
Temporalではタイムゾーンと日時がペアになったデータとしてTemporal.
が用意されており、このクラスのインスタンスはローカルな日時情報の取得や書式化機能を備えています。したがって、サーバから取得した日時情報をTemporal.
に変換できれば、その後の日時情報の取り扱いが容易になります。
サーバから取得した日時情報がUTCのタイムゾーン情報を含まない日時フォーマットだった場合、まずはTemporal.Temporal.
に変換します。その後、toZonedDateTimeISO()
メソッドでタイムゾーンを指定することでTemporal.
に変換できます。
const savedDateTime = "2025-01-01T00:00:00Z"; // サーバから取得した日時情報
const userTimeZone = "Asia/Tokyo"; // ユーザーのタイムゾーン
const zonedDateTime =
Temporal.Instant.from(savedDateTime).toZonedDateTimeISO(userTimeZone);
サーバから取得した日時情報がタイムスタンプであった場合はもう少しシンプルで、Temporal.Temporal.
を生成できます。ただしタイムスタンプはBigIntで表現されたナノ秒単位の値であることには注意が必要です。
const savedTimeStump = 1735689600000; // サーバから取得したタイムスタンプをnano秒に変換
const userTimeZone = "Asia/Tokyo"; // ユーザーのタイムゾーン
const zonedDateTime = new Temporal.ZonedDateTime(
BigInt(savedTimeStump) * 1000n,
userTimeZone
);
ナノ秒に変換するのが面倒な場合はTemporal.
のfromEpochMilliseconds()
メソッドを使ってTemporal.
を生成してからtoZonedDateTime()
メソッドを使ってTemporal.
に変換する方法もあります。
const savedTimeStump = 1735689600000; // サーバから取得したタイムスタンプ
const userTimeZone = "Asia/Tokyo"; // ユーザーのタイムゾーン
const zoneDateTime =
Temporal.Instant.fromEpochMilliseconds(savedTimeStump).toZonedDateTime(
userTimeZone
);
このようにして取得した日時情報をTemporal.
に変換できれば、Temporal.
のtoLocaleString()
メソッドを使ってユーザーのローカルタイムゾーンでの日時文字列を取得できます。
const localeDateTimeString = zonedDateTime.toLocaleString("ja-JP", {
dateStyle: "medium",
timeStyle: "short",
});
console.log(localeDateTimeString); // "2025年1月1日水曜日 9:00"
ちなみにTemporal.Intl.
のオプションと同様のものが使用でき、日時のフォーマット自体もIntl.
と同様の挙動になります。
ユーザーの入力した日時情報からサーバに送る情報を生成する
逆にユーザーが入力した日時情報をサーバに送るための日時情報を生成する場合、ユーザーが入力した日時情報はローカルなタイムゾーンで表現されています。そのため、ユーザーが入力した日時情報をUTCの日時フォーマットやタイムスタンプに変換してサーバに送る必要があります。
この場合もTemporal.
を経由することで、UTCの日時フォーマットやタイムスタンプに変換できます。
// UTCの日時フォーマットに変換
const utcDateTimeString = zonedDateTime.toString(). // "2025-01-01T09:00:00+09:00[Asia/Tokyo]"
// ミリ秒のタイムスタンプに変換
const utcDateTimeString = zonedDateTime.toInstant().epochMilliseconds; // "1735689600000"
つまり、ユーザーの入力値とユーザーの利用するタイムゾーンからTemporal.
を生成できれば上記のような変換処理を行えることになります。この方法は主に2つの方法があり、1つはTemporal.
を経由する方法、もう1つがTemporal.
のfrom()
メソッドを利用する方法です。
Temporalはタイムゾーン情報を持たないローカルな日時データを表すClassとしてTemporal.
を持っています。このTemporal.
は初期化時に第1引数から順に年月日時分秒……のように値を渡すことで生成できます。
const inputDateTime = new Temporal.PlainDateTime(2025, 1, 1, 9, 0); // 2025年1月1日9時0分
PlainDateTime
自体はタイムゾーン情報を持たないため、PlainDateTimeにあるtoZonedDateTime()
メソッドでタイムゾーンを指定しTemporal.
に変換できます。
const userTimeZone = "Asia/Tokyo"; // ユーザーのタイムゾーン
const zonedDateTime = inputDateTime.toZonedDateTime(userTimeZone);
またPlainDateTime
を経由せずに直接Temporal.
を生成する方法もあります。Temporal.
のfrom()
メソッドを使うことで、年月日時分秒……のような値をオブジェクトして渡しTemporal.
を生成できます。
const userTimeZone = "Asia/Tokyo"; // ユーザーのタイムゾーン
const zonedDateTime = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 9,
minute: 0,
timeZone: userTimeZone,
});
これだけ見るとTemporal.
のfrom()
メソッドのほうが簡潔でわかりやすいように見えますが、実際に使う場合はどのタイミングでタイムゾーン情報を持たせて変換するかがポイントになります。ユーザーの入力値から直接Temporal.
を生成しようとする場合、入力を処理する末端の処理で常にユーザーの利用するタイムゾーンを取得する必要があります。一方UTCの日時フォーマットやタイムスタンプに変換する必要があるのは値を保存してサーバなどに情報を送るタイミングですから、このような変換処理はもう少し上位の保存処理や送信処理で行う方が読み易いかもしれません。この場合変換処理までは一時的にPlainDateTime
で保持し、変換処理でTemporal.
に変換するという方法も十分に有用です。
JavaScriptで日付の計算を行う
最後に、Temporalを使ってJavaScriptで日付の計算をする際の方法を紹介します。Temporal.
やTemporal.
クラスはメソッドとして日時の計算用のメソッドを持っています。
add()
/subtract()
:日時の加算減算compare()
/equals()
:日時の比較since()
/until()
:日時の差分計算round()
:日時の丸め
これらの計算メソッドを使う上で、よく使われるのがTemporal.
クラスです。Temporal.
クラスは日時の長さを表すクラスで、以下のようにfromメソッドや初期化時に年数や日数などを指定して生成できます。
const duration = Temporal.Duration.from({ days: 1, hours: 12 }); // 1.5日
const duration_ = new Temporal.Duration(0, 0, 0, 1, 12); // 上と同じ
Temporal.
クラスは日時の計算メソッドの引数や戻り値として日時の加算減算や差分計算に使われます。
日時の加算減算
add()
/subtract()
メソッドは日時の加算減算を行うメソッドです。これらのメソッドは第1引数にTemporal.
を受け取り、そのぶんだけ日時を加算減算します。
add()
/subtract()
メソッドは日時の加算減算を行うメソッドです。これらのメソッドは第1引数にTemporal.
を受け取り、そのぶんだけ日時を加算減算します。
const zonedDateTime = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 9,
minute: 0,
timeZone: "Asia/Tokyo",
});
const duration = Temporal.Duration.from({ hours: 5 }); // 1.5日
const addedZonedDateTime = zonedDateTime.add(duration); // 2025/01/01 14:00 JST
日時の比較
compare()
/equals()
メソッドは日時を比較するメソッドです。compare()
メソッドは静的メソッドで引数に受け取った2つの日時を比較して、その前後関係を返します。第1引数の日時の方が前の場合は-1、第2引数の日時の方が前の場合は1、同じ場合は0を返します。
const zonedDateTime1 = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 9,
minute: 0,
timeZone: "Asia/Tokyo",
});
const zonedDateTime2 = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 14,
minute: 0,
timeZone: "Asia/Tokyo",
});
const compareResult = Temporal.ZonedDateTime.compare(
zonedDateTime1,
zonedDateTime2
); // -1 (zonedDateTime1 < zonedDateTime2 なので)
これはArray.
などで受け取るコールバックと同じインタフェースですので、日時の並び替えに利用できます。
equals()
メソッドはインスタンスに生えたメソッドで、元の日時と引数で渡された日時が等しいかどうかを返します。
const zonedDateTime1 = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 9,
minute: 0,
timeZone: "Asia/Tokyo",
});
const zonedDateTime2 = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 9,
minute: 0,
timeZone: "Asia/Tokyo",
});
const equalsResult = zonedDateTime1.equals(zonedDateTime2); // true
日時の差分計算
since()
/until()
メソッドは日時の差分計算をするメソッドです。これらは両方とも引数に渡された日時との差分をTemporal.
で返します。since()
メソッドは元の日時が引数の日時よりも後の場合は正の値、前の場合は負の値を返します。逆にuntil()
メソッドは元の日時が引数の日時よりも後の場合は負の値、前の場合は正の値を返します。
const zonedDateTime1 = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 9,
minute: 0,
timeZone: "Asia/Tokyo",
});
const zonedDateTime2 = Temporal.ZonedDateTime.from({
year: 2025,
month: 1,
day: 1,
hour: 14,
minute: 0,
timeZone: "Asia/Tokyo",
});
// 以下は両方 zonedDateTime2 - zonedDateTime1 を意味する
const duration = zonedDateTime2.since(zonedDateTime1); // 5時間
const duration2 = zonedDateTime1.until(zonedDateTime2); // 5時間
サポート状況と今後
最後にTemporalのサポート状況と今後の展望について見ていきましょう。
序盤に述べたとおり、TemporalはStage3の提案であり、まだ正式な仕様としては策定されていません。ただStage3は仕様策定中の最後の段階で試験的な実装が推奨されている状態です。そのため今後いくつかのランタイムで試験的なサポートが始まり、問題がなければ正式な仕様として策定される可能性が高いです。
2025年1月現在の各ブラウザのサポート状況は以下のとおりです。
- Chrome:サポートそれておらず実装も未定
- Firefox:Firefox 135のNightlyビルドで試験的なサポートが始まっている
- Safari:現在JavaScriptCoreで少しずつ実装が進んでいる
またNode.
- Node.
js:サポートそれておらず実装も未定 - Deno:version 1.
40から --unstable-temporal
フラグで試験的なサポートが始まっている
このようにランタイムでのサポートはまだまだなTemporalですが、いくつかPolyfillが存在しているので使用感を試すこともできます。
まとめ
今回は新しい日時操作API Temporalについて紹介しました。
まだまだ使える環境が限られているものの、JavaScriptの日時操作における問題点の多くを解決したAPIであり、今までライブラリ利用前提だった日時操作がこの標準前提になる日も近いかもしれません。ぜひ今のうちにTemporalのAPIやその設計思想、使い方を知って技術の選定などにも役立てくれるとうれしいです。