イベント(トライアルの有効期限、サブスクリプションのカウントダウン、SLA ウィンドウなど)間の経過時間や残り時間を計算し、それらの期間をローカライズされた人が読める形式で表示するには、これまで手動での計算や外部ライブラリが必要でした。以前の Date オブジェクトはエポック ミリ秒のみを追跡するため、暦月の計算、夏時間(DST)の移行、単位のバランス調整でエラーが発生しやすくなります。
Temporal API は、Temporal.ZonedDateTime を Temporal.Duration および Intl.DurationFormat と組み合わせて、DST セーフな時間差、構成可能な単位バランス、カレンダー対応の期間比較、ローカライズされた形式を提供します。
実装方法
Temporal.ZonedDateTimeエンドポイントを取得する: システム タイムゾーンの現在の時刻にはTemporal.Now.zonedDateTimeISO()を使用します。ターゲット イベントのタイムゾーンがユーザーのシステムのデフォルトと異なる場合は、明示的な IANA タイムゾーン文字列Temporal.Now.zonedDateTimeISO(timeZone)を渡します。.since()と.until()を使用して差分を計算する:now.since(start, options)を使用して、過去のイベントからの経過時間を計算します。now.until(end, options)を使用して、将来のイベントまでの残り時間を計算します。largestUnit('year'、'month'、'day'、'hour'など)とsmallestUnit('minute'など)を渡して、結果のTemporal.Durationのバランスを調整して丸めます。
- スタンドアロンの
Temporal.Durationオブジェクトのバランスを調整または比較する:Temporal.Duration.from({ minutes: 90 })でスタンドアロンの期間を作成し、duration.round({ largestUnit: 'hour' })で単位を再調整します(暦月、暦年、夏時間の移行をまたいで調整する場合はrelativeToを指定します)。Temporal.Duration.compare(d1, d2, { relativeTo })を使用して 2 つの期間を比較します。いずれかの期間にカレンダー単位(years、months、weeks)が含まれている場合、または夏時間の境界をまたぐ場合は、PlainDateまたはZonedDateTimeをrelativeToとして渡す必要があります。
- 人間向けとマシン向けの形式:
- ユーザー向けのローカライズされたテキストには、
duration.toLocaleString(locale, options)(Intl.DurationFormatに委任)を使用します。 duration.toString()は、機械で読み取り可能な ISO 8601 期間文字列(P1M2DT4Hなど)にのみ使用します。
- ユーザー向けのローカライズされたテキストには、
例: 試用期間の終了までの日数とローカライズされた形式
// 1. Get the current time in an explicit target IANA time zone (or omit the
// argument to use the system's current time zone).
const tz = 'America/New_York';
const now = Temporal.Now.zonedDateTimeISO(tz);
// 2. Combine separate date and time strings directly.
// Do not build an ISO string with template literals; chain Temporal methods instead.
const startDateStr = '2025-01-01';
const startTimeStr = '12:00:00';
const endDateStr = '2025-01-31';
const endTimeStr = '12:00:00';
const start = Temporal.PlainDate.from(startDateStr).toPlainDateTime(startTimeStr).toZonedDateTime(tz);
const end = Temporal.PlainDate.from(endDateStr).toPlainDateTime(endTimeStr).toZonedDateTime(tz);
// 3. Calculate differences using .since() and .until()
// ZonedDateTime differences default to largestUnit: 'hour' (days are not a fixed
// length across DST, so Temporal won't balance into days unless asked). Pass
// largestUnit: 'day' or a calendar unit ('week', 'month', 'year') when you want a
// human-scale breakdown instead of e.g. PT720H. smallestUnit rounds away
// seconds and sub-second noise so the formatted string stays readable.
const timeActive = now.since(start, { largestUnit: 'year', smallestUnit: 'minute' });
const timeRemaining = now.until(end, { largestUnit: 'year', smallestUnit: 'minute' });
// 4. Format durations for display with .toLocaleString() (Intl.DurationFormat)
// instead of manually concatenating unit numbers and suffixes. 'en-US' is used
// here for deterministic output; pass undefined to use the user's locale.
console.log(`Active: ${timeActive.toLocaleString('en-US', { style: 'narrow' })}`);
console.log(`Remaining: ${timeRemaining.toLocaleString('en-US', { style: 'long' })}`);
// Use .toString() for ISO 8601 serialization (e.g., API payloads or <time datetime="...">)
console.log(`ISO 8601: ${timeRemaining.toString()}`);
// 5. Check status via .sign, and compare durations with Temporal.Duration.compare()
const isExpired = timeRemaining.sign < 0;
// Pass relativeTo when comparing durations that may include calendar months/years or DST shifts
const isPastHalfway = Temporal.Duration.compare(timeActive, timeRemaining, { relativeTo: now }) > 0;
例: スタンドアロンの期間(round と Intl.DurationFormat)のバランス調整
// Convert a raw unit count (e.g., 90 minutes) into a balanced breakdown (1 hour, 30 minutes)
const rawDuration = Temporal.Duration.from({ minutes: 90 });
const balanced = rawDuration.round({ largestUnit: 'hour' });
// Format via duration.toLocaleString() or directly with Intl.DurationFormat
const formatter = new Intl.DurationFormat('en-US', { style: 'long' });
console.log(formatter.format(balanced)); // "1 hour, 30 minutes"
戦略的な実装とベスト プラクティス
- 推奨: 特定のタイムゾーンで発生する現実世界のイベント(サブスクリプションの更新やイベントのカウントダウンなど)に関連する計算には
Temporal.ZonedDateTimeを使用し、システムのデフォルト以外のゾーンに対して計算する場合は、明示的なtimeZoneをTemporal.Now.zonedDateTimeISO(timeZone)に渡します。 - 推奨:
`${dateStr}T${timeStr}`のような ISO 文字列を補間するのではなく、Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)やTemporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr })などの Temporal メソッドを使用して、別々の日付文字列と時刻文字列を結合します。 - 推奨事項: 1 時間を超える期間をバランスよく表示する場合は、
.since()、.until()、.round()でlargestUnit('day'、'month'、'year'など)を渡します。ZonedDateTimeの差分メソッドはデフォルトで'hour'になります。 - 2 つの期間を比較する場合は
Temporal.Duration.compare(d1, d2, { relativeTo })を使用し、いずれかの期間にカレンダー単位(months、years)またはタイムゾーン対応の日が含まれる場合は、relativeToアンカーを指定します。 ${duration.years}y ${duration.months}m...を手動で補間するのではなく、duration.toLocaleString()またはnew Intl.DurationFormat(locale, { style })('long'、'short'、'narrow'、'digital')を使用して、ユーザー表示用の期間をフォーマットする。- 推奨:
duration.toString()は、機械で読み取り可能な ISO 8601 期間文字列(例:PT1H30M)に使用します。ユーザー向けのコピーには使用しないでください。 PlainTimeをHH:MMとしてレンダリングする(例:<input type="time">の場合)には、文字列.slice(0, 5)ではなく.toPlainTime().toString({ smallestUnit: 'minute' })を使用します。- インスタンスを直接変更しないでください。
Temporalオブジェクトは不変です。.add()、.subtract()、.round()、.with()などのオペレーションは、新しいインスタンスを返します。 - 差分自体に
Date/.getTime()演算を使用しないでください。Dateにはタイムゾーン対応のカレンダー演算がなく、ミリ秒単位の精度しかありません。 - 推奨: 計算された
Temporal.Duration(またはTemporal.ZonedDateTime.compare())のネイティブ.signプロパティを使用して、期間が過去 / 期限切れの時点(sign < 0)を表しているか、将来の保留中の開始を表しているかを確認します。
代替戦略
Temporal のフォールバックとブラウザ サポート
ネイティブの Temporal API をまだサポートしていないブラウザの場合は、機能検出を使用して temporal-polyfill(約 15 KB min+gz)を動的に読み込みます。
temporal-polyfill をインポートしても、Temporal オブジェクトがグローバル スコープに自動的に割り当てられるわけではありません。コードが globalThis.Temporal に依存している場合は、明示的に割り当ててください。
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();