Loading pageā¦
Rust walkthroughs
Loading pageā¦
chrono, how do you properly handle timezone-aware DateTime conversions to avoid panics?Timezone conversions in chrono can panic when datetime values are invalid or ambiguous during timezone transitions. Understanding these edge cases and using the proper APIs prevents runtime crashes and handles real-world datetime scenarios correctly.
Timezones have transitions that create gaps and overlaps:
use chrono::{DateTime, TimeZone, Utc, FixedOffset};
fn demonstrate_gap() {
// Daylight Saving Time spring forward creates a gap
// For example, in US/Eastern on 2024-03-10:
// 2:00 AM becomes 3:00 AM, so 2:30 AM never exists
// Naive approach - this can panic!
let eastern = chrono_tz::US::Eastern;
// This time doesn't exist:
// eastern.with_ymd_and_hms(2024, 3, 10, 2, 30, 0).unwrap();
// panic: No such local time
}The gap occurs when clocks spring forward for daylight saving time, and overlaps occur when clocks fall back.
chrono distinguishes between naive and timezone-aware types:
use chrono::{NaiveDateTime, NaiveDate, NaiveTime, DateTime, Utc, TimeZone};
fn naive_vs_aware() {
// Naive datetime - no timezone, can be ambiguous
let naive: NaiveDateTime = "2024-06-15 14:30:00".parse().unwrap();
// This represents a moment in time but doesn't specify which timezone
// Converting to a specific timezone can fail or be ambiguous
// Timezone-aware datetime - unambiguous moment in time
let aware: DateTime<Utc> = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
// This represents a specific moment (UTC 14:30 on June 15, 2024)
// Converting to other timezones is always valid
}Naive datetimes lack timezone information, making conversions potentially problematic.
Conversions from naive to timezone-aware times return LocalResult:
use chrono::{NaiveDateTime, TimeZone, Utc, LocalResult};
use chrono_tz::US::Eastern;
fn local_result_variants() {
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
match Eastern.with_ymd_and_hms(2024, 3, 10, 2, 30, 0) {
LocalResult::None => {
// Time doesn't exist (gap during DST spring forward)
println!("No such local time - the clocks jumped forward");
}
LocalResult::Single(dt) => {
// Unambiguous time
println!("Unique time: {}", dt);
}
LocalResult::Ambiguous(earliest, latest) => {
// Time occurs twice (overlap during DST fall back)
println!("Ambiguous time:");
println!(" First occurrence: {} (DST)", earliest);
println!(" Second occurrence: {} (standard)", latest);
}
}
}The LocalResult enum captures all three possibilities for local time conversion.
chrono's single-result conversion methods can panic:
use chrono::{TimeZone, Utc};
use chrono_tz::US::Eastern;
fn panic_scenarios() {
// This is fine - the time exists
let valid = Eastern.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
// This panics! The time doesn't exist (DST gap)
// let invalid = Eastern.with_ymd_and_hms(2024, 3, 10, 2, 30, 0).unwrap();
// thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'
// unwrap() treats LocalResult::None as None and panics
// LocalResult::Ambiguous returns just the latest time
}Using unwrap() on LocalResult loses information and can panic.
When a time doesn't exist due to DST, you must decide how to handle it:
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::US::Eastern;
fn handle_gap() {
let naive: NaiveDateTime = "2024-03-10 02:30:00".parse().unwrap();
match Eastern.from_local_datetime(&naive) {
LocalResult::None => {
// Option 1: Use the next valid time
let next_valid = Eastern.from_local_datetime(&naive)
.earliest()
.unwrap();
println!("Using next valid time: {}", next_valid);
// Option 2: Use a different timezone (UTC) then convert
let utc_time = Utc.from_utc_datetime(&naive);
println!("Interpreted as UTC: {}", utc_time);
}
LocalResult::Single(dt) => println!("Valid time: {}", dt),
LocalResult::Ambiguous(early, late) => {
println!("Ambiguous - using latest: {}", late);
}
}
}The earliest() and latest() methods on LocalResult help choose consistent behavior.
During fall-back transitions, times can occur twice:
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::US::Eastern;
fn handle_overlap() {
// In US/Eastern, 2024-11-03 01:30:00 occurs twice
// Once in EDT (UTC-4), once in EST (UTC-5)
match Eastern.with_ymd_and_hms(2024, 11, 3, 1, 30, 0) {
LocalResult::Ambiguous(earliest, latest) => {
// earliest = 01:30 EDT (UTC-4) = 05:30 UTC
// latest = 01:30 EST (UTC-5) = 06:30 UTC
println!("Earliest (DST): {} offset {}", earliest, earliest.offset());
println!("Latest (standard): {} offset {}", latest, latest.offset());
// Choose based on your application's needs:
// - Use earliest if you expect DST time
// - Use latest if you expect standard time
// - Ask the user to disambiguate
}
_ => unreachable!("This time should be ambiguous"),
}
}The one-hour difference matters for applications that care about exact timing.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime};
use chrono_tz::{US::Eastern, Tz};
fn safe_conversion(
naive: NaiveDateTime,
tz: Tz,
) -> Result<DateTime<Tz>, String> {
match tz.from_local_datetime(&naive) {
LocalResult::Single(dt) => Ok(dt),
LocalResult::None => {
Err(format!(
"Time {} doesn't exist in timezone {} (DST gap)",
naive, tz
))
}
LocalResult::Ambiguous(early, late) => {
Err(format!(
"Time {} is ambiguous in {} (occurs at {} and {})",
naive, tz, early, late
))
}
}
}use chrono::{DateTime, NaiveDateTime, Utc, TimeZone};
fn utc_first_approach(naive: NaiveDateTime) -> DateTime<Utc> {
// Store/convert to UTC first, then display in local timezone
let utc: DateTime<Utc> = Utc.from_utc_datetime(&naive);
utc
}
fn display_in_timezone(utc: DateTime<Utc>, tz: &str) -> String {
let tz: chrono_tz::Tz = tz.parse().unwrap();
utc.with_timezone(&tz).format("%Y-%m-%d %H:%M:%S %Z").to_string()
}UTC has no DST transitions, so conversions to/from UTC are always valid.
use chrono::{NaiveDateTime, TimeZone, LocalResult, DateTime};
use chrono_tz::Tz;
enum AmbiguityResolution {
Earliest, // Prefer DST time during fall-back
Latest, // Prefer standard time during fall-back
}
fn convert_with_preference(
naive: NaiveDateTime,
tz: Tz,
resolution: AmbiguityResolution,
) -> Option<DateTime<Tz>> {
match tz.from_local_datetime(&naive) {
LocalResult::Single(dt) => Some(dt),
LocalResult::None => None, // Gap - caller must handle
LocalResult::Ambiguous(early, late) => {
match resolution {
AmbiguityResolution::Earliest => Some(early),
AmbiguityResolution::Latest => Some(late),
}
}
}
}Converting from one timezone to another is always safe:
use chrono::{DateTime, TimeZone};
use chrono_tz::{US::Eastern, US::Pacific, Tz};
fn safe_timezone_conversion() {
// Once you have a timezone-aware DateTime, converting is always valid
let eastern_time: DateTime<Tz> = Eastern.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
// This conversion is always safe - it's the same instant in time
let pacific_time = eastern_time.with_timezone(&Pacific);
let utc_time = eastern_time.with_timezone(&chrono_tz::UTC);
println!("Eastern: {}", eastern_time);
println!("Pacific: {}", pacific_time);
println!("UTC: {}", utc_time);
// All represent the same instant: 2024-06-15 18:30 UTC
}The danger only exists when converting from naive datetimes to timezone-aware datetimes.
The chrono-tz crate provides comprehensive timezone support:
use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::{US::Eastern, Europe::London, Tz};
fn chrono_tz_example() {
// Parse timezone from string
let tz: Tz = "America/New_York".parse().unwrap();
// Use timezone in conversion
let naive = NaiveDateTime::from_timestamp_opt(1700000000, 0).unwrap();
match tz.from_local_datetime(&naive) {
LocalResult::Single(dt) => println!("Time in {}: {}", tz, dt),
_ => println!("Ambiguous or invalid time"),
}
// Named timezone constants
let ny_time = Eastern.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
let london_time = London.with_ymd_and_hms(2024, 6, 15, 19, 30, 0).unwrap();
}When you know the UTC offset explicitly:
use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone, Utc};
fn fixed_offset_example() {
// UTC-5 (Eastern Standard Time)
let est = FixedOffset::west_opt(5 * 3600).unwrap();
// UTC+5:30 (India Standard Time)
let ist = FixedOffset::east_opt((5 * 3600) + (30 * 60)).unwrap();
// Fixed offsets don't have DST transitions
// Conversions are always valid
let naive = NaiveDateTime::from_timestamp_opt(1700000000, 0).unwrap();
let est_time = est.from_utc_datetime(&naive);
let ist_time = ist.from_utc_datetime(&naive);
// These conversions are always valid - no DST
}FixedOffset has no DST transitions, so conversions are always unambiguous.
use chrono::{NaiveDateTime, NaiveDate, NaiveTime, DateTime, TimeZone, LocalResult};
use chrono_tz::Tz;
fn parse_user_datetime(
date_str: &str,
time_str: &str,
timezone_str: &str,
) -> Result<DateTime<Tz>, DateTimeError> {
let date: NaiveDate = date_str.parse()
.map_err(|_| DateTimeError::InvalidDate)?;
let time: NaiveTime = time_str.parse()
.map_err(|_| DateTimeError::InvalidTime)?;
let naive = date.and_time(time);
let tz: Tz = timezone_str.parse()
.map_err(|_| DateTimeError::InvalidTimezone)?;
match tz.from_local_datetime(&naive) {
LocalResult::Single(dt) => Ok(dt),
LocalResult::None => Err(DateTimeError::Gap {
naive,
timezone: timezone_str.to_string(),
}),
LocalResult::Ambiguous(early, late) => Err(DateTimeError::Ambiguous {
naive,
timezone: timezone_str.to_string(),
early,
late,
}),
}
}
#[derive(Debug)]
enum DateTimeError {
InvalidDate,
InvalidTime,
InvalidTimezone,
Gap {
naive: NaiveDateTime,
timezone: String,
},
Ambiguous {
naive: NaiveDateTime,
timezone: String,
early: DateTime<Tz>,
late: DateTime<Tz>,
},
}use chrono::{DateTime, Utc, TimeZone};
use chrono_tz::Tz;
fn parse_with_timezone() {
// Parse with explicit timezone
let dt: DateTime<Tz> = "2024-06-15 14:30:00 America/New_York"
.parse()
.unwrap();
// Parse UTC (always safe)
let utc: DateTime<Utc> = "2024-06-15T14:30:00Z".parse().unwrap();
// Parse with offset
let with_offset: DateTime<chrono::FixedOffset> =
"2024-06-15T14:30:00-05:00".parse().unwrap();
// Convert to timezone
let in_eastern = with_offset.with_timezone(&chrono_tz::US::Eastern);
}use chrono::{NaiveDateTime, TimeZone, LocalResult};
use chrono_tz::US::Eastern;
fn debug_timezone_transitions() {
// Check if a time is valid before using it
fn is_valid_time(naive: NaiveDateTime, tz: chrono_tz::Tz) -> bool {
matches!(tz.from_local_datetime(&naive), LocalResult::Single(_))
}
// Find the next valid time after a gap
fn next_valid_time(
mut naive: NaiveDateTime,
tz: chrono_tz::Tz,
) -> DateTime<chrono_tz::Tz> {
loop {
match tz.from_local_datetime(&naive) {
LocalResult::Single(dt) => return dt,
LocalResult::Ambiguous(early, _) => return early,
LocalResult::None => {
// Advance by 1 minute and try again
naive = naive + chrono::Duration::minutes(1);
}
}
}
}
}Timezone-aware datetime conversions can fail due to DST gaps and overlaps. The key to avoiding panics is:
Never use unwrap() on LocalResult. Always handle all three cases: Single, None, and Ambiguous.
Convert to UTC first. Store and process datetimes in UTC, converting to local timezones only for display. UTC has no DST transitions.
Use chrono-tz for timezone database support. It provides proper handling of historical timezone transitions.
When accepting user input in local time, validate and disambiguate. For gaps, either reject the input or adjust to a valid time. For overlaps, either ask the user or pick a consistent default.
FixedOffset is always safe for conversions because it has no DST.
The LocalResult type is your friendāit explicitly represents all the ways a local time can map to a timezone-aware instant. Match on it explicitly rather than using convenience methods that panic, and your datetime handling will be robust across all timezone transitions.