過去,如要計算事件 (例如試用期到期、訂閱倒數或服務水準協議時間範圍) 之間經過或剩餘的時間,並以本地化、人類可讀的格式呈現這些時間範圍,必須手動計算或使用外部程式庫。舊版 Date 物件只會追蹤 Epoch 毫秒,因此日曆月份算術、日光節約時間 (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 })比較兩個時間長度,如果任一時間長度包含日曆單位 (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)。 - 建議做法:使用 Temporal 方法 (例如
Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)或Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr })) 結合不同的日期和時間字串,而非插入`${dateStr}T${timeStr}`等 ISO 字串。 - 務必在
.since()、.until()或.round()中傳遞largestUnit(例如'day'、'month'、'year'),以便平衡超過小時的時間長度;ZonedDateTime差異方法預設為'hour'。 - 務必使用
Temporal.Duration.compare(d1, d2, { relativeTo })比較兩個時間長度,並在任一時間長度涉及日曆單位 (months、years) 或時區感知天數時,提供relativeTo錨定。 - 務必使用
duration.toLocaleString()或new Intl.DurationFormat(locale, { style })('long'、'short'、'narrow'或'digital') 格式化時間長度,向使用者顯示,而非手動插入${duration.years}y ${duration.months}m...。 - 務必使用
duration.toString()呈現機器可讀的 ISO 8601 時間長度字串 (例如PT1H30M),切勿用於面向使用者的文案。 - 務必使用
.toPlainTime().toString({ smallestUnit: 'minute' })而非字串.slice(0, 5),將PlainTime算繪為HH:MM(例如<input type="time">)。 - 請勿直接修改例項;
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();
})();