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

  1. In UTC speichern – Zeitstempel immer in UTC speichern, nur zur Anzeige in lokal konvertieren
  2. Verwenden Sie Zeitzonen-fähige Bibliotheken – Versuchen Sie nicht, die Sommerzeit manuell zu handhaben
  3. Randfälle validieren – Testen Sie mit Spring-Forward- und Fallback-Daten
  4. Mehrdeutigkeit explizit behandeln – Geben Sie an, welches Vorkommen doppelter Stunden Sie meinen
  5. 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-fns
  • luxon – Moderne Datetime-Bibliothek mit hervorragender DST-Verarbeitung
  • moment-timezone – Umfangreiche Zeitzonendatenbank (Wartungsmodus)

Python:

  • pytz – Standard-Zeitzonenbibliothek für Python
  • dateutil – Alternative mit guter DST-Unterstützung
  • zoneinfo (Python 3.9+) - Integrierte Zeitzonenunterstützung

Kurzreferenz

ProblemLö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
DauerberechnungVerwenden Sie UTC-Zeitstempel, keine Ortszeiten
Wiederkehrende EreignisseJedes Vorkommen einzeln lokalisieren
TestenTesten Sie immer mit Sommerzeit-Übergangsdaten

Verwandte Ressourcen