Tutorial
Umgang mit Sommerzeitumstellungen: Vollständiges Tutorial
Einführung
Die Umstellung auf Sommerzeit (DST) ist einer der schwierigsten Aspekte bei der Arbeit mit Zeitstempeln. Zweimal im Jahr springen die Uhren „vor“ oder „zurück“, wodurch Grenzfälle entstehen, die zu Fehlern, Datenverlust und falschen Berechnungen führen können. In diesem Tutorial erfahren Sie, wie Sie DST-Übergänge in Ihren Anwendungen korrekt handhaben.
Sommerzeitübergänge verstehen
Was passiert während der Sommerzeit?
Spring Forward (Beginn der Sommerzeit)
- Die Uhren werden um eine Stunde vorgestellt (normalerweise um 2:00 Uhr → 3:00 Uhr).
- Eine Stunde "fehlt" - Zeiten wie 2:30 Uhr gibt es nicht
- Dauer: Der Tag ist nur 23 Stunden lang
Fallback (Ende der Sommerzeit)
- Die Uhren werden 1 Stunde zurückgestellt (normalerweise um 2:00 Uhr → 1:00 Uhr).
- Eine Stunde wird „dupliziert“ – 1:30 Uhr kommt zweimal vor
- Dauer: Der Tag dauert 25 Stunden
Auswirkungen auf die reale Welt
// Spring Forward - March 10, 2024 (US)
// Problem: Scheduled task at 2:30 AM doesn't execute
const scheduled = new Date('2024-03-10T02:30:00');
// This time doesn't exist! JavaScript may interpret it as 3:30 AM
// Fall Back - November 3, 2024 (US)
// Problem: Log entries at 1:30 AM appear twice
const firstOccurrence = new Date('2024-11-03T01:30:00-04:00'); // EDT
const secondOccurrence = new Date('2024-11-03T01:30:00-05:00'); // EST
// Same wall clock time, different actual times!
Häufige DST-Probleme
Problem 1: Fehlende Stunde (Spring Forward)
Wenn die Uhren vorgestellt werden, kann der Versuch, in der fehlenden Stunde einen Zeitstempel zu erstellen, zu unerwartetem Verhalten führen.
JavaScript-Verhalten
// March 10, 2024, 2:30 AM in New York doesn't exist
const date = new Date('2024-03-10T02:30:00');
console.log(date.toLocaleString('en-US', {
timeZone: 'America/New_York',
hour12: false
}));
// Output varies by browser/environment
// Most will interpret as 3:30 AM or 1:30 AM
Python-Verhalten
from datetime import datetime
import pytz
ny_tz = pytz.timezone('America/New_York')
# Naive datetime in missing hour
try:
dt = ny_tz.localize(datetime(2024, 3, 10, 2, 30))
print(dt)
except pytz.exceptions.NonExistentTimeError as e:
print(f"Error: {e}")
# Error: 2024-03-10 02:30:00
Lösung: Nicht vorhandene Zeiten explizit behandeln
# Option 1: Use is_dst parameter
dt = ny_tz.localize(datetime(2024, 3, 10, 2, 30), is_dst=None)
# Raises exception for ambiguous time
# Option 2: Normalize after creation
naive_dt = datetime(2024, 3, 10, 2, 30)
dt = ny_tz.normalize(ny_tz.localize(naive_dt, is_dst=False))
print(dt)
# 2024-03-10 03:30:00 EDT (adjusted forward)
Problem 2: Doppelte Stunde (Fallback)
Wenn die Uhren zurückgestellt werden, tritt die gleiche Uhrzeit zweimal auf, was zu Mehrdeutigkeiten führt.
JavaScript-Beispiel
// November 3, 2024, 1:30 AM in New York occurs twice
// First occurrence (before fall back)
const first = new Date('2024-11-03T01:30:00-04:00'); // EDT
// Second occurrence (after fall back)
const second = new Date('2024-11-03T01:30:00-05:00'); // EST
console.log(first.getTime()); // 1730616600000
console.log(second.getTime()); // 1730620200000
console.log(second - first); // 3600000 (1 hour difference)
Python-Beispiel
from datetime import datetime
import pytz
ny_tz = pytz.timezone('America/New_York')
# Ambiguous time - which occurrence?
try:
dt = ny_tz.localize(datetime(2024, 11, 3, 1, 30))
except pytz.exceptions.AmbiguousTimeError as e:
print(f"Ambiguous: {e}")
# Specify which occurrence
first = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=True) # Before fall back
second = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=False) # After fall back
print(first) # 2024-11-03 01:30:00 EDT-0400
print(second) # 2024-11-03 01:30:00 EST-0500
Problem 3: Falsche Dauerberechnungen
Sommerzeitübergänge wirken sich auf Dauerberechnungen aus, wenn kalenderbasierte Arithmetik verwendet wird.
// Calculate hours between midnight on DST transition days
// Spring Forward Day (23 hours)
const springStart = new Date('2024-03-10T00:00:00');
const springEnd = new Date('2024-03-10T23:59:59');
const springHours = (springEnd - springStart) / 3600000;
console.log(springHours); // 23.999... hours (not 24!)
// Fall Back Day (25 hours)
const fallStart = new Date('2024-11-03T00:00:00');
const fallEnd = new Date('2024-11-03T23:59:59');
const fallHours = (fallEnd - fallStart) / 3600000;
console.log(fallHours); // 24.999... hours (appears normal but day is 25 hours)
Best Practices für den Umgang mit der Sommerzeit
1. Verwenden Sie immer Zeitzonen-bezogene Datumsangaben
JavaScript mit date-fns-tz
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz';
// Always work in UTC internally
const utcDate = zonedTimeToUtc('2024-03-10 02:30', 'America/New_York');
// Convert to local only for display
const nyDate = utcToZonedTime(utcDate, 'America/New_York');
console.log(format(nyDate, 'yyyy-MM-dd HH:mm:ss zzz', { timeZone: 'America/New_York' }));
Python mit Pytz
from datetime import datetime
import pytz
# Always work with timezone-aware datetimes
utc = pytz.UTC
ny_tz = pytz.timezone('America/New_York')
# Create timezone-aware datetime
dt_utc = datetime(2024, 3, 10, 7, 30, tzinfo=utc) # UTC time
dt_ny = dt_utc.astimezone(ny_tz) # Convert to NY time
print(dt_ny) # 2024-03-10 03:30:00 EDT (automatically adjusted for DST)
2. DST-Übergänge erkennen
JavaScript: Überprüfen Sie, ob das Datum auf Sommerzeit eingestellt ist
function isDST(date, timezone) {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const janOffset = jan.getTimezoneOffset();
const julOffset = jul.getTimezoneOffset();
const stdOffset = Math.max(janOffset, julOffset);
const currentOffset = date.getTimezoneOffset();
return currentOffset < stdOffset;
}
const winterDate = new Date('2024-01-15T12:00:00');
const summerDate = new Date('2024-07-15T12:00:00');
console.log(isDST(winterDate)); // false
console.log(isDST(summerDate)); // true
Python: Sommerzeit-Übergangstermine finden
from datetime import datetime, timedelta
import pytz
def find_dst_transitions(year, timezone_name):
"""Find DST transition dates for a given year and timezone."""
tz = pytz.timezone(timezone_name)
transitions = []
# Check each day of the year
for day in range(365):
date = datetime(year, 1, 1) + timedelta(days=day)
today = tz.localize(datetime(year, 1, 1) + timedelta(days=day), is_dst=None)
tomorrow = tz.localize(datetime(year, 1, 1) + timedelta(days=day + 1), is_dst=None)
# Check if UTC offset changed
if today.utcoffset() != tomorrow.utcoffset():
transitions.append({
'date': date.strftime('%Y-%m-%d'),
'from_offset': str(today.utcoffset()),
'to_offset': str(tomorrow.utcoffset()),
'type': 'Spring Forward' if today.utcoffset() < tomorrow.utcoffset() else 'Fall Back'
})
return transitions
# Find 2024 DST transitions in New York
transitions = find_dst_transitions(2024, 'America/New_York')
for t in transitions:
print(f"{t['date']}: {t['type']} ({t['from_offset']} → {t['to_offset']})")
# Output:
# 2024-03-10: Spring Forward (-5:00:00 → -4:00:00)
# 2024-11-03: Fall Back (-4:00:00 → -5:00:00)
3. Gehen Sie ordnungsgemäß mit fehlenden Stunden um
JavaScript: Auf gültige Zeit normalisieren
function normalizeToValidTime(dateString, timezone) {
try {
// Attempt to create date
const date = new Date(dateString);
// Check if time exists by comparing round-trip conversion
const formatted = date.toLocaleString('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
// If times don't match, time was adjusted
return {
original: dateString,
normalized: date.toISOString(),
wasAdjusted: !dateString.includes(formatted.split(',')[1].trim())
};
} catch (error) {
return { error: error.message };
}
}
const result = normalizeToValidTime('2024-03-10T02:30:00', 'America/New_York');
console.log(result);
// { original: '2024-03-10T02:30:00', normalized: '2024-03-10T07:30:00.000Z', wasAdjusted: true }
Python: Explizite DST-Behandlung
def safe_localize(tz, dt, prefer_dst=True):
"""
Safely localize datetime, handling DST transitions.
Args:
tz: pytz timezone
dt: naive datetime
prefer_dst: If True, prefer DST time during ambiguous hour
Returns:
Localized datetime
"""
try:
# Try to localize normally
return tz.localize(dt, is_dst=None)
except pytz.exceptions.AmbiguousTimeError:
# Ambiguous time (fall back) - specify preference
return tz.localize(dt, is_dst=prefer_dst)
except pytz.exceptions.NonExistentTimeError:
# Non-existent time (spring forward) - normalize forward
return tz.normalize(tz.localize(dt, is_dst=False))
# Usage
ny_tz = pytz.timezone('America/New_York')
# Missing hour
missing = safe_localize(ny_tz, datetime(2024, 3, 10, 2, 30))
print(missing) # 2024-03-10 03:30:00 EDT (adjusted forward)
# Duplicate hour
duplicate = safe_localize(ny_tz, datetime(2024, 11, 3, 1, 30), prefer_dst=True)
print(duplicate) # 2024-11-03 01:30:00 EDT (first occurrence)
4. Zeitstempel in UTC speichern
Speichern Sie Zeitstempel immer in UTC und konvertieren Sie sie nur zur Anzeige in die Ortszeit.
// Database storage pattern
class EventScheduler {
// Store in UTC
scheduleEvent(localDateString, timezone) {
const localDate = new Date(localDateString);
const utcTimestamp = localDate.getTime();
// Save to database
return {
utc_timestamp: utcTimestamp,
utc_iso: new Date(utcTimestamp).toISOString(),
original_timezone: timezone
};
}
// Retrieve and display in local time
getEventInTimezone(utcTimestamp, timezone) {
const date = new Date(utcTimestamp);
return date.toLocaleString('en-US', {
timeZone: timezone,
dateStyle: 'full',
timeStyle: 'long'
});
}
}
const scheduler = new EventScheduler();
// Schedule event
const event = scheduler.scheduleEvent('2024-03-10T02:30:00', 'America/New_York');
console.log(event);
// { utc_timestamp: 1710054600000, utc_iso: '2024-03-10T07:30:00.000Z', ... }
// Display in different timezones
console.log(scheduler.getEventInTimezone(event.utc_timestamp, 'America/New_York'));
console.log(scheduler.getEventInTimezone(event.utc_timestamp, 'Europe/London'));
5. DST-Edge-Fälle testen
Testen Sie Ihren Code immer mit Sommerzeit-Übergangsdaten.
// Test suite for DST handling
describe('DST Transition Tests', () => {
const dstDates = {
springForward: '2024-03-10',
fallBack: '2024-11-03',
missingHour: '2024-03-10T02:30:00',
duplicateHour: '2024-11-03T01:30:00'
};
test('handles missing hour in spring forward', () => {
const result = normalizeToValidTime(
dstDates.missingHour,
'America/New_York'
);
expect(result.wasAdjusted).toBe(true);
});
test('duration calculation on spring forward day', () => {
const start = new Date(`${dstDates.springForward}T00:00:00`);
const end = new Date(`${dstDates.springForward}T23:59:59`);
const hours = (end - start) / 3600000;
expect(hours).toBeCloseTo(23, 0); // 23 hours, not 24
});
test('duration calculation on fall back day', () => {
const start = new Date(`${dstDates.fallBack}T00:00:00`);
const end = new Date(`${dstDates.fallBack}T23:59:59`);
const hours = (end - start) / 3600000;
expect(hours).toBeCloseTo(24, 0); // Appears as 24 but day is 25 hours
});
});
Praktische Szenarien
Szenario 1: Wiederkehrende Ereignisse planen
from datetime import datetime, timedelta
import pytz
def schedule_daily_task(start_date, local_time, timezone_name, days=30):
"""
Schedule a task at the same local time each day, accounting for DST.
"""
tz = pytz.timezone(timezone_name)
events = []
for day in range(days):
# Create naive datetime for each day
date = start_date + timedelta(days=day)
naive_dt = datetime.combine(date, local_time)
# Safely localize (handles DST transitions)
try:
localized = tz.localize(naive_dt, is_dst=None)
except pytz.exceptions.NonExistentTimeError:
# Time doesn't exist (spring forward), adjust forward
localized = tz.normalize(tz.localize(naive_dt, is_dst=False))
except pytz.exceptions.AmbiguousTimeError:
# Time occurs twice (fall back), use first occurrence
localized = tz.localize(naive_dt, is_dst=True)
events.append({
'local_time': localized.strftime('%Y-%m-%d %H:%M:%S %Z'),
'utc_time': localized.astimezone(pytz.UTC).strftime('%Y-%m-%d %H:%M:%S UTC'),
'timestamp': int(localized.timestamp())
})
return events
# Schedule daily task at 2:00 AM, crossing DST boundary
from datetime import time, date
events = schedule_daily_task(
start_date=date(2024, 3, 8),
local_time=time(2, 0, 0),
timezone_name='America/New_York',
days=5
)
for event in events:
print(f"{event['local_time']} → {event['utc_time']}")
# Output shows how UTC time shifts on DST transition day
Szenario 2: Berechnung der Geschäftszeiten während der Sommerzeit
function calculateBusinessHours(startDate, endDate, timezone) {
const businessHoursPerDay = 8; // 9 AM - 5 PM
const businessStart = 9;
const businessEnd = 17;
let totalHours = 0;
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const dayOfWeek = currentDate.getDay();
// Skip weekends
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
// Check if it's a DST transition day
const dayStart = new Date(currentDate);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(currentDate);
dayEnd.setHours(23, 59, 59, 999);
const dayLength = (dayEnd - dayStart) / 3600000;
if (dayLength < 24) {
// Spring forward - missing hour might affect business hours
console.log(`Spring forward on ${currentDate.toDateString()}`);
totalHours += Math.min(businessHoursPerDay, dayLength - (24 - dayLength));
} else if (dayLength > 24) {
// Fall back - extra hour
console.log(`Fall back on ${currentDate.toDateString()}`);
totalHours += businessHoursPerDay; // Business hours unchanged
} else {
totalHours += businessHoursPerDay;
}
}
currentDate.setDate(currentDate.getDate() + 1);
}
return totalHours;
}
// Calculate business hours March 1-15, 2024 (includes DST transition)
const hours = calculateBusinessHours(
new Date('2024-03-01'),
new Date('2024-03-15'),
'America/New_York'
);
console.log(`Total business hours: ${hours}`);
Häufige Fallstricke
❌ Tun Sie das nicht
// Bad: Assuming all days are 24 hours
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const hours = (tomorrow - today) / 3600000; // Won't be exactly 24 on DST days!
// Bad: Using string concatenation for times
const timeString = `${year}-${month}-${day} 02:30:00`;
const date = new Date(timeString); // Might not exist on spring forward day!
// Bad: Ignoring timezone in calculations
const event1 = new Date('2024-11-03T01:30:00'); // Which occurrence?
✅ Tun Sie dies stattdessen
// Good: Use UTC for calculations
const tomorrow = new Date(today.getTime() + 86400000); // Always exactly 24 hours
// Good: Use timezone-aware libraries
import { zonedTimeToUtc } from 'date-fns-tz';
const safeDate = zonedTimeToUtc('2024-03-10 02:30', 'America/New_York');
// Good: Always include timezone offset
const event1 = new Date('2024-11-03T01:30:00-04:00'); // First occurrence (EDT)
const event2 = new Date('2024-11-03T01:30:00-05:00'); // Second occurrence (EST)
Zusammenfassung und Best Practices
Wichtige Erkenntnisse
- In UTC speichern – Zeitstempel immer in UTC speichern, nur zur Anzeige in lokal konvertieren
- Verwenden Sie Zeitzonen-fähige Bibliotheken – Versuchen Sie nicht, die Sommerzeit manuell zu handhaben
- Randfälle validieren – Testen Sie mit Spring-Forward- und Fallback-Daten
- Mehrdeutigkeit explizit behandeln – Geben Sie an, welches Vorkommen doppelter Stunden Sie meinen
- Fehlende Stunden normalisieren – Passen Sie nicht vorhandene Zeiten nach vorne an oder verwenden Sie die nächstgelegene gültige Zeit
Empfohlene Bibliotheken
JavaScript:
date-fns-tz– Zeitzonenunterstützung für date-fnsluxon– Moderne Datetime-Bibliothek mit hervorragender DST-Verarbeitungmoment-timezone– Umfangreiche Zeitzonendatenbank (Wartungsmodus)
Python:
pytz– Standard-Zeitzonenbibliothek für Pythondateutil– Alternative mit guter DST-Unterstützungzoneinfo(Python 3.9+) - Integrierte Zeitzonenunterstützung
Kurzreferenz
| Problem | Lösung |
|---|---|
| Fehlende Stunde (vorwärts springen) | Auf die nächste gültige Zeit normalisieren (1 Stunde vorwärts gehen) |
| Doppelte Stunde (Fallback) | Verwenden Sie den Zeitzonen-Offset, um anzugeben, welches Vorkommen |
| Dauerberechnung | Verwenden Sie UTC-Zeitstempel, keine Ortszeiten |
| Wiederkehrende Ereignisse | Jedes Vorkommen einzeln lokalisieren |
| Testen | Testen Sie immer mit Sommerzeit-Übergangsdaten |