1use crate::Tz;
2use time::{Duration, OffsetDateTime};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ZonedDateTime {
10 instant: OffsetDateTime,
11 tz: Tz,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct LocalParts {
17 pub year: i32,
18 pub month_1: u8, pub day: u8, pub hour: u8, pub minute: u8, pub second: u8, pub nano: u32,
24 pub dow_monday0: u8, }
26
27impl ZonedDateTime {
28 pub fn new(instant: OffsetDateTime, tz: Tz) -> Self {
29 let utc = instant.to_offset(time::UtcOffset::UTC);
31 Self { instant: utc, tz }
32 }
33
34 pub fn instant(&self) -> OffsetDateTime {
35 self.instant
36 }
37
38 pub fn tz(&self) -> &Tz {
39 &self.tz
40 }
41
42 pub fn add(&self, dur: Duration) -> Self {
45 Self {
46 instant: self.instant + dur,
47 tz: self.tz.clone(),
48 }
49 }
50
51 pub fn local_parts(&self) -> LocalParts {
57 #[cfg(all(feature = "wasm", not(feature = "native")))]
58 {
59 let utc_ms = (self.instant.unix_timestamp_nanos() / 1_000_000) as f64;
60 if let Some(parts) = crate::wasm::intl_local_parts(utc_ms, self.tz.as_iana()) {
61 return parts;
62 }
63 }
64
65 let local = self.to_local_offset();
66 LocalParts {
67 year: local.year(),
68 month_1: local.month() as u8,
69 day: local.day(),
70 hour: local.hour(),
71 minute: local.minute(),
72 second: local.second(),
73 nano: local.nanosecond(),
74 dow_monday0: local.weekday().number_days_from_monday(),
75 }
76 }
77
78 #[cfg(feature = "native")]
79 fn to_local_offset(&self) -> OffsetDateTime {
80 use time_tz::OffsetDateTimeExt;
81 if let Some(tz) = time_tz::timezones::get_by_name(self.tz.as_iana()) {
82 self.instant.to_timezone(tz)
83 } else {
84 self.instant
86 }
87 }
88
89 #[cfg(not(feature = "native"))]
90 fn to_local_offset(&self) -> OffsetDateTime {
91 self.instant
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use time::macros::datetime;
100
101 fn seoul() -> Tz {
102 Tz::parse("Asia/Seoul").unwrap_or_else(|_| Tz::seoul())
103 }
104
105 #[test]
106 fn instant_roundtrip_preserves_utc() {
107 let utc = datetime!(2026-05-10 00:00:00 UTC);
108 let zdt = ZonedDateTime::new(utc, Tz::utc());
109 assert_eq!(zdt.instant(), utc);
110 }
111
112 #[test]
113 fn add_shifts_instant_preserves_tz() {
114 let utc = datetime!(2026-05-10 00:00:00 UTC);
115 let tz = seoul();
116 let zdt = ZonedDateTime::new(utc, tz.clone());
117 let shifted = zdt.add(Duration::hours(15));
118 assert_eq!(shifted.instant(), utc + Duration::hours(15));
119 assert_eq!(shifted.tz(), &tz);
120 }
121
122 #[test]
123 fn local_parts_utc_midnight() {
124 let utc = datetime!(2026-05-10 00:00:00 UTC);
125 let zdt = ZonedDateTime::new(utc, Tz::utc());
126 let parts = zdt.local_parts();
127 assert_eq!(parts.hour, 0);
128 assert_eq!(parts.year, 2026);
129 assert_eq!(parts.month_1, 5);
130 assert_eq!(parts.day, 10);
131 }
132
133 #[cfg(feature = "native")]
134 #[test]
135 fn seoul_utc_midnight_is_hour_9() {
136 let utc = datetime!(2026-05-10 00:00:00 UTC);
138 let zdt = ZonedDateTime::new(utc, seoul());
139 assert_eq!(zdt.local_parts().hour, 9);
140 }
141
142 #[cfg(feature = "native")]
143 #[test]
144 fn kst_09_stored_as_utc_00() {
145 let utc = datetime!(2026-05-10 00:00:00 UTC);
147 let zdt = ZonedDateTime::new(utc, seoul());
148 assert_eq!(zdt.instant(), utc);
149 assert_eq!(zdt.local_parts().hour, 9);
150 }
151
152 #[cfg(feature = "native")]
153 #[test]
154 fn dst_transition_new_york_2024() {
155 let tz = Tz::parse("America/New_York").expect("valid");
158 let before = datetime!(2024-03-10 06:59:00 UTC);
159 let after = datetime!(2024-03-10 07:00:00 UTC);
160 let zdt_before = ZonedDateTime::new(before, tz.clone());
161 let zdt_after = ZonedDateTime::new(after, tz);
162 assert_eq!(zdt_before.local_parts().hour, 1);
163 assert_eq!(zdt_after.local_parts().hour, 3);
164 }
165
166 #[test]
167 fn dow_monday0_sunday() {
168 let utc = datetime!(2026-05-10 12:00:00 UTC);
170 let zdt = ZonedDateTime::new(utc, Tz::utc());
171 assert_eq!(zdt.local_parts().dow_monday0, 6);
172 }
173
174 #[test]
175 fn dow_monday0_monday() {
176 let utc = datetime!(2026-05-11 12:00:00 UTC);
178 let zdt = ZonedDateTime::new(utc, Tz::utc());
179 assert_eq!(zdt.local_parts().dow_monday0, 0);
180 }
181
182 #[cfg(feature = "native")]
183 mod proptest_zoned {
184 use super::*;
185 use proptest::prelude::*;
186 use time::OffsetDateTime;
187
188 proptest! {
189 #[test]
190 fn roundtrip_instant_preserved(
191 unix_secs in -2_000_000_000i64..2_000_000_000i64
192 ) {
193 let instant = OffsetDateTime::from_unix_timestamp(unix_secs)
194 .unwrap_or(OffsetDateTime::UNIX_EPOCH);
195 let zdt = ZonedDateTime::new(instant, Tz::utc());
196 let recovered = zdt.instant();
197 prop_assert_eq!(recovered.unix_timestamp(), instant.unix_timestamp());
198 }
199 }
200 }
201}