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
- Receber endpoints
Temporal.ZonedDateTime: useTemporal.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. - 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') esmallestUnit(por exemplo,'minute') para equilibrar e arredondar oTemporal.Durationresultante.
- Use
- Equilibrar ou comparar objetos
Temporal.Durationindependentes:- Crie durações independentes com
Temporal.Duration.from({ minutes: 90 })e reequilibre as unidades comduration.round({ largestUnit: 'hour' })(fornecendorelativeToao equilibrar meses, anos ou transições de horário de verão). - Compare duas durações com
Temporal.Duration.compare(d1, d2, { relativeTo }). Transmitir umPlainDateouZonedDateTimecomorelativeToé 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.
- Crie durações independentes com
- Formato para humanos x máquinas:
- Use
duration.toLocaleString(locale, options)(que delega paraIntl.DurationFormat) para textos localizados voltados ao usuário. - Use
duration.toString()apenas para strings de duração ISO 8601 legíveis por máquina (comoP1M2DT4H).
- Use
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.ZonedDateTimepara 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 umtimeZoneexplícito paraTemporal.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)ouTemporal.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çaZonedDateTimeusam'hour'como padrão. - USE
Temporal.Duration.compare(d1, d2, { relativeTo })ao comparar duas durações, fornecendo uma âncorarelativeTosempre 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()ounew 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
PlainTimecomoHH: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
Temporalsã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. ODatenão tem matemática de calendário com reconhecimento de fuso horário e só tem precisão de milissegundos. - USE a propriedade nativa
.signdoTemporal.Duration(ouTemporal.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();
})();