Ereignisdifferenzen berechnen und Dauern mit Temporal formatieren

Webfunktionen: Temporal Intl.DurationFormat

Die Berechnung der verstrichenen oder verbleibenden Zeit zwischen Ereignissen (z. B. Ablauf von Testzeiträumen, Countdowns für Abos oder SLA-Fenster) und die Darstellung dieser Zeiträume in einem lokalisierten, menschenlesbaren Format erforderte bisher manuelle Berechnungen oder externe Bibliotheken. Das alte Date-Objekt erfasst nur Millisekunden seit der Epoche, was die Berechnung von Kalendermonaten, die Umstellung auf Sommerzeit und die Einheitenabstimmung fehleranfällig macht.

Die Temporal API kombiniert Temporal.ZonedDateTime mit Temporal.Duration und Intl.DurationFormat, um Zeitdifferenzen zu liefern, die die Sommerzeit berücksichtigen, konfigurierbare Einheitenverteilung, kalenderbasierte Dauervergleiche und lokalisierte Formatierung.

Implementierung

  1. Temporal.ZonedDateTime-Endpunkte abrufen: Verwenden Sie Temporal.Now.zonedDateTimeISO() für die aktuelle Zeit in der Systemzeitzone oder übergeben Sie einen expliziten IANA-Zeitzonenstring (Temporal.Now.zonedDateTimeISO(timeZone)), wenn die Zeitzone des Zielereignisses von der System-Standardeinstellung des Nutzers abweicht.
  2. Differenziale mit .since() und .until() berechnen:
    • Mit now.since(start, options) können Sie die Zeit berechnen, die seit einem vergangenen Ereignis verstrichen ist.
    • Verwenden Sie now.until(end, options), um die verbleibende Zeit bis zu einem zukünftigen Ereignis zu berechnen.
    • Übergeben Sie largestUnit (z.B. 'year', 'month', 'day', 'hour') und smallestUnit (z.B. 'minute'), um die resultierende Temporal.Duration auszugleichen und zu runden.
  3. Eigenständige Temporal.Duration-Objekte ausgleichen oder vergleichen:
    • Erstellen Sie eigenständige Dauern mit Temporal.Duration.from({ minutes: 90 }) und gleichen Sie Einheiten mit duration.round({ largestUnit: 'hour' }) aus. Geben Sie dabei relativeTo an, wenn Sie über Kalendermonate, Jahre oder Sommerzeitübergänge hinweg ausgleichen.
    • Vergleichen Sie zwei Dauern mit Temporal.Duration.compare(d1, d2, { relativeTo }). Wenn eine der Dauern Kalendereinheiten (years, months, weeks) enthält oder die Sommerzeitgrenzen überschreitet, muss ein PlainDate oder ZonedDateTime als relativeTo übergeben werden.
  4. Format für Menschen im Vergleich zu Maschinen:
    • Verwenden Sie duration.toLocaleString(locale, options) (das an Intl.DurationFormat delegiert) für lokalisierten, für Nutzer sichtbaren Text.
    • Verwenden Sie duration.toString() nur für maschinenlesbare ISO 8601-Dauerstrings (z. B. P1M2DT4H).

Beispiel: Unterschiedliche Ablaufdaten für Testzeiträume und lokalisierte Formatierung

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

Beispiel: Eigenständige Dauern ausgleichen (round und 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 Implementierung und Best Practices

  • DO use Temporal.ZonedDateTime for calculations involving real-world events that occur in specific time zones (like subscription renewals or event countdowns), and pass an explicit timeZone to Temporal.Now.zonedDateTimeISO(timeZone) when calculating against a zone other than the system default.
  • DO combine separate date and time strings with Temporal methods, e.g. Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz) or Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), instead of interpolating an ISO string like `${dateStr}T${timeStr}`.
  • ÜBERGEBEN Sie largestUnit (z. B. 'day', 'month', 'year') in .since(), .until() oder .round(), wenn Sie Dauern über Stunden hinaus ausgleichen möchten. ZonedDateTime-Differenzmethoden sind standardmäßig auf 'hour' eingestellt.
  • VERWENDEN Sie Temporal.Duration.compare(d1, d2, { relativeTo }), wenn Sie zwei Zeiträume vergleichen, und geben Sie einen relativeTo-Anker an, wenn einer der Zeiträume Kalendereinheiten (months, years) oder zeitzonenabhängige Tage enthält.
  • Formatiere Zeiträume für die Anzeige für Nutzer mit duration.toLocaleString() oder new Intl.DurationFormat(locale, { style }) ('long', 'short', 'narrow' oder 'digital'), anstatt ${duration.years}y ${duration.months}m... manuell zu interpolieren.
  • DO use duration.toString() for machine-readable ISO 8601 duration strings (e.g., PT1H30M), never as user-facing copy.
  • DO render a PlainTime as HH:MM (e.g., for <input type="time">) using .toPlainTime().toString({ smallestUnit: 'minute' }) rather than string .slice(0, 5).
  • Ändern Sie Instanzen NICHT direkt. Temporal-Objekte sind unveränderlich. Vorgänge wie .add(), .subtract(), .round() oder .with() geben eine neue Instanz zurück.
  • Verwenden Sie für die Differenz NICHT die Arithmetik von Date / .getTime(). Date bietet keine zeitzonenabhängige Kalenderarithmetik und nur eine Genauigkeit im Millisekundenbereich.
  • DO use the native .sign property of the computed Temporal.Duration (or Temporal.ZonedDateTime.compare()) to check if a duration represents a past/expired time point (sign < 0) or a future pending start.

Fallback-Strategie

Fallbacks und Browserunterstützung für Temporal

Für Browser, die die native Temporal API noch nicht unterstützen, können Sie die Funktionserkennung verwenden und temporal-polyfill dynamisch laden (~15 KB min+gz).

Beachten Sie, dass das Importieren von temporal-polyfill das Temporal-Objekt nicht automatisch dem globalen Bereich zuweist. Weisen Sie ihn explizit zu, wenn Ihr Code auf globalThis.Temporal basiert.

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