How does chrono::TimeZone::with_ymd_and_hms handle ambiguous datetime values during timezone transitions?

with_ymd_and_hs returns a LocalResult enum that can represent unique datetimes, ambiguous datetimes (during fall-back transitions), or non-existent datetimes (during spring-forward transitions), forcing callers to explicitly handle timezone transition edge cases rather than silently losing information. This design prevents subtle bugs where a datetime could represent two different moments in time during daylight saving transitions.

Basic with_ymd_and_hms Usage

use chrono::{TimeZone, Utc, FixedOffset};
 
fn basic_usage() {
    // For UTC or fixed offset, there's always exactly one result
    let dt = Utc.with_ymd_and_hms(2024, 3, 15, 10, 30, 0);
    
    // Returns LocalResult<DateTime<T>>
    match dt {
        chrono::LocalResult::Single(dt) => {
            println!("Unique datetime: {}", dt);
        }
        _ => {
            println!("Ambiguous or non-existent");
        }
    }
    
    // Fixed offset also always returns Single
    let offset = FixedOffset::east_opt(5 * 3600).unwrap(); // UTC+5
    let dt = offset.with_ymd_and_hms(2024, 3, 15, 10, 30, 0);
    
    assert!(matches!(dt, chrono::LocalResult::Single(_)));
}

For simple timezones, with_ymd_and_hms returns a Single result.

The LocalResult Enum

use chrono::LocalResult;
 
fn local_result_variants() {
    // LocalResult<T> has three variants:
    
    // None: DateTime doesn't exist (skipped during spring forward)
    // Example: 2:00 AM - 3:00 AM might be skipped
    
    // Single: Exactly one valid DateTime
    
    // Ambiguous: Two possible DateTimes (during fall back)
    // Example: 1:00 AM - 2:00 AM might occur twice
    
    // The type is:
    // pub enum LocalResult<T> {
    //     None,
    //     Single(T),
    //     Ambiguous(T, T),
    // }
}

LocalResult captures all three possibilities for local time to datetime conversion.

Spring Forward: Non-Existent Times

use chrono::{TimeZone, FixedOffset};
 
fn spring_forward_example() {
    // Consider a timezone that springs forward at 2:00 AM
    // 1:59 AM -> 3:00 AM (2:00 AM - 2:59 AM don't exist)
    
    // This is common in many timezones during DST transition
    // In the US, for example, March 2024 at 2:00 AM
    
    // If we try to create 2:30 AM during the skipped hour:
    // The result would be LocalResult::None
    
    // For demonstration with a fixed offset (which doesn't have DST):
    let offset = FixedOffset::east_opt(5 * 3600).unwrap();
    
    // Fixed offsets always have exactly one result
    let result = offset.with_ymd_and_hms(2024, 3, 15, 2, 30, 0);
    assert!(matches!(result, LocalResult::Single(_)));
    
    // But with actual timezone (like America/New_York):
    // The same time during spring forward would be None
}
 
fn handle_nonexistent_time() {
    // When a time doesn't exist, you have options:
    // 1. Report error to user
    // 2. Use the earliest() or latest() method
    // 3. Manually adjust to valid time
    
    // Example handling:
    fn get_datetime_or_error(
        tz: &impl TimeZone,
        year: i32,
        month: u32,
        day: u32,
        hour: u32,
        min: u32,
        sec: u32,
    ) -> Result<chrono::DateTime<impl TimeZone>, String> {
        match tz.with_ymd_and_hms(year, month, day, hour, min, sec) {
            LocalResult::None => {
                Err(format!(
                    "Time {}-{:02}-{:02} {:02}:{:02}:{:02} doesn't exist (DST transition)",
                    year, month, day, hour, min, sec
                ))
            }
            LocalResult::Single(dt) => Ok(dt),
            LocalResult::Ambiguous(earliest, latest) => {
                Err(format!(
                    "Ambiguous time (DST transition): choose {:?} or {:?}",
                    earliest, latest
                ))
            }
        }
    }
}

During spring forward, times that fall in the skipped hour return None.

Fall Back: Ambiguous Times

use chrono::LocalResult;
 
