Loading page…
Rust walkthroughs
Loading page…
chrono::DateTime::from_utc differ from with_timezone for timezone conversion edge cases?chrono::DateTime::from_utc constructs a DateTime from a naive UTC datetime and a timezone, treating the input as authoritative—it assumes the UTC timestamp is valid and applies timezone offset rules to produce the local representation. In contrast, with_timezone converts an existing DateTime between timezones while preserving the underlying UTC instant, which handles edge cases differently: for ambiguous times (like during DST fallback), with_timezone preserves the original UTC instant, while from_utc on ambiguous local times may produce unexpected results because it doesn't know which occurrence of the ambiguous time was intended. The key distinction is direction and authority: from_utc starts from UTC and applies a timezone (no ambiguity), while with_timezone converts between timezones by going through UTC internally, which ensures the instant is preserved even when the local time representation is ambiguous.
use chrono::{DateTime, TimeZone, Utc, FixedOffset, NaiveDateTime};
fn from_utc_basic() {
let naive_utc = NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
// Construct DateTime from UTC + timezone
let dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_utc);
// dt represents 2024-01-15 12:00:00 UTC
let offset = FixedOffset::east_opt(5 * 3600).unwrap(); // +05:00
let dt_offset: DateTime<FixedOffset> = offset.from_utc_datetime(&naive_utc);
// Same instant, displayed as 2024-01-15 17:00:00 +05:00
}from_utc takes a naive UTC datetime and creates a DateTime in the specified timezone.
use chrono::{DateTime, Utc, FixedOffset, TimeZone};
fn with_timezone_basic() {
// Start with UTC datetime
let utc_dt: DateTime<Utc> = "2024-01-15T12:00:00Z".parse().unwrap();
// Convert to another timezone
let offset = FixedOffset::east_opt(5 * 3600).unwrap();
let offset_dt: DateTime<FixedOffset> = utc_dt.with_timezone(&offset);
// offset_dt represents same instant, displayed as 2024-01-15 17:00:00 +05:00
// The UTC instant is preserved
assert_eq!(utc_dt.timestamp(), offset_dt.timestamp());
}with_timezone converts an existing DateTime to another timezone, preserving the instant.
use chrono::{DateTime, Utc, FixedOffset, NaiveDateTime, TimeZone};
fn authority_difference() {
// from_utc: UTC is authoritative
// "I have a UTC timestamp, what does it look like in timezone X?"
let naive_utc = NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
let offset = FixedOffset::east_opt(5 * 3600).unwrap();
let dt = offset.from_utc_datetime(&naive_utc);
// UTC timestamp is the truth, timezone just displays it differently
// with_timezone: Original instant is authoritative
// "I have this instant, what's the time in timezone Y?"
let utc_dt: DateTime<Utc> = "2024-01-15T12:00:00Z".parse().unwrap();
let offset_dt = utc_dt.with_timezone(&offset);
// The instant (timestamp) is preserved, timezone changes display
}from_utc trusts the UTC timestamp; with_timezone preserves the instant.
use chrono::{DateTime, Utc, FixedOffset, NaiveDateTime, TimeZone};
use chrono_tz::Tz; // Requires chrono-tz crate
fn dst_fallback_ambiguous() {
// During DST fallback, the same local time occurs twice
// Example: 2024-11-03 01:30 in America/New_York
// This time exists twice:
// - First: EDT (UTC-4)
// - Second: EST (UTC-5)
let tz: Tz = "America/New_York".parse().unwrap();
// with_timezone: Preserves the UTC instant
let utc_dt: DateTime<Utc> = "2024-11-03T05:30:00Z".parse().unwrap(); // 01:30 EDT
let ny_dt = utc_dt.with_timezone(&tz);
// Result: 2024-11-03 01:30:00 EDT (unambiguous because we started from UTC)
let utc_dt2: DateTime<Utc> = "2024-11-03T06:30:00Z".parse().unwrap(); // 01:30 EST
let ny_dt2 = utc_dt2.with_timezone(&tz);
// Result: 2024-11-03 01:30:00 EST (different UTC instant)
// These represent different instants, even though local time looks similar
assert_ne!(utc_dt.timestamp(), utc_dt2.timestamp());
}with_timezone is unambiguous because it preserves the UTC instant.
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
use chrono_tz::Tz;
fn from_utc_unambiguous() {
let tz: Tz = "America/New_York".parse().unwrap();
// from_utc is never ambiguous because UTC has no DST
let naive_utc = NaiveDateTime::parse_from_str("2024-11-03 05:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
let dt = Utc.from_utc_datetime(&naive_utc);
let ny_dt = dt.with_timezone(&tz);
// Unambiguous: we started from UTC, which has no DST ambiguity
// The issue is if you try to go the other direction:
// from_local with ambiguous local time
let naive_local = NaiveDateTime::parse_from_str("2024-11-03 01:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
// This is ambiguous - is it EDT or EST?
// tz.from_local_datetime(&naive_local) returns Ambiguous result
let result = tz.from_local_datetime(&naive_local);
match result {
chrono::LocalResult::Ambiguous(earliest, latest) => {
// Could be either EDT or EST
println!("Ambiguous: {:?} or {:?}", earliest, latest);
}
chrono::LocalResult::Single(dt) => {
println!("Unambiguous: {:?}", dt);
}
chrono::LocalResult::None => {
println!("Nonexistent time (during DST spring forward)");
}
}
}from_utc avoids ambiguity because UTC has no DST; local-to-UTC conversion encounters ambiguity.
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
use chrono_tz::Tz;
fn nonexistent_time() {
let tz: Tz = "America/New_York".parse().unwrap();
// During spring forward, 02:00-03:00 doesn't exist
// Clocks jump from 01:59:59 EST to 03:00:00 EDT
let naive_local = NaiveDateTime::parse_from_str("2024-03-10 02:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
// from_local returns None for nonexistent time
match tz.from_local_datetime(&naive_local) {
chrono::LocalResult::None => {
println!("Time doesn't exist: 02:30 is skipped during DST transition");
}
_ => unreachable!(),
}
// with_timezone is always valid - we start from valid UTC
let utc_dt: DateTime<Utc> = "2024-03-10T07:30:00Z".parse().unwrap();
let ny_dt = utc_dt.with_timezone(&tz);
// Valid: UTC instant exists, local display adjusts
}with_timezone never produces nonexistent times because UTC is always valid.
use chrono::{DateTime, Utc, TimeZone};
use chrono_tz::Tz;
fn timezone_to_timezone() {
let ny_tz: Tz = "America/New_York".parse().unwrap();
let tokyo_tz: Tz = "Asia/Tokyo".parse().unwrap();
// Start with New York time
let ny_dt: DateTime<Tz> = "2024-01-15T12:00:00-05:00".parse().unwrap();
// with_timezone converts to Tokyo time
let tokyo_dt = ny_dt.with_timezone(&tokyo_tz);
// Result: 2024-01-16 02:00:00 +09:00 (same instant)
// Internally, this goes through UTC:
// ny_dt -> UTC -> tokyo_dt
assert_eq!(ny_dt.timestamp(), tokyo_dt.timestamp());
// with_timezone handles all edge cases correctly
// because UTC is the intermediate representation
}with_timezone converts through UTC internally, preserving instants correctly.
use chrono::{DateTime, Utc, FixedOffset, NaiveDateTime, TimeZone};
fn same_result_different_path() {
let offset = FixedOffset::east_opt(5 * 3600).unwrap(); // +05:00
// Path 1: from_utc
let naive_utc = NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
let dt1: DateTime<FixedOffset> = offset.from_utc_datetime(&naive_utc);
// Path 2: with_timezone (from equivalent UTC DateTime)
let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_utc);
let dt2: DateTime<FixedOffset> = utc_dt.with_timezone(&offset);
// These represent the same instant
assert_eq!(dt1.timestamp(), dt2.timestamp());
// But the paths are different:
// dt1: naive UTC -> offset DateTime directly
// dt2: naive UTC -> Utc DateTime -> offset DateTime
// For simple fixed offsets, results are identical
// For DST-aware timezones, the paths matter
}For fixed offsets, both paths produce identical results; DST-aware timezones differ.
use chrono::{DateTime, Utc, FixedOffset, TimeZone};
use chrono_tz::Tz;
fn fixed_vs_dst() {
// FixedOffset: No DST transitions, always same offset
let fixed: FixedOffset = FixedOffset::east_opt(5 * 3600).unwrap();
// from_utc is straightforward - no ambiguity possible
let naive = chrono::NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
let dt_fixed = fixed.from_utc_datetime(&naive);
// Always 2024-01-15 17:00:00 +05:00
// DST-aware timezone: Offset changes based on date
let tz: Tz = "America/New_York".parse().unwrap();
// from_utc still works - UTC is unambiguous
let dt_dst = Utc.from_utc_datetime(&naive).with_timezone(&tz);
// Correctly applies DST offset for that date
// The key difference:
// - FixedOffset: offset is constant
// - Tz: offset depends on the date (DST rules)
}Fixed offsets are simple; DST-aware timezones apply date-specific rules.
use chrono::{DateTime, TimeZone};
use chrono_tz::Tz;
fn with_timezone_internals() {
// with_timezone internally:
// 1. Gets the UTC timestamp from the source DateTime
// 2. Creates a new DateTime in target timezone from that timestamp
// This is why it's always unambiguous:
// - UTC timestamp uniquely identifies an instant
// - Each instant has exactly one representation in any timezone
let ny_tz: Tz = "America/New_York".parse().unwrap();
let utc: DateTime<Utc> = "2024-11-03T06:00:00Z".parse().unwrap();
// During DST transition in New York
let ny_dt = utc.with_timezone(&ny_tz);
// Result: 2024-11-03 01:00:00 EST
// The timezone applies offset rules to the UTC instant
// No ambiguity because UTC instant is unambiguous
}with_timezone is unambiguous because it operates on the UTC instant.
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
fn version_notes() {
// chrono 0.4.x:
// - from_utc() takes &NaiveDateTime
// - Returns DateTime<Tz>
let naive = NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
let dt: DateTime<Utc> = Utc.from_utc_datetime(&naive);
// Alternative: DateTime::from_utc (deprecated in some versions)
// DateTime::<Utc>::from_utc(naive) // May be deprecated
// Recommended: use TimeZone::from_utc_datetime
let dt2: DateTime<Utc> = Utc.from_utc_datetime(&naive);
assert_eq!(dt, dt2);
}Use TimeZone::from_utc_datetime for constructing from naive UTC datetime.
use chrono::{DateTime, Utc, TimeZone};
use chrono_tz::Tz;
fn round_trip() {
let tz: Tz = "America/New_York".parse().unwrap();
// Start with UTC
let utc_original: DateTime<Utc> = "2024-06-15T12:00:00Z".parse().unwrap();
// Round trip: UTC -> New York -> UTC
let ny_dt = utc_original.with_timezone(&tz);
let utc_roundtrip = ny_dt.with_timezone(&Utc);
assert_eq!(utc_original, utc_roundtrip);
// The instant is preserved through all conversions
assert_eq!(utc_original.timestamp(), ny_dt.timestamp());
assert_eq!(utc_original.timestamp(), utc_roundtrip.timestamp());
// This works even across DST transitions
let utc_original: DateTime<Utc> = "2024-11-03T05:30:00Z".parse().unwrap();
let ny_dt = utc_original.with_timezone(&tz);
let utc_roundtrip = ny_dt.with_timezone(&Utc);
assert_eq!(utc_original, utc_roundtrip);
}Round trips through with_timezone preserve the instant exactly.
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
use chrono_tz::Tz;
fn ambiguity_resolution() {
let tz: Tz = "America/New_York".parse().unwrap();
// Ambiguous local time during DST fallback
let naive = NaiveDateTime::parse_from_str("2024-11-03 01:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
// from_local returns Ambiguous
match tz.from_local_datetime(&naive) {
chrono::LocalResult::Ambiguous(earliest, latest) => {
// earliest: 01:30 EDT (UTC-4)
// latest: 01:30 EST (UTC-5)
println!("Earliest (EDT): {:?}", earliest);
println!("Latest (EST): {:?}", latest);
// These are different UTC instants
assert_ne!(earliest.timestamp(), latest.timestamp());
}
_ => panic!("Expected ambiguous"),
}
// with_timezone never has this problem:
// It starts from UTC instant, so there's no ambiguity
let utc: DateTime<Utc> = "2024-11-03T05:30:00Z".parse().unwrap();
let ny = utc.with_timezone(&tz); // Always unambiguous
}Local-to-UTC conversion encounters ambiguity; with_timezone (UTC-to-local) does not.
use chrono::{DateTime, Utc, TimeZone};
use chrono_tz::Tz;
fn practical_guidance() {
// When you have UTC timestamp and need local time:
// Use with_timezone (or from_utc_datetime)
let utc_dt: DateTime<Utc> = "2024-01-15T12:00:00Z".parse().unwrap();
let tz: Tz = "America/New_York".parse().unwrap();
let local = utc_dt.with_timezone(&tz);
// When you have naive local time and need to convert:
// Be careful about DST - use from_local and handle Ambiguous/None
let naive_local = chrono::NaiveDateTime::parse_from_str(
"2024-03-10 02:30:00",
"%Y-%m-%d %H:%M:%S"
).unwrap();
match tz.from_local_datetime(&naive_local) {
chrono::LocalResult::Single(dt) => {
// Unambiguous, use dt
}
chrono::LocalResult::Ambiguous(early, late) => {
// Choose which one based on application logic
}
chrono::LocalResult::None => {
// Time doesn't exist during DST spring forward
// Handle error or adjust
}
}
// Best practice: Store and transmit UTC, convert to local for display
// This avoids most DST-related issues
}Store UTC, convert to local for display to avoid DST issues.
from_utc characteristics:
// - Takes NaiveDateTime (assumed to be UTC)
// - Creates DateTime in specified timezone
// - Never ambiguous (UTC has no DST)
// - Directly constructs from UTC representation
// - Use when you have UTC timestamp and need timezone-specific display
let naive_utc = NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
let dt = tz.from_utc_datetime(&naive_utc);with_timezone characteristics:
// - Takes existing DateTime<OldTz>
// - Returns DateTime<NewTz>
// - Always unambiguous (preserves UTC instant)
// - Converts through UTC internally
// - Use when converting between timezones
let utc_dt: DateTime<Utc> = "2024-01-15T12:00:00Z".parse().unwrap();
let ny_dt = utc_dt.with_timezone(&tz);Edge case handling:
// Ambiguous times (DST fallback):
// - from_utc: Never ambiguous (starts from UTC)
// - with_timezone: Never ambiguous (preserves instant)
// - from_local: Returns Ambiguous with both possibilities
// Nonexistent times (DST spring forward):
// - from_utc: Never produces nonexistent (UTC always valid)
// - with_timezone: Never produces nonexistent (instant always valid)
// - from_local: Returns None for nonexistent times
// Best practice:
// - Always work in UTC when possible
// - Use with_timezone for display/conversion
// - Handle LocalResult when parsing local timesKey insight: from_utc and with_timezone both produce unambiguous results, but through different mechanisms: from_utc starts from UTC (which has no DST ambiguity) and applies timezone offset rules, while with_timezone preserves the underlying UTC instant when converting between timezones. The key difference is that from_utc is a construction operation (NaiveDateTime → DateTime), while with_timezone is a conversion operation (DateTime → DateTime). Edge cases arise when going the opposite direction: converting from local time to UTC using from_local encounters ambiguity (DST fallback) and nonexistence (DST spring forward) because local time representations don't uniquely identify instants. The rule is: UTC is the source of truth, and timezone conversions should always go through UTC to preserve instant semantics.