DST-Übergänge in wiederkehrenden Kalenderevents richtig handhaben
Wiederkehrende Kalenderevents haben ein DST-Problem, das nur zweimal im Jahr auftaucht – und fast immer in der Produktion, nicht in Tests.
Ein Benutzer erstellt ein wiederkehrendes Meeting: „Jeden Dienstag um 9 Uhr, America/New_York.” Im Winter ist 9 Uhr EST = 14:00 UTC. Im Sommer ist 9 Uhr EDT = 13:00 UTC. Die UTC-Zeit ändert sich; die Ortszeit bleibt gleich.
Wenn du 14:00 UTC als wiederkehrende Zeit gespeichert hast statt 09:00 America/New_York, feuert das Meeting im Sommer jeden Dienstag um 10 Uhr New Yorker Zeit. Der Benutzer bemerkt das im März, fragt sich, warum sein wiederkehrendes Meeting „verschoben” wurde, und öffnet ein Ticket.
Die Ursache
Eine wiederkehrende Zeit kann zwei verschiedene Dinge bedeuten:
- Wanduhrzeit: „Dieses Meeting ist immer um 9 Uhr in meiner Stadt.” Das UTC-Äquivalent ändert sich mit DST.
- Feste UTC-Zeit: „Dieses Meeting feuert immer um 14:00 UTC.” Die Ortszeit ändert sich mit DST.
Für benutzerseitige Planung – Meetings, Erinnerungen, Standups – meint der Benutzer die Wanduhrzeit. Wenn du einen festen UTC-Offset speicherst, implementierst du das andere – was fast nie das ist, was der Benutzer will.
Das korrekte Speichermodell
Speichere zwei Dinge:
- Die lokale Zeit und Wiederholungsregel (
RRULE):DTSTART;TZID=America/New_York:20260106T090000/RRULE:FREQ=WEEKLY;BYDAY=TU - Den IANA-Zeitzonennamen:
America/New_York
Wenn du das nächste Vorkommen berechnest, konvertiere die Ortszeit zum Berechnungszeitpunkt – nicht zum Erstellungszeitpunkt. Das stellt sicher, dass Vorkommen nach DST den korrekten (Sommer-)Offset verwenden.
Speichere keinen festen UTC-Offset (+05:30, -05:00) als Zone für ein wiederkehrendes Event. Offsets sind Momentaufnahmen. IANA-Namen sind Regeln.
iCalendar (RFC 5545) macht das richtig
Die iCalendar-Spezifikation verwendet TZID bei DTSTART genau aus diesem Grund:
BEGIN:VEVENT
DTSTART;TZID=America/New_York:20260106T090000
RRULE:FREQ=WEEKLY;BYDAY=TU
SUMMARY:Wöchentlicher Standup
END:VEVENT
Wenn ein Kalender-Client diese Wiederholung nach DST expandiert, berechnet er jedes Vorkommen in America/New_York – die Juni-Vorkommen werden als 13:00 UTC und die Januar-Vorkommen als 14:00 UTC generiert, beide korrekt auf 9 Uhr Ortszeit gemappt.
Wenn du ein Scheduling-Tool baust, folge diesem Modell.
Vorkommen im Code generieren
JavaScript (mit Temporal API):
import { Temporal } from "@js-temporal/polyfill";
const timeZone = "America/New_York";
const startLocal = Temporal.PlainDateTime.from("2026-01-06T09:00:00");
// Nächste 8 Dienstags-Vorkommen generieren
const occurrences = [];
let current = startLocal;
for (let i = 0; i < 8; i++) {
const zonedDT = current.toZonedDateTime(timeZone);
occurrences.push(zonedDT.toInstant().toString()); // UTC-Instant
current = current.add({ weeks: 1 });
}
// Januar-Vorkommen → 2026-01-06T14:00:00Z (EST, UTC-5)
// Juni-Vorkommen → 2026-06-02T13:00:00Z (EDT, UTC-4) ✓
Python:
from datetime import datetime
from zoneinfo import ZoneInfo
from dateutil.rrule import rrule, WEEKLY, TU
tz = ZoneInfo('America/New_York')
start = datetime(2026, 1, 6, 9, 0, tzinfo=tz)
occurrences = list(rrule(WEEKLY, byweekday=TU, dtstart=start, count=8))
for dt in occurrences:
print(dt.astimezone(ZoneInfo('UTC')))
# Jedes Vorkommen wird in America/New_York berechnet — DST automatisch berücksichtigt
DST-Handling testen
Schreibe explizite Tests rund um DST-Übergangsdaten. Für America/New_York:
- Spring Forward: zweiter Sonntag im März (Uhren springen von 2 Uhr auf 3 Uhr)
- Fall Back: erster Sonntag im November (Uhren fallen von 2 Uhr auf 1 Uhr)
Teste, dass ein wiederkehrendes Event, das für 9 Uhr am Dienstag vor und nach jedem Übergang eingestellt ist, in beiden Wochen um 9 Uhr Ortszeit feuert – nicht um 9 Uhr und dann um 10 Uhr.
// Test: Dienstag vor Spring Forward → 14:00 UTC
// Test: Dienstag nach Spring Forward → 13:00 UTC
// Beide sollten als 09:00 America/New_York angezeigt werden ✓
Dieser Test ist günstig zu schreiben und deckt die gesamte Klasse der DST-Bugs bei wiederkehrenden Events ab.