Standortunabhängige Daten und unvollständige Datumsangaben mit Temporal erfassen

Webfunktionen: Temporal

Das Aufzeichnen chronologischer Daten, die unabhängig vom Standort des Betrachters identisch bleiben sollen, z. B. Geburtsdaten, tägliche Alarme, Ablaufdaten von Kreditkarten oder monatliche Abrechnungszeiträume, war mit dem alten Date-Objekt in der Vergangenheit fehleranfällig. Da ein Date immer einen bestimmten Zeitpunkt darstellt und in der lokalen Zeitzone des Betrachters angezeigt wird, kann das Speichern eines Datums wie "1990-01-01" dazu führen, dass Nutzer westlich der UTC "1989-12-31" sehen. Beim Modellieren von Teil-Datumsangaben (z. B. nur der Monat des Ablaufdatums einer Karte) werden willkürliche Platzhaltertage erzwungen, die zu Überlauffehlern führen.

Die Temporal API bietet eine Reihe von standortunabhängigen „Plain“-Typen, die keinen Zeitzonenversatz haben und Kalenderdaten und Uhrzeiten genau so darstellen, wie sie von einem Kalender oder einer Uhr abgelesen werden:

Typ Was wird modelliert? Typische Anwendungsfälle
Temporal.PlainDate Vollständiges Kalenderdatum (year, month, day) Geburtsdaten, Termine, Abrechnungsdaten für Abos
Temporal.PlainTime Echtzeit (hour, minute, second, ...) Tägliche Wecker (9:00 Uhr am Standort des Nutzers), Öffnungszeiten
Temporal.PlainDateTime Kalenderdatum + Uhrzeit ohne Zeitzone Floating-Zeitpläne für die lokale Zeitzone, die einen PlainDate und einen PlainTime kombinieren, bevor eine Zeitzone angehängt wird
Temporal.PlainMonthDay Monat und Tag ohne Jahr Wiederkehrende jährliche Geburtstage, Jahrestage, feste Feiertage
Temporal.PlainYearMonth Jahr und Monat ohne Tag Ablaufdatum von Kreditkarten (2027-12), monatliche Abrechnungszeiträume

Implementierung

  1. Wählen Sie den spezifischsten Temporal.Plain*-Typ aus, der zu Ihren Daten passt:
    • Verwenden Sie Temporal.PlainDate.from() für vollständige Kalenderdaten und Temporal.PlainTime.from() für Uhrzeiten.
    • Verwenden Sie Temporal.PlainDateTime.from() (oder plainDate.toPlainDateTime(plainTime)), wenn Sie ein Kalenderdatum und eine Uhrzeit kombinieren, ohne eine Zeitzone festzulegen.
    • Verwenden Sie Temporal.PlainMonthDay.from() für jährliche Datumsangaben ohne Jahr und konvertieren Sie sie mit .toPlainDate({ year }) in einen PlainDate, bevor Sie Datumsberechnungen durchführen.
    • Verwenden Sie Temporal.PlainYearMonth.from() für Jahres- und Monatswerte, damit keine beliebigen Tageszahlen in Berechnungen einfließen.
  2. Beachten Sie den Parameter calendar beim Erstellen von Plain-Typen: ISO 8601 ('iso8601') ist der Standardkalender beim Parsen von ISO-Strings oder beim Erstellen mit PlainDate.from({ year, month, day }). Wenn Sie PlainMonthDay oder PlainYearMonth aus Property Bags erstellen oder mit Nicht-ISO-Kalendern arbeiten, übergeben Sie calendar explizit (z.B. { month: 10, day: 31, calendar: 'iso8601' }).
  3. .add() mit overflow-Optionen für wiederkehrende Kalenderintervalle verwenden: Beim Aufrufen von plainDate.add({ months: 1 }) wird standardmäßig { overflow: 'constrain' } verwendet, wodurch Monatsenddaten wie der 31. Januar auf den 28./29. Februar beschränkt werden. Übergeben Sie { overflow: 'reject' }, wenn ungültige Zieldaten einen RangeError auslösen sollen, anstatt sie zu klemmen.

Beispiel: Vollständige Datumsangaben, Uhrzeiten und PlainDateTime

// 1. Parse a calendar date and a wall-clock time from input strings
const birthdate = Temporal.PlainDate.from('1990-01-01');
const reminderTime = Temporal.PlainTime.from('09:00');

// Combine into a location-agnostic PlainDateTime without string interpolation
const localAppointment = birthdate.toPlainDateTime(reminderTime);

// 2. Display without time zone conversion; outputs the same date in any time zone
console.log(birthdate.toLocaleString('en-GB'));
console.log(localAppointment.toString()); // "1990-01-01T09:00:00"

// 3. Read calendar fields directly from the Plain type. A PlainDate already *is*
// a calendar date, so there is no need for the formatToParts() workaround that
// legacy Date code uses to recover year/month/day from an instant.
const { year, month, day } = birthdate;

Beispiel: Unvollständige Datumsangaben (PlainMonthDay und PlainYearMonth)

// Annual recurring event (no year component)
// Include explicit calendar when constructing from an object
const annualHoliday = Temporal.PlainMonthDay.from({
  month: 10,
  day: 31,
  calendar: 'iso8601',
});

const today = Temporal.Now.plainDateISO();
const isToday = annualHoliday.equals(today.toPlainMonthDay());

// Convert PlainMonthDay to a PlainDate with a specific year before doing arithmetic,
// because month lengths (such as February) depend on whether the year is a leap year.
const occurrenceThisYear = annualHoliday.toPlainDate({ year: today.year });

