Como calcular diferenças de eventos e formatar durações com o Temporal

Recursos da Web: Temporal Intl.DurationFormat

Calcular o tempo decorrido ou restante entre eventos (como expiração de testes, contagens regressivas de assinaturas ou janelas de SLA) e apresentar esses períodos em um formato localizado e legível por humanos sempre exigiu cálculos manuais ou bibliotecas externas. O objeto Date legado rastreia apenas milissegundos de época, o que torna a aritmética de meses do calendário, as transições de horário de verão e o balanceamento de unidades propensos a erros.

A API Temporal combina Temporal.ZonedDateTime com Temporal.Duration e Intl.DurationFormat para fornecer diferenças de tempo seguras para o horário de verão, balanceamento de unidade configurável, comparações de duração que consideram o calendário e formatação localizada.

Como implementar

  1. Receber endpoints Temporal.ZonedDateTime: use Temporal.Now.zonedDateTimeISO() para o horário atual no fuso horário do sistema ou transmita uma string de fuso horário da IANA explícita (Temporal.Now.zonedDateTimeISO(timeZone)) quando o fuso horário do evento de destino for diferente do padrão do sistema do usuário.
  2. Calcule diferenciais com .since() e .until():
    • Use now.since(start, options) para calcular o tempo decorrido desde um evento anterior.
    • Use now.until(end, options) para calcular o tempo restante até um evento futuro.
    • Transmita largestUnit (por exemplo, 'year', 'month', 'day', 'hour') e smallestUnit (por exemplo, 'minute') para equilibrar e arredondar o Temporal.Duration resultante.
  3. Equilibrar ou comparar objetos Temporal.Duration independentes:
    • Crie durações independentes com Temporal.Duration.from({ minutes: 90 }) e reequilibre as unidades com duration.round({ largestUnit: 'hour' }) (fornecendo relativeTo ao equilibrar meses, anos ou transições de horário de verão).
    • Compare duas durações com Temporal.Duration.compare(d1, d2, { relativeTo }). Transmitir um PlainDate ou ZonedDateTime como relativeTo é obrigatório sempre que uma das durações contém unidades de calendário (years, months, weeks) ou cruza os limites do horário de verão.
  4. Formato para humanos x máquinas:
    • Use duration.toLocaleString(locale, options) (que delega para Intl.DurationFormat) para textos localizados voltados ao usuário.
    • Use duration.toString() apenas para strings de duração ISO 8601 legíveis por máquina (como P1M2DT4H).

Exemplo: diferença de expiração do teste e formatação localizada

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

Exemplo: como equilibrar durações independentes (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"

Implementação estratégica e práticas recomendadas

  • RECOMENDAMOS usar Temporal.ZonedDateTime para cálculos que envolvem eventos do mundo real que ocorrem em fusos horários específicos (como renovações de assinaturas ou contagens regressivas de eventos) e transmitir um timeZone explícito para Temporal.Now.zonedDateTimeISO(timeZone) ao calcular em relação a um fuso horário diferente do padrão do sistema.
  • RECOMENDADO: combine strings de data e hora separadas com métodos temporais, como Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz) ou Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), em vez de interpolar uma string ISO como `${dateStr}T${timeStr}`.
  • FAÇA a transmissão de largestUnit (por exemplo, 'day', 'month', 'year') em .since(), .until() ou .round() quando quiser que as durações sejam equilibradas além das horas. Os métodos de diferença ZonedDateTime usam 'hour' como padrão.
  • USE Temporal.Duration.compare(d1, d2, { relativeTo }) ao comparar duas durações, fornecendo uma âncora relativeTo sempre que uma das durações envolver unidades de calendário (months, years) ou dias que consideram o fuso horário.
  • RECOMENDAMOS formatar as durações para exibição ao usuário usando duration.toLocaleString() ou new Intl.DurationFormat(locale, { style }) ('long', 'short', 'narrow' ou 'digital') em vez de interpolar manualmente ${duration.years}y ${duration.months}m....
  • RECOMENDADO: use duration.toString() para strings de duração ISO 8601 legíveis por máquina (por exemplo, PT1H30M), nunca como texto voltado ao usuário.
  • RECOMENDADO: renderize um PlainTime como HH:MM (por exemplo, para <input type="time">) usando .toPlainTime().toString({ smallestUnit: 'minute' }) em vez da string .slice(0, 5).
  • NÃO modifique as instâncias diretamente. Os objetos Temporal são imutáveis. Operações como .add(), .subtract(), .round() ou .with() retornam uma nova instância.
  • NÃO use a aritmética de Date / .getTime() para o diferencial em si. O Date não tem matemática de calendário com reconhecimento de fuso horário e só tem precisão de milissegundos.
  • USE a propriedade nativa .sign do Temporal.Duration (ou Temporal.ZonedDateTime.compare()) calculado para verificar se uma duração representa um momento passado/expirado (sign < 0) ou um início pendente no futuro.

Estratégia de fallback

Substitutos e suporte do navegador para Temporal

Para navegadores que ainda não são compatíveis com a API Temporal nativa, use a detecção de recursos e carregue dinamicamente temporal-polyfill (~15 KB min+gz).

A importação de temporal-polyfill não atribui automaticamente o objeto Temporal ao escopo global. Atribua-o explicitamente se o código depender de 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