Obliczanie różnic między zdarzeniami i formatowanie czasu trwania za pomocą interfejsu Temporal

Funkcje internetowe: Temporal Intl.DurationFormat

Obliczanie czasu, który upłynął lub pozostał między zdarzeniami (takimi jak wygaśnięcie okresu próbnego, odliczanie do zakończenia subskrypcji lub okna gwarancji jakości usług), i prezentowanie tych przedziałów w zlokalizowanym, zrozumiałym dla człowieka formacie wymagało dotychczas ręcznych obliczeń lub zewnętrznych bibliotek. Starszy obiekt Date śledzi tylko milisekundy od początku epoki, co sprawia, że obliczenia dotyczące miesięcy kalendarzowych, przejścia na czas letni i równoważenie jednostek są podatne na błędy.

Interfejs API Temporal łączy Temporal.ZonedDateTime z Temporal.Duration i Intl.DurationFormat, aby zapewnić różnice czasowe uwzględniające czas letni, konfigurowalne równoważenie jednostek, porównania czasu trwania uwzględniające kalendarz i zlokalizowane formatowanie.

Jak to zrobić?

  1. Uzyskiwanie punktów końcowych Temporal.ZonedDateTime: użyj Temporal.Now.zonedDateTimeISO() dla bieżącego czasu w strefie czasowej systemu lub przekaż jawny ciąg znaków strefy czasowej IANA (Temporal.Now.zonedDateTimeISO(timeZone)), gdy strefa czasowa docelowego wydarzenia różni się od ustawienia domyślnego systemu użytkownika.
  2. Obliczanie różnic za pomocą .since() i .until():
    • Użyj funkcji now.since(start, options), aby obliczyć czas, który upłynął od zdarzenia w przeszłości.
    • Użyj now.until(end, options), aby obliczyć czas pozostały do przyszłego wydarzenia.
    • Przekaż largestUnit (np. 'year', 'month', 'day', 'hour') i smallestUnit (np. 'minute'), aby zrównoważyć i zaokrąglić wynikowy Temporal.Duration.
  3. Zrównoważ lub porównaj samodzielne obiekty Temporal.Duration:
    • Utwórz samodzielne okresy za pomocą Temporal.Duration.from({ minutes: 90 }) i ponownie zbilansuj jednostki za pomocą duration.round({ largestUnit: 'hour' }) (podając relativeTo podczas bilansowania w miesiącach kalendarzowych, latach lub przejściach na czas letni).
    • Porównaj 2 okresy za pomocą Temporal.Duration.compare(d1, d2, { relativeTo }) – przekazanie PlainDate lub ZonedDateTime jako relativeTo jest wymagane, gdy którykolwiek z okresów zawiera jednostki kalendarzowe (years, months, weeks) lub przekracza granice czasu letniego.
  4. Formatowanie dla ludzi i maszyn:
    • Używaj duration.toLocaleString(locale, options) (który przekazuje uprawnienia do Intl.DurationFormat) w przypadku zlokalizowanego tekstu dla użytkownika.
    • Używaj duration.toString() tylko w przypadku ciągów znaków czasu trwania w formacie ISO 8601 czytelnym dla komputerów (np. P1M2DT4H).

Przykład: różnica w dacie zakończenia okresu próbnego i formatowanie zlokalizowane

// 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;

Przykład: równoważenie czasu trwania samodzielnych reklam (round i 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"

Strategiczne wdrażanie i sprawdzone metody

  • UŻYWAJ Temporal.ZonedDateTime do obliczeń związanych z wydarzeniami w świecie rzeczywistym, które występują w określonych strefach czasowych (np. odnowienia subskrypcji lub odliczanie do wydarzenia), i przekazuj jawny parametr timeZone do Temporal.Now.zonedDateTimeISO(timeZone) podczas obliczeń dotyczących strefy innej niż ustawienie domyślne systemu.
  • ZALECENIE: łącz osobne ciągi znaków daty i godziny za pomocą metod Temporal, np. Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz) lub Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), zamiast interpolować ciąg znaków ISO, np. `${dateStr}T${timeStr}`.
  • PRZEKAZUJ largestUnit (np. 'day', 'month', 'year') w .since(), .until() lub .round(), gdy chcesz zrównoważyć czas trwania poza godzinami; metody różnicowe ZonedDateTime domyślnie ustawiają 'hour'.
  • UŻYWAJ funkcji Temporal.Duration.compare(d1, d2, { relativeTo }) podczas porównywania 2 czasów trwania, podając punkt odniesienia relativeTo, gdy którykolwiek z czasów trwania obejmuje jednostki kalendarzowe (months, years) lub dni uwzględniające strefę czasową.
  • DO format durations for user display using duration.toLocaleString() or new Intl.DurationFormat(locale, { style }) ('long', 'short', 'narrow', or 'digital') instead of manually interpolating ${duration.years}y ${duration.months}m....
  • UŻYWAJ duration.toString() w przypadku ciągów znaków czasu trwania w formacie ISO 8601 czytelnym dla komputerów (np. PT1H30M), nigdy w tekście dla użytkownika.
  • TAK – renderuj PlainTime jako HH:MM (np. w przypadku <input type="time">) za pomocą .toPlainTime().toString({ smallestUnit: 'minute' }), a nie ciągu znaków .slice(0, 5).
  • NIE modyfikuj instancji bezpośrednio. Obiekty Temporal są niezmienne. Operacje takie jak .add(), .subtract(), .round() lub .with() zwracają nową instancję.
  • NIE używaj arytmetyki Date / .getTime() do obliczania różnicy. Date nie ma arytmetyki kalendarza uwzględniającej strefę czasową i zapewnia dokładność tylko do milisekund.
  • UŻYWAJ właściwości natywnej .sign obliczonego Temporal.Duration (lub Temporal.ZonedDateTime.compare()), aby sprawdzić, czy czas trwania reprezentuje przeszły/wygasły punkt czasowy (sign < 0) czy przyszłe oczekujące rozpoczęcie.

Strategia zastępcza

Zastępcze rozwiązania i obsługa Temporal w przeglądarkach

W przypadku przeglądarek, które nie obsługują jeszcze natywnego interfejsu API Temporal, użyj wykrywania funkcji i dynamicznie wczytaj temporal-polyfill (~15 KB min+gz).

Pamiętaj, że zaimportowanie temporal-polyfill nie powoduje automatycznego przypisania obiektu Temporal do zakresu globalnego. Przypisz go bezpośrednio, jeśli Twój kod opiera się na 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();
})();

Intl.DurationFormat