fn fall_back_example() {
    // Consider a timezone that falls back at 2:00 AM
    // 1:59 AM (DST) -> 1:00 AM (standard)
    // 1:00 AM - 1:59 AM occur twice (once in DST, once in standard)
    
    // If we try to create 1:30 AM during the ambiguous hour:
    // The result would be LocalResult::Ambiguous(earliest, latest)
    
    // earliest: The first occurrence (usually in DST, "summer time")
    // latest: The second occurrence (usually in standard, "winter time")
    
    // Example handling:
    fn handle_ambiguous_time() {
        // Simulate ambiguous result
        let result: LocalResult<i32> = LocalResult::Ambiguous(1, 2);
        
        match result {
            LocalResult::None => println!("No valid time"),
            LocalResult::Single(t) => println!("Unique time: {}", t),
            LocalResult::Ambiguous(earliest, latest) => {
                println!("Ambiguous: {} or {}", earliest, latest);
                println!("Earliest: first occurrence (usually DST)");
                println!("Latest: second occurrence (usually standard time)");
            }
        }
    }
}

During fall back, times in the repeated hour return Ambiguous.

Working with LocalResult

use chrono::{TimeZone, LocalResult, DateTime};
 
fn local_result_methods() {
    // LocalResult provides several utility methods:
    
    // single() - Get the single value or None
    fn get_single<T>(result: LocalResult<T>) -> Option<T> {
        result.single()
    }
    
    // earliest() - Get earliest time for ambiguous, or single, or None
    fn get_earliest<T>(result: LocalResult<T>) -> Option<T> {
        result.earliest()
    }
    
    // latest() - Get latest time for ambiguous, or single, or None
    fn get_latest<T>(result: LocalResult<T>) -> Option<T> {
        result.latest()
    }
    
    // unwrap() - Panic on None or Ambiguous
    fn unwrap_single<T>(result: LocalResult<T>) -> T {
        // Panics if None or Ambiguous
        result.unwrap()
    }
    
    // map() - Transform the contained value(s)
    fn map_result<T, U, F>(result: LocalResult<T>, f: F) -> LocalResult<U>
    where
        F: Fn(T) -> U,
    {
        result.map(f)
    }
}

LocalResult provides methods to safely extract values.

Practical Example: User Input

use chrono::{TimeZone, LocalResult};
 
fn parse_user_datetime(
    year: i32, month: u32, day: u32,
    hour: u32, min: u32, sec: u32,
) -> Result<chrono::NaiveDateTime, String> {
    // When parsing user input, we need to handle all cases
    
    // For UTC, there's no ambiguity
    let result = chrono::Utc.with_ymd_and_hms(year, month, day, hour, min, sec);
    
    match result {
        LocalResult::Single(dt) => Ok(dt.naive_utc()),
        LocalResult::None => {
            // The time doesn't exist
            // Options:
            // 1. Error out
            // 2. Adjust to nearest valid time
            Err(format!(
                "Invalid time: {}-{:02}-{:02} {:02}:{:02}:{:02} doesn't exist",
                year, month, day, hour, min, sec
            ))
        }
        LocalResult::Ambiguous(earliest, latest) => {
            // The time is ambiguous
            // Options:
            // 1. Ask user to clarify
            // 2. Choose one (earliest or latest)
            // 3. Default to latest (standard time)
            println!(
                "Warning: Ambiguous time during DST transition. Using latest.",
            );
            Ok(latest.naive_utc())
        }
    }
}

User input handling requires explicit decisions for edge cases.

Comparing with year() month() day() etc.

use chrono::{TimeZone, Datelike, Timelike};
 
fn component_methods() {
    // chrono also provides builder-style methods
    
    // with_ymd_and_hms is convenient for complete construction
    let dt1 = chrono::Utc.with_ymd_and_hms(2024, 3, 15, 10, 30, 0);
    
    // Alternative: chain individual components
    let dt2 = chrono::Utc
        .with_ymd_and_hms(2024, 1, 1, 0, 0, 0)  // Start with something
        .single()
        .map(|dt| {
            dt.with_year(2024)
              .and_then(|dt| dt.with_month(3))
              .and_then(|dt| dt.with_day(15))
              .and_then(|dt| dt.with_hour(10))
              .and_then(|dt| dt.with_minute(30))
        });
    
    // with_ymd_and_hms returns LocalResult directly
    // with_* methods on DateTime return Option<DateTime>
    // They have different use cases:
    // - with_ymd_and_hms: Creating from scratch
    // - with_*: Modifying existing datetime
}

with_ymd_and_hms is for initial creation; component methods are for modification.

LocalResult and Error Handling

use chrono::{TimeZone, LocalResult};
use std::fmt;
 
// Custom error type for datetime conversion
#[derive(Debug)]
pub enum DateTimeError {
    NonExistentTime,
    AmbiguousTime { earliest: chrono::NaiveDateTime, latest: chrono::NaiveDateTime },
}
 
impl fmt::Display for DateTimeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DateTimeError::NonExistentTime => {
                write!(f, "DateTime doesn't exist (DST spring forward)")
            }
            DateTimeError::AmbiguousTime { earliest, latest } => {
                write!(f, "Ambiguous time (DST fall back): {} or {}", earliest, latest)
            }
        }
    }
}
 
