DST Handling
How Pragmatic.Temporal handles Daylight Saving Time transitions.
The DST Problem
Section titled “The DST Problem”When clocks change, two problems occur:
- Spring Forward: Some local times don’t exist (e.g., 2:30 AM skipped)
- Fall Back: Some local times are ambiguous (e.g., 2:30 AM occurs twice)
DST Policies
Section titled “DST Policies”Pragmatic.Temporal provides explicit policies for handling these edge cases:
public enum DstPolicy{ ThrowOnAmbiguous, // Throw exception for ambiguous times ThrowOnInvalid, // Throw exception for invalid times SkipForward, // Move invalid time forward to valid time SkipBackward, // Move invalid time backward to valid time UseEarlier, // Use earlier offset for ambiguous times UseLater // Use later offset for ambiguous times}Invalid Times (Spring Forward)
Section titled “Invalid Times (Spring Forward)”When clocks spring forward, some times don’t exist:
// In US Eastern: March 10, 2024, 2:00 AM clocks move to 3:00 AM// 2:30 AM doesn't exist
var local = new LocalDateTime(2024, 3, 10, 2, 30);
// Option 1: Throw (default for strict applications)try{ var zoned = ZonedDateTime.FromLocal(local, "America/New_York", DstPolicy.ThrowOnInvalid);}catch (InvalidTimeException ex){ // Handle the invalid time}
// Option 2: Skip forward to valid timevar skipped = ZonedDateTime.FromLocal(local, "America/New_York", DstPolicy.SkipForward);// Result: 3:30 AM (skipped the gap)
// Option 3: Skip backward (less common)var backward = ZonedDateTime.FromLocal(local, "America/New_York", DstPolicy.SkipBackward);// Result: 1:30 AMAmbiguous Times (Fall Back)
Section titled “Ambiguous Times (Fall Back)”When clocks fall back, some times occur twice:
// In US Eastern: November 3, 2024, 2:00 AM clocks move to 1:00 AM// 1:30 AM occurs twice (once in EDT, once in EST)
var local = new LocalDateTime(2024, 11, 3, 1, 30);
// Option 1: Throw (default for strict applications)try{ var zoned = ZonedDateTime.FromLocal(local, "America/New_York", DstPolicy.ThrowOnAmbiguous);}catch (AmbiguousTimeException ex){ // Handle the ambiguous time}
// Option 2: Use earlier occurrence (still in Daylight time)var earlier = ZonedDateTime.FromLocal(local, "America/New_York", DstPolicy.UseEarlier);// Result: 1:30 AM EDT (UTC-4)
// Option 3: Use later occurrence (in Standard time)var later = ZonedDateTime.FromLocal(local, "America/New_York", DstPolicy.UseLater);// Result: 1:30 AM EST (UTC-5)Best Practices
Section titled “Best Practices”1. Store UTC, Convert at Boundaries
Section titled “1. Store UTC, Convert at Boundaries”public class Event{ // Store as UTC public DateTimeOffset StartsAt { get; set; }
// Display in user's timezone public ZonedDateTime GetLocalStart(string timezone) { return ZonedDateTime.FromUtc(StartsAt, timezone); }}2. Be Explicit About DST Policy
Section titled “2. Be Explicit About DST Policy”// Good: Explicit policyvar zoned = ZonedDateTime.FromLocal(local, zone, DstPolicy.SkipForward);
// Avoid: Implicit behaviorvar zoned = ZonedDateTime.FromLocal(local, zone); // What happens on DST?3. Validate User Input
Section titled “3. Validate User Input”public Result<ZonedDateTime, TemporalError> ParseUserDateTime( string dateTimeStr, string timezone){ if (!LocalDateTime.TryParse(dateTimeStr, out var local)) return new InvalidFormatError();
try { return ZonedDateTime.FromLocal(local, timezone, DstPolicy.ThrowOnInvalid); } catch (InvalidTimeException) { return new InvalidTimeError($"The time {dateTimeStr} doesn't exist in {timezone}"); } catch (AmbiguousTimeException) { return new AmbiguousTimeError($"The time {dateTimeStr} is ambiguous in {timezone}"); }}4. Test DST Transitions
Section titled “4. Test DST Transitions”[Theory][InlineData("2024-03-10T02:30:00", "America/New_York", false)] // Invalid[InlineData("2024-11-03T01:30:00", "America/New_York", true)] // Ambiguouspublic void ShouldHandleDstTransitions(string dateTime, string zone, bool isAmbiguous){ var local = LocalDateTime.Parse(dateTime);
// Test your handling logic}Common Timezone DST Rules
Section titled “Common Timezone DST Rules”| Region | Spring Forward | Fall Back |
|---|---|---|
| US Eastern | 2nd Sunday March, 2 AM | 1st Sunday November, 2 AM |
| US Pacific | 2nd Sunday March, 2 AM | 1st Sunday November, 2 AM |
| Central Europe | Last Sunday March, 2 AM | Last Sunday October, 3 AM |
| UK | Last Sunday March, 1 AM | Last Sunday October, 2 AM |
| Australia (Sydney) | 1st Sunday October, 2 AM | 1st Sunday April, 3 AM |
Note: Some regions don’t observe DST (e.g., Arizona, most of Asia, Africa).
Scheduling Across DST
Section titled “Scheduling Across DST”When scheduling recurring events:
// Problem: "Every day at 2:30 AM" - what happens on DST days?
// Solution: Use wall clock time with explicit policypublic IEnumerable<ZonedDateTime> GetDailyOccurrences( LocalTime time, string timezone, LocalDate start, int count){ var current = start; for (int i = 0; i < count; i++) { var local = current.At(time);
// Skip forward if time doesn't exist (DST spring forward) var zoned = ZonedDateTime.FromLocal(local, timezone, DstPolicy.SkipForward); yield return zoned;
current = current.AddDays(1); }}