use crate::Tz;
use time::{Duration, OffsetDateTime};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZonedDateTime {
instant: OffsetDateTime,
tz: Tz,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalParts {
pub year: i32,
pub month_1: u8, pub day: u8, pub hour: u8, pub minute: u8, pub second: u8, pub nano: u32,
pub dow_monday0: u8, }
impl ZonedDateTime {
pub fn new(instant: OffsetDateTime, tz: Tz) -> Self {
let utc = instant.to_offset(time::UtcOffset::UTC);
Self { instant: utc, tz }
}
pub fn instant(&self) -> OffsetDateTime {
self.instant
}
pub fn tz(&self) -> &Tz {
&self.tz
}
pub fn add(&self, dur: Duration) -> Self {
Self {
instant: self.instant + dur,
tz: self.tz.clone(),
}
}
pub fn local_parts(&self) -> LocalParts {
#[cfg(all(feature = "wasm", not(feature = "native")))]
{
let utc_ms = (self.instant.unix_timestamp_nanos() / 1_000_000) as f64;
if let Some(parts) = crate::wasm::intl_local_parts(utc_ms, self.tz.as_iana()) {
return parts;
}
}
let local = self.to_local_offset();
LocalParts {
year: local.year(),
month_1: local.month() as u8,
day: local.day(),
hour: local.hour(),
minute: local.minute(),
second: local.second(),
nano: local.nanosecond(),
dow_monday0: local.weekday().number_days_from_monday(),
}
}
#[cfg(feature = "native")]
fn to_local_offset(&self) -> OffsetDateTime {
use time_tz::OffsetDateTimeExt;
if let Some(tz) = time_tz::timezones::get_by_name(self.tz.as_iana()) {
self.instant.to_timezone(tz)
} else {
self.instant
}
}
#[cfg(not(feature = "native"))]
fn to_local_offset(&self) -> OffsetDateTime {
self.instant
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
fn seoul() -> Tz {
Tz::parse("Asia/Seoul").unwrap_or_else(|_| Tz::seoul())
}
#[test]
fn instant_roundtrip_preserves_utc() {
let utc = datetime!(2026-05-10 00:00:00 UTC);
let zdt = ZonedDateTime::new(utc, Tz::utc());
assert_eq!(zdt.instant(), utc);
}
#[test]
fn add_shifts_instant_preserves_tz() {
let utc = datetime!(2026-05-10 00:00:00 UTC);
let tz = seoul();
let zdt = ZonedDateTime::new(utc, tz.clone());
let shifted = zdt.add(Duration::hours(15));
assert_eq!(shifted.instant(), utc + Duration::hours(15));
assert_eq!(shifted.tz(), &tz);
}
#[test]
fn local_parts_utc_midnight() {
let utc = datetime!(2026-05-10 00:00:00 UTC);
let zdt = ZonedDateTime::new(utc, Tz::utc());
let parts = zdt.local_parts();
assert_eq!(parts.hour, 0);
assert_eq!(parts.year, 2026);
assert_eq!(parts.month_1, 5);
assert_eq!(parts.day, 10);
}
#[cfg(feature = "native")]
#[test]
fn seoul_utc_midnight_is_hour_9() {
let utc = datetime!(2026-05-10 00:00:00 UTC);
let zdt = ZonedDateTime::new(utc, seoul());
assert_eq!(zdt.local_parts().hour, 9);
}
#[cfg(feature = "native")]
#[test]
fn kst_09_stored_as_utc_00() {
let utc = datetime!(2026-05-10 00:00:00 UTC);
let zdt = ZonedDateTime::new(utc, seoul());
assert_eq!(zdt.instant(), utc);
assert_eq!(zdt.local_parts().hour, 9);
}
#[cfg(feature = "native")]
#[test]
fn dst_transition_new_york_2024() {
let tz = Tz::parse("America/New_York").expect("valid");
let before = datetime!(2024-03-10 06:59:00 UTC);
let after = datetime!(2024-03-10 07:00:00 UTC);
let zdt_before = ZonedDateTime::new(before, tz.clone());
let zdt_after = ZonedDateTime::new(after, tz);
assert_eq!(zdt_before.local_parts().hour, 1);
assert_eq!(zdt_after.local_parts().hour, 3);
}
#[test]
fn dow_monday0_sunday() {
let utc = datetime!(2026-05-10 12:00:00 UTC);
let zdt = ZonedDateTime::new(utc, Tz::utc());
assert_eq!(zdt.local_parts().dow_monday0, 6);
}
#[test]
fn dow_monday0_monday() {
let utc = datetime!(2026-05-11 12:00:00 UTC);
let zdt = ZonedDateTime::new(utc, Tz::utc());
assert_eq!(zdt.local_parts().dow_monday0, 0);
}
#[cfg(feature = "native")]
mod proptest_zoned {
use super::*;
use proptest::prelude::*;
use time::OffsetDateTime;
proptest! {
#[test]
fn roundtrip_instant_preserved(
unix_secs in -2_000_000_000i64..2_000_000_000i64
) {
let instant = OffsetDateTime::from_unix_timestamp(unix_secs)
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
let zdt = ZonedDateTime::new(instant, Tz::utc());
let recovered = zdt.instant();
prop_assert_eq!(recovered.unix_timestamp(), instant.unix_timestamp());
}
}
}
}