Calcular el tiempo transcurrido o restante entre eventos (como vencimientos de pruebas, cuentas regresivas de suscripciones o ventanas de ANS) y presentar esos períodos en un formato localizado y legible por humanos históricamente requería cálculos manuales o bibliotecas externas. El objeto Date heredado solo hace un seguimiento de los milisegundos de la época, lo que hace que la aritmética del mes calendario, las transiciones del horario de verano (DST) y el balanceo de unidades sean propensos a errores.
La API de Temporal vincula Temporal.ZonedDateTime con Temporal.Duration y Intl.DurationFormat para proporcionar diferencias de tiempo seguras para el horario de verano, balanceo de unidades configurable, comparaciones de duración que tienen en cuenta el calendario y formato localizado.
Cómo implementarla
- Obtén extremos de
Temporal.ZonedDateTime: UsaTemporal.Now.zonedDateTimeISO()para la hora actual en la zona horaria del sistema o pasa una cadena de zona horaria de IANA explícita (Temporal.Now.zonedDateTimeISO(timeZone)) cuando la zona horaria del evento de destino difiera de la predeterminada del sistema del usuario. - Calcula los diferenciales con
.since()y.until():- Usa
now.since(start, options)para calcular el tiempo transcurrido desde un evento pasado. - Usa
now.until(end, options)para calcular el tiempo restante hasta un evento futuro. - Pasa
largestUnit(p.ej.,'year','month','day','hour') ysmallestUnit(p.ej.,'minute') para equilibrar y redondear elTemporal.Durationresultante.
- Usa
- Equilibra o compara objetos
Temporal.Durationindependientes:- Crea duraciones independientes con
Temporal.Duration.from({ minutes: 90 })y reequilibra las unidades conduration.round({ largestUnit: 'hour' })(proporcionandorelativeTocuando se equilibren meses, años o transiciones de horario de verano). - Compara dos duraciones con
Temporal.Duration.compare(d1, d2, { relativeTo }). Se requiere pasar unPlainDateoZonedDateTimecomorelativeTosiempre que cualquiera de las duraciones contenga unidades de calendario (years,months,weeks) o cruce los límites del horario de verano.
- Crea duraciones independientes con
- Formato para humanos vs. máquinas:
- Usa
duration.toLocaleString(locale, options)(que delega enIntl.DurationFormat) para el texto localizado para el usuario. - Usa
duration.toString()solo para las cadenas de duración ISO 8601 legibles por máquinas (comoP1M2DT4H).
- Usa
Ejemplo: Diferencia de vencimiento de la prueba y formato localizado
// 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;
Ejemplo: Balanceo de duraciones independientes (round y 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"
Implementación estratégica y prácticas recomendadas
- SÍ usa
Temporal.ZonedDateTimepara cálculos que involucren eventos del mundo real que ocurren en zonas horarias específicas (como renovaciones de suscripciones o cuentas regresivas de eventos) y pasa untimeZoneexplícito aTemporal.Now.zonedDateTimeISO(timeZone)cuando calcules en una zona que no sea la predeterminada del sistema. - SÍ combina cadenas de fecha y hora separadas con métodos de Temporal, p.ej.,
Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)oTemporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), en lugar de interpolar una cadena ISO como`${dateStr}T${timeStr}`. - SÍ pasa
largestUnit(p.ej.,'day','month','year') en.since(),.until()o.round()cuando quieras que las duraciones se equilibren más allá de las horas; los métodos de diferencia deZonedDateTimese establecen de forma predeterminada en'hour'. - SÍ usa
Temporal.Duration.compare(d1, d2, { relativeTo })cuando compares dos duraciones, proporcionando un anclajerelativeTosiempre que cualquiera de las duraciones involucre unidades de calendario (months,years) o días que tengan en cuenta la zona horaria. - SÍ debes dar formato a las duraciones para la visualización del usuario con
duration.toLocaleString()onew Intl.DurationFormat(locale, { style })('long','short','narrow'o'digital') en lugar de interpolar${duration.years}y ${duration.months}m...de forma manual. - SÍ usa
duration.toString()para las cadenas de duración ISO 8601 legibles por máquinas (p.ej.,PT1H30M), nunca como texto para el usuario. - SÍ renderices un
PlainTimecomoHH:MM(p.ej., para<input type="time">) con.toPlainTime().toString({ smallestUnit: 'minute' })en lugar de la cadena.slice(0, 5). - NO modifiques las instancias directamente; los objetos
Temporalson inmutables. Las operaciones como.add(),.subtract(),.round()o.with()devuelven una instancia nueva. - NO recurras a la aritmética de
Date/.getTime()para el diferencial en sí;Dateno tiene operaciones matemáticas de calendario que tengan en cuenta la zona horaria y solo tiene precisión de milisegundos. - SÍ uses la propiedad nativa
.signdelTemporal.Duration(oTemporal.ZonedDateTime.compare()) calculado para verificar si una duración representa un momento pasado o vencido (sign < 0) o un inicio pendiente futuro.
Estrategia de resguardo
Resguardos y compatibilidad de navegadores para Temporal
Para los navegadores que aún no admiten la API de Temporal nativa, usa la detección de funciones y carga temporal-polyfill de forma dinámica (~15 KB min+gz).
Ten en cuenta que importar temporal-polyfill no asigna automáticamente el objeto Temporal al alcance global. Asígnalo de forma explícita si tu código depende 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();
})();