// Monthly expiration (no day component, preventing day-of-month overflow bugs)
const cardExpiry = Temporal.PlainYearMonth.from({
  year: 2027,
  month: 12,
  calendar: 'iso8601',
});
const currentMonth = today.toPlainYearMonth();
const isExpired = Temporal.PlainYearMonth.compare(cardExpiry, currentMonth) < 0;

Beispiel: Wiederkehrende monatliche Intervalle (overflow: 'constrain' im Vergleich zu 'reject')

const billingStart = Temporal.PlainDate.from('2024-01-31');

// Default overflow: 'constrain' clamps Jan 31 + 1 month to the last valid day of February
const nextBilling = billingStart.add({ months: 1 }, { overflow: 'constrain' });
console.log(nextBilling.toString()); // "2024-02-29" (2024 is a leap year)

// Use overflow: 'reject' to throw a RangeError when the target day does not exist
try {
  billingStart.add({ months: 1 }, { overflow: 'reject' });
} catch (err) {
  // RangeError: day 31 does not exist in February
}

Beispiel: Interoperabilität mit Legacy-Date

Ein Legacy-Date ist ein Zeitstempel (ein Zeitpunkt), kein Kalenderdatum. Wenn Sie "1990-01-01" als Date speichern, müssen Sie einen Zeitpunkt auswählen (normalerweise Mitternacht UTC). Wenn Sie ihn in einer Zeitzone westlich von UTC lesen, erhalten Sie "31/12/1989". Wenn eine Bibliothek oder API weiterhin einen Date erfordert, konvertieren Sie explizit über Temporal.Instant, anstatt ISO-Strings manuell zu erstellen:

// PlainDate -> Date: attach a time zone (time of day defaults to the start of
// that day) to get an Instant, then hand the epoch milliseconds to Date.
// 'UTC' is a deliberate choice and must be documented; any zone would do,
// but the stored instant depends on it.
const instant = birthdate.toZonedDateTime('UTC').toInstant();
const legacyDate = new Date(instant.epochMilliseconds);

// Date -> PlainDate: go back through Instant (legacyDate.toTemporalInstant()
// is equivalent), then project into an explicit time zone. The result depends
// on the zone, which is exactly the drift risk.
const viewedDate = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime())
  .toZonedDateTimeISO('America/New_York')
  .toPlainDate();

// Compare Plain types with .equals() (or Temporal.PlainDate.compare), not strings.
console.log(viewedDate.equals(birthdate)); // false: the calendar date drifted to 1989-12-31

Strategische Implementierung und Best Practices

  • Verwenden Sie Temporal.PlainDate für vollständige Kalenderdaten, Temporal.PlainTime für Uhrzeiten, Temporal.PlainDateTime für lokale Datums- und Uhrzeitpaare ohne Zeitzone, Temporal.PlainMonthDay für wiederkehrende jährliche Daten und Temporal.PlainYearMonth für Ablaufdaten oder Abrechnungszeiträume, die nur aus Jahr und Monat bestehen.
  • Führen Sie KEINE arithmetischen Operationen direkt für Temporal.PlainMonthDay aus. Konvertieren Sie den Wert zuerst mit .toPlainDate({ year }) in einen Temporal.PlainDate, da die Monatslängen je nach Jahr variieren.
  • DO use .add() on Temporal.PlainDate for recurring calendar intervals, relying on overflow: 'constrain' (the default) to clamp month-end dates or overflow: 'reject' to throw a RangeError on nonexistent dates.
  • Geben Sie calendar (z.B. calendar: 'iso8601') an, wenn Sie PlainMonthDay- oder Nicht-ISO-Plain-Typen aus Objekten erstellen.
  • Verwenden Sie KEINE einfachen Typen, wenn Sie einen bestimmten Zeitpunkt in der physischen Zeit (einen Moment) darstellen müssen. Verwenden Sie Temporal.Instant oder Temporal.ZonedDateTime für Logs, Ereigniszeitstempel oder die Planung über Zeitzonen hinweg.
  • Erstellen Sie KEINE Date-Objekte aus manuell erstellten ISO-Strings (z. B. `${date}T00:00:00Z`). Erstellen Sie ein Date-Objekt aus dem epochMilliseconds-Wert eines Temporal.Instant- oder Temporal.ZonedDateTime-Objekts und lesen Sie es mit Temporal.Instant.fromEpochMilliseconds(date.getTime()) oder date.toTemporalInstant() aus, damit die Zeitzone explizit angegeben wird.
  • Verwenden Sie Intl.DateTimeFormat.prototype.formatToParts() NICHT, um Werte für Jahr, Monat oder Tag zu extrahieren oder zu vergleichen. .year, .month und .day direkt aus einem einfachen Typ lesen und mit .equals() vergleichen.
  • DO identify time zones by IANA name (e.g., America/New_York) and never by a fixed UTC offset. Die Zeitverschiebung ändert sich mit der Sommerzeit und der Gesetzgebung, sodass Labels wie "UTC-5" für einen Teil des Jahres falsch sind.
  • DENKEN SIE DARAN, dass Temporal-Objekte unveränderlich sind. Methoden wie .add(), .subtract() oder .with() geben immer eine neue Instanz mit einer neuen Referenz zurück, anstatt das ursprüngliche Objekt direkt zu ändern. Sie können daher sicher direkt im reaktiven UI-Status gespeichert werden.

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();
})();