Gebeurtenisverschillen berekenen en duur opmaken met Temporal

Als je de verstreken of resterende tijd tussen gebeurtenissen (zoals het verlopen van een proefperiode, het aftellen van een abonnement of SLA-vensters) wilt berekenen en die perioden wilt presenteren in een gelokaliseerde, door mensen leesbare indeling, moest je in het verleden handmatig rekenen of externe bibliotheken gebruiken. Het verouderde Date-object houdt alleen milliseconden bij, waardoor berekeningen met kalendermaanden, overgangen naar zomertijd en eenheidsbalancering foutgevoelig zijn.

De Temporal API koppelt Temporal.ZonedDateTime aan Temporal.Duration en Intl.DurationFormat om DST-veilige tijdsverschillen, instelbare eenheidsbalans, kalenderbewuste duurvergelijkingen en gelokaliseerde opmaak te bieden.

Implementeren

  1. Eindpunten van Temporal.ZonedDateTime verkrijgen: Gebruik Temporal.Now.zonedDateTimeISO() voor de huidige tijd in de tijdzone van het systeem of geef een expliciete IANA-tijdzonetekenreeks door (Temporal.Now.zonedDateTimeISO(timeZone)) als de tijdzone van de doelgebeurtenis verschilt van de systeemstandaard van de gebruiker.
  2. Verschillen berekenen met .since() en .until():
    • Gebruik now.since(start, options) om de verstreken tijd sinds een eerdere gebeurtenis te berekenen.
    • Gebruik now.until(end, options) om de resterende tijd tot een toekomstige gebeurtenis te berekenen.
    • Geef largestUnit (bijv. 'year', 'month', 'day', 'hour') en smallestUnit (bijv. 'minute') door om de resulterende Temporal.Duration in evenwicht te brengen en af te ronden.
  3. Zelfstandige Temporal.Duration-objecten in evenwicht brengen of vergelijken:
    • Maak zelfstandige duurwaarden met Temporal.Duration.from({ minutes: 90 }) en breng eenheden opnieuw in balans met duration.round({ largestUnit: 'hour' }) (geef relativeTo op als je de balans opmaakt voor kalendermaanden, jaren of overgangen naar zomertijd).
    • Vergelijk 2 perioden met Temporal.Duration.compare(d1, d2, { relativeTo }). Je moet een PlainDate of ZonedDateTime doorgeven als relativeTo als een van de perioden kalendereenheden (years, months, weeks) bevat of de grenzen van de zomertijd overschrijdt.
  4. Indeling voor mensen versus machines:
    • Gebruik duration.toLocaleString(locale, options) (die delegeert naar Intl.DurationFormat) voor gelokaliseerde, op gebruikers gerichte tekst.
    • Gebruik duration.toString() alleen voor machinaal leesbare ISO 8601-duurtekenreeksen (zoals P1M2DT4H).

Voorbeeld: verschil in vervaldatum van proefperiode en gelokaliseerde opmaak

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

Voorbeeld: Zelfstandige duur in evenwicht brengen (round en 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"

Strategische implementatie en best practices

  • Gebruik WEL Temporal.ZonedDateTime voor berekeningen met gebeurtenissen in de echte wereld die plaatsvinden in specifieke tijdzones (zoals abonnementsverlengingen of aftellen naar evenementen) en geef een expliciete timeZone door aan Temporal.Now.zonedDateTimeISO(timeZone) als je een berekening uitvoert voor een andere zone dan de systeemstandaard.
  • WEL: Combineer afzonderlijke datum- en tijdtekenreeksen met Temporal-methoden, zoals Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz) of Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), in plaats van een ISO-tekenreeks zoals `${dateStr}T${timeStr}` te interpoleren.
  • Geef largestUnit (bijv. 'day', 'month', 'year') door in .since(), .until() of .round() als je de duur wilt balanceren buiten uren. De standaardwaarde voor ZonedDateTime-verschilmethoden is 'hour'.
  • Gebruik WEL Temporal.Duration.compare(d1, d2, { relativeTo }) als je 2 perioden vergelijkt en geef een relativeTo-anker op als een van de perioden kalendereenheden (months, years) of tijdzonebewuste dagen bevat.
  • WEL: Maak de duur op voor weergave aan de gebruiker met duration.toLocaleString() of new Intl.DurationFormat(locale, { style }) ('long', 'short', 'narrow' of 'digital') in plaats van ${duration.years}y ${duration.months}m... handmatig te interpoleren.
  • Gebruik WEL duration.toString() voor machinaal leesbare ISO 8601-duurtekenreeksen (bijv. PT1H30M), nooit als op gebruikers gerichte tekst.
  • WEL: Render een PlainTime als HH:MM (bijvoorbeeld voor <input type="time">) met .toPlainTime().toString({ smallestUnit: 'minute' }) in plaats van de tekenreeks .slice(0, 5).
  • Wijzig instanties NIET rechtstreeks. Temporal-objecten zijn onveranderlijk. Bewerkingen zoals .add(), .subtract(), .round() of .with() retourneren een nieuwe instantie.
  • Gebruik GEEN Date / .getTime()-rekenkunde voor het verschil zelf. Date heeft geen tijdzonebewuste agendawiskunde en alleen millisecondeprecisie.
  • Gebruik WEL de native property .sign van de berekende Temporal.Duration (of Temporal.ZonedDateTime.compare()) om te controleren of een duur een tijdstip in het verleden/verlopen tijdstip (sign < 0) of een toekomstige start in behandeling vertegenwoordigt.

Reservestrategie

Fallbacks en browserondersteuning voor Temporal

Voor browsers die de native Temporal API nog niet ondersteunen, gebruik je functiedetectie en laad je temporal-polyfill dynamisch (~15 KB min+gz).

Houd er rekening mee dat als je temporal-polyfill importeert, het Temporal-object niet automatisch wordt toegewezen aan het globale bereik. Wijs de waarde expliciet toe als je code afhankelijk is van 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