Loading page…
Rust walkthroughs
Loading page…
chrono::NaiveDateTime::and_local_timezone handle ambiguous or non-existent local times during DST transitions?The and_local_timezone method on chrono::NaiveDateTime converts a naive datetime (one without timezone information) into a timezone-aware datetime by combining it with a specific timezone. During daylight saving time transitions, this conversion can encounter two edge cases: ambiguous times (when the same local time occurs twice during a "fall back" transition) and non-existent times (when a local time is skipped during a "spring forward" transition). Rather than panicking or silently choosing an interpretation, and_local_timezone returns a LocalResult enum that explicitly represents these cases—Single for unambiguous times, Ambiguous for times that could be either of two offsets, and None for times that don't exist in that timezone. This design forces developers to consciously decide how to handle DST edge cases.
use chrono::{NaiveDateTime, TimeZone, Utc, FixedOffset};
use chrono::offset::LocalResult;
fn basic_conversion() {
// Create a naive datetime (no timezone)
let naive: NaiveDateTime = "2024-06-15 12:00:00".parse().unwrap();
// Convert to UTC
let utc = naive.and_local_timezone(Utc);
assert!(matches!(utc, LocalResult::Single(_)));
// Convert to fixed offset timezone
let offset = FixedOffset::east_opt(5 * 3600).unwrap(); // UTC+5
let with_offset = naive.and_local_timezone(offset);
assert!(matches!(with_offset, LocalResult::Single(_)));
}Most conversions yield Single, indicating an unambiguous result.
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono::offset::AmbiguousOffset;
fn explain_local_result() {
// LocalResult has three variants:
// - Single<T>: Unique, unambiguous result
// - Ambiguous<T>: Two possible offsets (DST fall-back)
// - None<T>: No valid result (DST spring-forward gap)
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
// In US Eastern timezone, this time doesn't exist
// (clocks spring forward from 2:00 AM to 3:00 AM)
let eastern = chrono_tz::US::Eastern;
let result = naive.and_local_timezone(eastern);
match result {
LocalResult::Single(dt) => println!("Unambiguous: {}", dt),
LocalResult::Ambiguous(earlier, later) => {
println!("Ambiguous: {} or {}", earlier, later);
}
LocalResult::None => println!("Non-existent time"),
}
}LocalResult captures all possible outcomes of timezone conversion.
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::US::Eastern;
fn spring_forward_gap() {
// US Eastern timezone, March 2024 DST transition
// Clocks jump from 2:00 AM to 3:00 AM
// Times between 2:00 AM and 3:00 AM don't exist
// 2:30 AM on March 10, 2024 doesn't exist in US Eastern
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
let result = naive.and_local_timezone(Eastern);
match result {
LocalResult::None => {
println!("This local time doesn't exist!");
println!("Clocks jumped from 2:00 AM to 3:00 AM");
}
_ => unreachable!(),
}
// 1:30 AM exists (before transition)
let before_transition: NaiveDateTime = "2024-03-10 01:30:00".parse().unwrap();
assert!(matches!(
before_transition.and_local_timezone(Eastern),
LocalResult::Single(_)
));
// 3:00 AM exists (after transition)
let after_transition: NaiveDateTime = "2024-03-10 03:00:00".parse().unwrap();
assert!(matches!(
after_transition.and_local_timezone(Eastern),
LocalResult::Single(_)
));
}Non-existent times occur when clocks spring forward, creating a gap.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime};
use chrono_tz::US::Eastern;
fn fall_back_ambiguous() {
// US Eastern timezone, November 2024 DST transition
// Clocks fall back from 2:00 AM to 1:00 AM
// 1:30 AM occurs twice: before and after transition
let naive: NaiveDateTime = "2024-11-03 01:30:00".parse().unwrap();
let result = naive.and_local_timezone(Eastern);
match result {
LocalResult::Ambiguous(earlier, later) => {
println!("Ambiguous time!");
println!("Earlier occurrence: {} (offset: {:?})",
earlier, earlier.offset());
println!("Later occurrence: {} (offset: {:?})",
later, later.offset());
// Earlier: EDT (UTC-4)
// Later: EST (UTC-5)
assert!(earlier.offset().local_minus_utc() == -4 * 3600);
assert!(later.offset().local_minus_utc() == -5 * 3600);
}
_ => unreachable!(),
}
// 2:00 AM is unambiguous (only occurs in EST)
let unambiguous: NaiveDateTime = "2024-11-03 02:00:00".parse().unwrap();
assert!(matches!(
unambiguous.and_local_timezone(Eastern),
LocalResult::Single(_)
));
}Ambiguous times occur when clocks fall back, causing the same local time to occur twice.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime};
use chrono_tz::Tz;
use chrono::offset::AmbiguousOffset;
fn resolve_ambiguity() {
let naive: NaiveDateTime = "2024-11-03 01:30:00".parse().unwrap();
let tz = chrono_tz::US::Eastern;
let result = naive.and_local_timezone(tz);
// Option 1: Choose earlier (EDT - daylight time)
let earlier = result.earliest().unwrap();
println!("Earlier (EDT): {}", earlier);
// Option 2: Choose later (EST - standard time)
let later = result.latest().unwrap();
println!("Later (EST): {}", later);
// Option 3: Explicit matching for custom resolution
let resolved = match result {
LocalResult::Single(dt) => dt,
LocalResult::Ambiguous(earlier, later) => {
// Choose based on business rules
// Default to standard time (later occurrence)
later
}
LocalResult::None => {
panic!("Cannot resolve non-existent time");
}
};
// Option 4: Use single() which returns None for ambiguous
let single = result.single();
assert!(single.is_none());
}Multiple strategies exist for resolving ambiguous times.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime, Duration};
use chrono_tz::Tz;
fn handle_nonexistent_time() {
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
let tz = chrono_tz::US::Eastern;
let result = naive.and_local_timezone(tz);
// single() returns None for non-existent times
assert!(result.single().is_none());
// earliest() and latest() also return None
assert!(result.earliest().is_none());
assert!(result.latest().is_none());
// Common strategy: adjust forward to next valid time
let resolved = match result {
LocalResult::Single(dt) => dt,
LocalResult::Ambiguous(earliest, _latest) => earliest,
LocalResult::None => {
// Move forward to next valid time
// During spring forward, add 1 hour to skip the gap
let adjusted = naive + Duration::hours(1);
adjusted.and_local_timezone(tz).single().unwrap()
}
};
println!("Adjusted to: {}", resolved);
// Alternative: move backward
let resolved_backward = match result {
LocalResult::Single(dt) => dt,
LocalResult::Ambiguous(earliest, _latest) => earliest,
LocalResult::None => {
let adjusted = naive - Duration::hours(1);
adjusted.and_local_timezone(tz).single().unwrap()
}
};
}Non-existent times require adjustment to a valid time.
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::US::Eastern;
fn localresult_methods() {
// single() - Returns Some for unambiguous, None for ambiguous/non-existent
let normal_time: NaiveDateTime = "2024-06-15 12:00:00".parse().unwrap();
let single_result = normal_time.and_local_timezone(Eastern);
let single_dt = single_result.single(); // Some(DateTime)
// earliest() - Returns earliest for ambiguous, Some for single, None for non-existent
let ambiguous_time: NaiveDateTime = "2024-11-03 01:30:00".parse().unwrap();
let ambiguous_result = ambiguous_time.and_local_timezone(Eastern);
let earliest_dt = ambiguous_result.earliest(); // Some(DateTime - EDT)
// latest() - Returns latest for ambiguous, Some for single, None for non-existent
let latest_dt = ambiguous_result.latest(); // Some(DateTime - EST)
// map() - Transform the contained value(s)
let mapped = ambiguous_result.map(|dt| dt.timestamp());
// unwrap() - Panics on None or Ambiguous
// Only use when you're certain the time is unambiguous
let unwrapped = normal_time.and_local_timezone(Eastern).unwrap();
}LocalResult provides multiple methods for extracting values safely.
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::{Europe::London, Australia::Sydney, US::Eastern};
fn different_timezones() {
// Different timezones have different DST transitions
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
// US Eastern: non-existent (spring forward)
let us_result = naive.and_local_timezone(Eastern);
assert!(matches!(us_result, LocalResult::None));
// London: different transition date
// UK DST transitions on last Sunday of March
let uk_result = naive.and_local_timezone(London);
// March 10 is before UK transition, so it exists
assert!(matches!(uk_result, LocalResult::Single(_)));
// Sydney: DST transitions are different (Southern Hemisphere)
// Sydney's autumn is March-May, spring is September-November
let au_result = naive.and_local_timezone(Sydney);
assert!(matches!(au_result, LocalResult::Single(_)));
}
fn different_dates() {
// UK spring forward: March 31, 2024 (last Sunday)
let uk_gap: NaiveDateTime = "2024-03-31 01:30:00".parse().unwrap();
assert!(matches!(
uk_gap.and_local_timezone(chrono_tz::Europe::London),
LocalResult::None
));
// Australia fall back: April 7, 2024 (first Sunday)
let au_ambiguous: NaiveDateTime = "2024-04-07 02:30:00".parse().unwrap();
// In Sydney, this would be during fall-back transition
}DST transition dates vary by timezone and hemisphere.
use chrono::{NaiveDateTime, TimeZone, FixedOffset, LocalResult};
fn fixed_offset_no_dst() {
// FixedOffset represents a constant offset from UTC
// No DST transitions, so no ambiguity or gaps
let offset = FixedOffset::east_opt(5 * 3600).unwrap(); // UTC+5
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
// Always single result for fixed offsets
let result = naive.and_local_timezone(offset);
assert!(matches!(result, LocalResult::Single(_)));
// No DST means every time exists and is unambiguous
let any_time: NaiveDateTime = "2024-11-03 01:30:00".parse().unwrap();
let any_result = any_time.and_local_timezone(offset);
assert!(matches!(any_result, LocalResult::Single(_)));
}Fixed offsets never have DST, avoiding ambiguity and gaps.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime, Duration};
use chrono_tz::Tz;
/// Converts naive datetime to timezone-aware, preferring later time for ambiguity
fn to_timezone_ambiguous_later(naive: NaiveDateTime, tz: Tz) -> Option<DateTime<Tz>> {
match naive.and_local_timezone(tz) {
LocalResult::Single(dt) => Some(dt),
LocalResult::Ambiguous(_, later) => Some(later),
LocalResult::None => None,
}
}
/// Converts with fallback to next valid time for non-existent
fn to_timezone_strict_forward(naive: NaiveDateTime, tz: Tz) -> DateTime<Tz> {
match naive.and_local_timezone(tz) {
LocalResult::Single(dt) => dt,
LocalResult::Ambiguous(earliest, _) => earliest,
LocalResult::None => {
// Find next valid time by advancing
let mut current = naive + Duration::minutes(15);
loop {
if let LocalResult::Single(dt) = current.and_local_timezone(tz) {
return dt;
}
current = current + Duration::minutes(15);
}
}
}
}
/// Converts with error reporting
#[derive(Debug)]
enum ConversionError {
Ambiguous { earlier: DateTime<Tz>, later: DateTime<Tz> },
NonExistent,
}
fn to_timezone_checked(naive: NaiveDateTime, tz: Tz) -> Result<DateTime<Tz>, ConversionError> {
match naive.and_local_timezone(tz) {
LocalResult::Single(dt) => Ok(dt),
LocalResult::Ambiguous(earlier, later) => {
Err(ConversionError::Ambiguous { earlier, later })
}
LocalResult::None => Err(ConversionError::NonExistent),
}
}Custom conversion functions encode policy decisions for edge cases.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime, Duration};
use chrono_tz::Tz;
#[derive(Debug)]
struct ScheduledEvent {
name: String,
datetime: DateTime<Tz>,
}
fn schedule_event(
name: String,
naive_time: NaiveDateTime,
timezone: Tz,
) -> Result<ScheduledEvent, String> {
match naive_time.and_local_timezone(timezone) {
LocalResult::Single(dt) => {
Ok(ScheduledEvent { name, datetime: dt })
}
LocalResult::Ambiguous(earlier, later) => {
// Ambiguous time during fall-back
// Policy: Use the earlier (daylight time) occurrence
Ok(ScheduledEvent {
name,
datetime: earlier
})
}
LocalResult::None => {
// Non-existent time during spring-forward
// Policy: Reject and require user to choose valid time
Err(format!(
"The time {} doesn't exist in {} (DST spring-forward gap)",
naive_time, timezone
))
}
}
}
fn schedule_with_adjustment(
name: String,
naive_time: NaiveDateTime,
timezone: Tz,
) -> ScheduledEvent {
// Automatically adjust invalid times
match naive_time.and_local_timezone(timezone) {
LocalResult::Single(dt) => ScheduledEvent { name, datetime: dt },
LocalResult::Ambiguous(_, later) => {
// Use later (standard time) for safety
ScheduledEvent { name, datetime: later }
}
LocalResult::None => {
// Skip forward to next valid time
let adjusted = naive_time + Duration::hours(1);
let dt = adjusted.and_local_timezone(timezone).single().unwrap();
ScheduledEvent { name, datetime: dt }
}
}
}Scheduling systems must define policies for DST edge cases.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime};
use chrono_tz::Tz;
fn parse_local_timestamp(input: &str, timezone: Tz) -> Result<DateTime<Tz>, String> {
// Parse user input as naive datetime
let naive: NaiveDateTime = input.parse()
.map_err(|e| format!("Invalid datetime format: {}", e))?;
// Convert to timezone-aware
match naive.and_local_timezone(timezone) {
LocalResult::Single(dt) => Ok(dt),
LocalResult::Ambiguous(earlier, later) => {
Err(format!(
"Ambiguous time during DST transition. \
Could be {} (EDT) or {} (EST). Please specify timezone offset.",
earlier, later
))
}
LocalResult::None => {
Err(format!(
"Time {} doesn't exist in {} due to DST spring-forward",
naive, timezone
))
}
}
}
fn parse_with_fallback(input: &str, timezone: Tz) -> Result<DateTime<Tz>, String> {
let naive: NaiveDateTime = input.parse()
.map_err(|e| format!("Invalid datetime: {}", e))?;
// Fallback strategy: prefer standard time for ambiguous
// Skip forward for non-existent
Ok(match naive.and_local_timezone(timezone) {
LocalResult::Single(dt) => dt,
LocalResult::Ambiguous(_earlier, later) => later, // Use standard time
LocalResult::None => {
// Move to next hour
let adjusted = naive + chrono::Duration::hours(1);
adjusted.and_local_timezone(timezone).single().unwrap()
}
})
}User input parsing requires handling all three LocalResult cases.
use chrono::{NaiveDateTime, TimeZone, LocalResult, Datelike, Timelike};
use chrono_tz::{US::Eastern, Tz};
fn check_dst_status(datetime: &DateTime<Tz>) -> &'static str {
let offset = datetime.offset().local_minus_utc();
// Eastern timezone: EDT is UTC-4, EST is UTC-5
match offset {
-4 * 3600 => "Daylight Time (EDT)",
-5 * 3600 => "Standard Time (EST)",
_ => "Unknown offset",
}
}
fn find_next_transition(from: DateTime<Tz>) -> Option<DateTime<Tz>> {
// DST typically happens at 2:00 AM local time
// Check the next few days for a transition
let mut check_time = from;
for _ in 0..365 {
check_time = check_time + chrono::Duration::days(1);
// Check around 2:00 AM for gaps or ambiguity
let naive = check_time.naive_local();
let hour_check = naive.with_hour(2)?;
if let LocalResult::None | LocalResult::Ambiguous(_, _) =
hour_check.and_local_timezone(from.timezone()) {
return Some(check_time);
}
}
None
}Proactive checking can warn users about upcoming DST transitions.
LocalResult variants:
| Variant | Meaning | .single() | .earliest() | .latest() |
|---------|---------|-------------|---------------|-------------|
| Single(T) | Unambiguous time | Some(T) | Some(T) | Some(T) |
| Ambiguous(T, T) | Two valid times | None | Some(first) | Some(second) |
| None | Non-existent time | None | None | None |
Common resolution strategies:
| Strategy | Ambiguous | Non-Existent | |----------|-----------|--------------| | Reject | Return error | Return error | | Prefer earlier | Use first (EDT) | N/A | | Prefer later | Use second (EST) | N/A | | Adjust forward | N/A | Skip to next valid | | Adjust backward | N/A | Use previous valid |
Key insight: and_local_timezone returns LocalResult because timezone conversions during DST transitions have three possible outcomes. A naive datetime like "2024-11-03 01:30:00" in US Eastern might refer to 1:30 AM before the fall-back (EDT, UTC-4) or after (EST, UTC-5)—these are genuinely different moments in time. Conversely, "2024-03-10 02:30:00" never existed because clocks jumped from 2:00 to 3:00. By encoding these cases in LocalResult, chrono forces explicit handling: developers must choose whether to prefer earlier/later times for ambiguity, and how to adjust for non-existent times. This prevents silent bugs where scheduled events mysteriously shift by an hour or disappear. The pattern extends beyond DST—any timezone with historical offset changes (political decisions, war-time adjustments) can produce similar edge cases. Safe datetime handling means always matching on LocalResult or using convenience methods like .single() with explicit fallback logic for the ambiguous and non-existent cases.