Calcolo dei differenziali degli eventi e formattazione delle durate con Temporal

Funzionalità web: Temporal Intl.DurationFormat

Il calcolo del tempo trascorso o rimanente tra gli eventi (come le scadenze delle prove, i countdown degli abbonamenti o le finestre degli SLA) e la presentazione di questi intervalli in un formato localizzato e leggibile dall'uomo ha storicamente richiesto calcoli manuali o librerie esterne. L'oggetto legacy Date monitora solo i millisecondi dell'epoca, rendendo l'aritmetica dei mesi di calendario, le transizioni dell'ora legale e il bilanciamento delle unità soggetti a errori.

L'API Temporal associa Temporal.ZonedDateTime a Temporal.Duration e Intl.DurationFormat per fornire differenze di fuso orario sicure per l'ora legale, bilanciamento delle unità configurabile, confronti di durata basati sul calendario e formattazione localizzata.

Modalità di implementazione

  1. Ottieni gli endpoint Temporal.ZonedDateTime: utilizza Temporal.Now.zonedDateTimeISO() per l'ora corrente nel fuso orario di sistema o passa una stringa esplicita del fuso orario IANA (Temporal.Now.zonedDateTimeISO(timeZone)) quando il fuso orario dell'evento target è diverso da quello predefinito del sistema dell'utente.
  2. Calcola i differenziali con .since() e .until():
    • Utilizza now.since(start, options) per calcolare il tempo trascorso da un evento passato.
    • Utilizza now.until(end, options) per calcolare il tempo rimanente fino a un evento futuro.
    • Passa largestUnit (ad es. 'year', 'month', 'day', 'hour') e smallestUnit (ad es. 'minute') per bilanciare e arrotondare il Temporal.Duration risultante.
  3. Bilancia o confronta gli oggetti Temporal.Duration autonomi:
    • Crea durate autonome con Temporal.Duration.from({ minutes: 90 }) e riequilibra le unità con duration.round({ largestUnit: 'hour' }) (fornendo relativeTo quando esegui il bilanciamento tra mesi di calendario, anni o transizioni dell'ora legale).
    • Confronta due durate con Temporal.Duration.compare(d1, d2, { relativeTo }): è necessario passare un PlainDate o un ZonedDateTime come relativeTo ogni volta che una delle due durate contiene unità di calendario (years, months, weeks) o supera i limiti dell'ora legale.
  4. Formato per persone e macchine:
    • Utilizza duration.toLocaleString(locale, options) (che delega a Intl.DurationFormat) per il testo localizzato rivolto agli utenti.
    • Utilizza duration.toString() solo per le stringhe di durata ISO 8601 leggibili da macchina (ad esempio P1M2DT4H).

Esempio: differenza di scadenza della prova e formattazione localizzata

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

Esempio: bilanciamento delle durate autonome (round e 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"

Implementazione strategica e best practice

  • SÌ: utilizza Temporal.ZonedDateTime per i calcoli che coinvolgono eventi reali che si verificano in fusi orari specifici (come i rinnovi degli abbonamenti o i countdown degli eventi) e passa un timeZone esplicito a Temporal.Now.zonedDateTimeISO(timeZone) quando esegui calcoli in un fuso orario diverso da quello predefinito di sistema.
  • SÌ: combina stringhe di data e ora separate con metodi temporali, ad esempio Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz) o Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), anziché interpolare una stringa ISO come `${dateStr}T${timeStr}`.
  • CONSIGLIATO: passa largestUnit (ad es. 'day', 'month', 'year') in .since(), .until() o .round() quando vuoi bilanciare le durate oltre le ore; i metodi di differenza ZonedDateTime sono predefiniti su 'hour'.
  • UTILIZZA Temporal.Duration.compare(d1, d2, { relativeTo }) quando confronti due durate, fornendo un'ancora relativeTo ogni volta che una delle due durate coinvolge unità di calendario (months, years) o giorni che tengono conto del fuso orario.
  • CONSIGLIATO: formatta le durate per la visualizzazione dell'utente utilizzando duration.toLocaleString() o new Intl.DurationFormat(locale, { style }) ('long', 'short', 'narrow' o 'digital') anziché interpolare manualmente ${duration.years}y ${duration.months}m....
  • SÌ: utilizza duration.toString() per le stringhe di durata ISO 8601 leggibili da macchina (ad es. PT1H30M), mai come testo rivolto agli utenti.
  • PUOI eseguire il rendering di un PlainTime come HH:MM (ad es. per <input type="time">) utilizzando .toPlainTime().toString({ smallestUnit: 'minute' }) anziché la stringa .slice(0, 5).
  • NON modificare direttamente le istanze; gli oggetti Temporal sono immutabili. Operazioni come .add(), .subtract(), .round() o .with() restituiscono una nuova istanza.
  • NON eseguire il fallback all'aritmetica Date/.getTime() per il differenziale stesso; Date non dispone di calcoli del calendario sensibili al fuso orario e ha una precisione solo in millisecondi.
  • SÌ: utilizza la proprietà nativa .sign del valore Temporal.Duration (o Temporal.ZonedDateTime.compare()) calcolato per verificare se una durata rappresenta un momento passato/scaduto (sign < 0) o un inizio futuro in attesa.

Strategia di riserva

Fallback e supporto del browser per Temporal

Per i browser che non supportano ancora l'API Temporal nativa, utilizza il rilevamento delle funzionalità e carica dinamicamente temporal-polyfill (~15 kB min+gz).

Tieni presente che l'importazione di temporal-polyfill non assegna automaticamente l'oggetto Temporal all'ambito globale. Assegnalo esplicitamente se il tuo codice si basa su 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