impl std::error::Error for DateTimeError {}
 
// Helper function for strict conversion
fn strict_datetime(
    tz: &impl TimeZone,
    year: i32,
    month: u32,
    day: u32,
    hour: u32,
    min: u32,
    sec: u32,
) -> Result<chrono::DateTime<impl TimeZone>, DateTimeError> {
    match tz.with_ymd_and_hms(year, month, day, hour, min, sec) {
        LocalResult::Single(dt) => Ok(dt),
        LocalResult::None => Err(DateTimeError::NonExistentTime),
        LocalResult::Ambiguous(earliest, latest) => {
            Err(DateTimeError::AmbiguousTime {
                earliest: earliest.naive_local(),
                latest: latest.naive_local(),
            })
        }
    }
}

Explicit error types make timezone issues clear in APIs.

Timezone-Specific Behavior

use chrono::{TimeZone, FixedOffset};
 
fn fixed_offset_behavior() {
    // FixedOffset has no DST transitions
    // All times are unique
    let offset = FixedOffset::east_opt(-5 * 3600).unwrap(); // UTC-5
    
    // Always returns Single
    let result = offset.with_ymd_and_hms(2024, 3, 10, 2, 30, 0);
    assert!(matches!(result, LocalResult::Single(_)));
    
    // No ambiguous times, no non-existent times
    // This is true for any fixed offset
}
 
fn utc_behavior() {
    // UTC also has no DST transitions
    // Always returns Single
    
    let result = chrono::Utc.with_ymd_and_hms(2024, 3, 10, 2, 30, 0);
    assert!(matches!(result, LocalResult::Single(_)));
}

FixedOffset and Utc never produce None or Ambiguous results.

Practical Pattern: Fallback Strategy

use chrono::{TimeZone, LocalResult, NaiveDateTime};
 
fn with_fallback(
    tz: &impl TimeZone,
    naive: NaiveDateTime,
) -> chrono::DateTime<impl TimeZone> {
    // Try to convert naive datetime to timezone-aware
    // Use fallback strategy for edge cases
    
    match tz.from_local_datetime(&naive) {
        LocalResult::Single(dt) => dt,
        LocalResult::None => {
            // Spring forward: push forward by offset
            // This is what most systems do
            tz.from_utc_datetime(&naive)
        }
        LocalResult::Ambiguous(earliest, latest) => {
            // Fall back: use later time (standard time)
            // This is a common default
            latest
        }
    }
}
 
// Or with explicit control
fn with_explicit_ambiguous_strategy(
    tz: &impl TimeZone,
    naive: NaiveDateTime,
    strategy: AmbiguousStrategy,
) -> Result<chrono::DateTime<impl TimeZone>, String> {
    match tz.from_local_datetime(&naive) {
        LocalResult::Single(dt) => Ok(dt),
        LocalResult::None => Err("Non-existent time".to_string()),
        LocalResult::Ambiguous(earliest, latest) => {
            match strategy {
                AmbiguousStrategy::Earliest => Ok(earliest),
                AmbiguousStrategy::Latest => Ok(latest),
                AmbiguousStrategy::Reject => {
                    Err("Ambiguous time during DST transition".to_string())
                }
            }
        }
    }
}
 
enum AmbiguousStrategy {
    Earliest,  // Use first occurrence (DST)
    Latest,    // Use second occurrence (standard)
    Reject,    // Return error
}

Define explicit strategies for handling ambiguous times.

Real-World Example: Scheduled Events

use chrono::{TimeZone, LocalResult, NaiveDateTime, DateTime};
 
struct ScheduledEvent {
    name: String,
    scheduled_time: NaiveDateTime,
}
 
impl ScheduledEvent {
    // For scheduling, ambiguity is critical
    // A meeting at 1:30 AM during fall back could mean two things
    
    fn resolve_time(
        &self,
        tz: &impl TimeZone,
        ambiguous_pref: AmbiguousPreference,
    ) -> Result<DateTime<impl TimeZone>, String> {
        match tz.from_local_datetime(&self.scheduled_time) {
            LocalResult::Single(dt) => Ok(dt),
            LocalResult::None => {
                Err(format!(
                    "Event '{}' is scheduled during non-existent hour",
                    self.name
                ))
            }
            LocalResult::Ambiguous(earliest, latest) => {
                match ambiguous_pref {
                    AmbiguousPreference::First => Ok(earliest),
                    AmbiguousPreference::Second => Ok(latest),
                    AmbiguousPreference::Utc => {
                        // Convert to UTC and back to disambiguate
                        // Or require explicit user choice
                        Err(format!(
                            "Event '{}' has ambiguous time during DST transition",
                            self.name
                        ))
                    }
                }
            }
        }
    }
}
 
