Раньше, чтобы рассчитать время, прошедшее или оставшееся между событиями (например, окончание пробного периода, обратный отсчет до окончания подписки или окна SLA), и представить эти промежутки в локализованном, человекочитаемом формате, требовались ручные вычисления или внешние библиотеки. Устаревший объект Date отслеживает только миллисекунды эпохи, что делает арифметику календарных месяцев, переходы на летнее время и балансировку единиц подверженными ошибкам.
API Temporal работает в паре с Temporal.ZonedDateTime, Temporal.Duration и Intl.DurationFormat, чтобы обеспечивать безопасную разницу во времени с учетом перехода на летнее время, настраиваемый баланс единиц, сравнение продолжительности с учетом календаря и локализованное форматирование.
Как реализовать
- Получение конечных точек
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 }). ПередачаPlainDateилиZonedDateTimeв качествеrelativeToтребуется, когда любая продолжительность содержит календарные единицы (years,months,weeks) или пересекает границы летнего времени.
- Создавайте отдельные продолжительности с помощью
- Формат для людей и машин
- Используйте
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 }), вместо интерполяции строки ISO, например`${dateStr}T${timeStr}`. - РЕКОМЕНДУЕТСЯ передавать
largestUnit(например,'day','month','year') в.since(),.until()или.round(), если вы хотите сбалансировать продолжительность за пределами часов. По умолчанию методы разницыZonedDateTimeиспользуют'hour'. - ИСПОЛЬЗУЙТЕ
Temporal.Duration.compare(d1, d2, { relativeTo })при сравнении двух периодов времени, указывая опорную датуrelativeTo, если в одном из периодов используются календарные единицы (months,years) или дни с учетом часового пояса. - РЕКОМЕНДУЕТСЯ форматировать длительность для показа пользователям с помощью
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нет календарной арифметики с учетом часовых поясов, а точность ограничена миллисекундами. - РЕКОМЕНДУЕТСЯ использовать нативное свойство
.signвычисленного объектаTemporal.Duration(илиTemporal.ZonedDateTime.compare()), чтобы проверить, представляет ли продолжительность прошедший/истекший момент времени (sign < 0) или предстоящее начало в будущем.
Резервная стратегия
Резервные варианты и поддержка Temporal в браузерах
Для браузеров, которые пока не поддерживают нативный API Temporal, используйте обнаружение функций и динамически загружайте temporal-polyfill (около 15 КБ 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();
})();