enum AmbiguousPreference {
    First,   // Use first occurrence
    Second,  // Use second occurrence
    Utc,     // Error, require explicit choice
}

Scheduling systems must handle ambiguity explicitly.

Testing DST Transitions

use chrono::{TimeZone, LocalResult};
 
fn test_dst_transitions() {
    // When testing code that handles DST transitions:
    
    // 1. Test with UTC (no DST)
    // 2. Test with FixedOffset (no DST)
    // 3. Test with timezone that has DST transitions
    
    // For actual timezone testing, you'd use a crate like tzfile
    // or chrono-tz for real timezone data
    
    // Example with a mock timezone behavior:
    fn test_ambiguous_handling() {
        // Simulate ambiguous result
        let ambiguous: LocalResult<i32> = LocalResult::Ambiguous(1, 2);
        
        assert_eq!(ambiguous.earliest(), Some(1));
        assert_eq!(ambiguous.latest(), Some(2));
        assert_eq!(ambiguous.single(), None);
        
        let single: LocalResult<i32> = LocalResult::Single(42);
        assert_eq!(single.single(), Some(42));
        assert_eq!(single.earliest(), Some(42));
        assert_eq!(single.latest(), Some(42));
        
        let none: LocalResult<i32> = LocalResult::None;
        assert_eq!(none.single(), None);
        assert_eq!(none.earliest(), None);
        assert_eq!(none.latest(), None);
    }
}

Test all three LocalResult variants explicitly.

Comparison with from_local_datetime

use chrono::{TimeZone, LocalResult, NaiveDateTime};
 
fn comparison_methods() {
    // with_ymd_and_hms: convenient for year/month/day/hour/min/sec
    // from_local_datetime: for NaiveDateTime
    
    let naive = NaiveDateTime::from_timestamp_opt(1700000000, 0).unwrap();
    
    // These are equivalent for creating from components:
    let result1 = chrono::Utc.with_ymd_and_hms(2023, 11, 14, 22, 13, 20);
    let result2 = chrono::Utc.from_local_datetime(&naive);
    
    // Both return LocalResult
    // from_local_datetime is more general (accepts any NaiveDateTime)
    // with_ymd_and_hms is more convenient (takes components)
    
    // from_local_datetime is useful when you already have NaiveDateTime
    fn from_naive(
        tz: &impl TimeZone,
        naive: NaiveDateTime,
    ) -> Result<DateTime<impl TimeZone>, String> {
        match tz.from_local_datetime(&naive) {
            LocalResult::Single(dt) => Ok(dt),
            LocalResult::None => Err("Non-existent time".into()),
            LocalResult::Ambiguous(_, _) => Err("Ambiguous time".into()),
        }
    }
    
    use chrono::DateTime;
}

from_local_datetime is the underlying method; with_ymd_and_hms is a convenience wrapper.

Summary

fn summary() {
    // ┌─────────────────────────────────────────────────────────────────────────┐
    // │ LocalResult Variant │ Meaning            │ Handling                    │
    // ├─────────────────────────────────────────────────────────────────────────┤
    // │ None                │ Time doesn't exist │ Adjust, error, or skip      │
    // │ Single              │ Unique valid time │ Use directly                │
    // │ Ambiguous           │ Two valid times   │ Choose earliest or latest    │
    // └─────────────────────────────────────────────────────────────────────────┘
    
    // Key points:
    // 1. with_ymd_and_hms returns LocalResult, not Option or Result
    // 2. Three cases: None, Single, Ambiguous
    // 3. None: spring forward skips times
    // 4. Ambiguous: fall back repeats times
    // 5. Single: normal, unique times
    // 6. UTC and FixedOffset never produce None or Ambiguous
    // 7. earliest/latest provide safe defaults for Ambiguous
    // 8. single() extracts value only when unique
    // 9. Forces explicit handling of DST edge cases
    // 10. Prevents subtle bugs from silent wrong choices
}

Key insight: with_ymd_and_hms returns LocalResult instead of a simple Option or Result because timezone transitions create a three-state problem: times can be unique (normal), non-existent (skipped during spring forward), or ambiguous (repeated during fall back). By returning a three-variant enum, chrono forces callers to acknowledge and handle all three cases, preventing subtle bugs where a meeting scheduled for 1:30 AM during a DST transition could silently end up at the wrong moment. UTC and fixed-offset timezones always produce Single results, making them safe choices when you want predictable datetime handling without DST complications.