1use core::fmt;
6
7use icu_calendar::types::RataDie;
8use icu_calendar::{AsCalendar, Iso};
9use zerovec::ule::AsULE;
10
11use crate::Time;
12use crate::{DateTime, ZonedDateTime, zone::UtcOffset};
13
14#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
63pub struct ZoneNameTimestamp(u32);
64
65const RD_EPOCH: RataDie = calendrical_calculations::gregorian::fixed_from_gregorian(1970, 1, 1);
66
67impl ZoneNameTimestamp {
68 pub fn to_zoned_date_time_iso(self) -> ZonedDateTime<Iso, UtcOffset> {
102 ZonedDateTime::from_epoch_milliseconds_and_utc_offset(
103 self.epoch_seconds() * 1000,
104 UtcOffset::zero(),
105 )
106 }
107
108 pub fn from_zoned_date_time<C: AsCalendar>(
110 zoned_date_time: ZonedDateTime<C, UtcOffset>,
111 ) -> Self {
112 Self::from_rd_time_zone(
113 zoned_date_time.date.to_rata_die(),
114 zoned_date_time.time,
115 zoned_date_time.zone,
116 )
117 }
118
119 #[deprecated(since = "2.2.0", note = "use `Self::from_zoned_date_time`")]
121 pub fn from_zoned_date_time_iso(zoned_date_time: ZonedDateTime<Iso, UtcOffset>) -> Self {
122 Self::from_zoned_date_time(zoned_date_time)
123 }
124
125 pub(crate) fn from_rd_time_zone(rd: RataDie, time: Time, zone: UtcOffset) -> Self {
126 Self::from_epoch_seconds(
127 (rd - RD_EPOCH) * 24 * 60 * 60 + time.seconds_since_midnight() as i64
128 - zone.to_seconds() as i64,
129 )
130 }
131
132 pub fn from_epoch_seconds(seconds: i64) -> Self {
134 let seconds = match seconds {
135 63593070..63593100 => 63593100,
138 307622400..307622700 => 307622700,
139 576041460..576042300 => 576042300,
140 576043260..576044100 => 576044100,
141 594180060..594180900 => 594180900,
142 607491060..607491900 => 607491900,
143 1601740860..1601741700 => 1601741700,
144 1633190460..1633191300 => 1633191300,
145 1664640060..1664640900 => 1664640900,
146 s => s,
147 };
148 let qh = seconds / 60 / 15;
149 let qh_clamped = qh.clamp(Self::far_in_past().0 as i64, Self::far_in_future().0 as i64);
150 Self(qh_clamped as u32)
152 }
153
154 fn epoch_seconds(self) -> i64 {
160 match self.0 as i64 * 15 * 60 {
161 63593100 => 63593070,
163 307622700 => 307622400,
164 576042300 => 576041460,
165 576044100 => 576043260,
166 594180900 => 594180060,
167 607491900 => 607491060,
168 1601741700 => 1601740860,
169 1633191300 => 1633190460,
170 1664640900 => 1664640060,
171 ms => ms,
172 }
173 }
174
175 #[deprecated(
177 since = "2.1.0",
178 note = "returns a UTC DateTime, which is the wrong type. Use `to_zoned_date_time_iso` instead"
179 )]
180 pub fn to_date_time_iso(self) -> DateTime<Iso> {
181 let ZonedDateTime {
182 date,
183 time,
184 zone: _utc_offset_zero,
185 } = self.to_zoned_date_time_iso();
186 DateTime { date, time }
187 }
188
189 #[deprecated(
193 since = "2.1.0",
194 note = "implicitly interprets the DateTime as UTC. Use `from_zoned_date_time_iso` instead."
195 )]
196 pub fn from_date_time_iso(DateTime { date, time }: DateTime<Iso>) -> Self {
197 Self::from_zoned_date_time(ZonedDateTime {
198 date,
199 time,
200 zone: UtcOffset::zero(),
201 })
202 }
203
204 pub fn far_in_past() -> Self {
206 Self(0)
207 }
208
209 pub fn far_in_future() -> Self {
211 Self(0xFFFFFF)
212 }
213}
214
215impl fmt::Debug for ZoneNameTimestamp {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 write!(f, "~{:?}", self.to_zoned_date_time_iso())
218 }
219}
220
221impl AsULE for ZoneNameTimestamp {
222 type ULE = <u32 as AsULE>::ULE;
223 #[inline]
224 fn to_unaligned(self) -> Self::ULE {
225 self.0.to_unaligned()
226 }
227 #[inline]
228 fn from_unaligned(unaligned: Self::ULE) -> Self {
229 Self(u32::from_unaligned(unaligned))
230 }
231}
232
233#[cfg(feature = "alloc")]
234impl<'a> zerovec::maps::ZeroMapKV<'a> for ZoneNameTimestamp {
235 type Container = zerovec::ZeroVec<'a, Self>;
236 type Slice = zerovec::ZeroSlice<Self>;
237 type GetType = <Self as AsULE>::ULE;
238 type OwnedType = Self;
239}
240
241#[cfg(feature = "serde")]
242impl serde::Serialize for ZoneNameTimestamp {
243 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
244 where
245 S: serde::Serializer,
246 {
247 #[cfg(feature = "alloc")]
248 if serializer.is_human_readable() {
249 let date_time = self.to_zoned_date_time_iso();
250 let year = date_time.date.era_year().year;
251 let month = date_time.date.month().number();
252 let day = date_time.date.day_of_month().0;
253 let hour = date_time.time.hour.number();
254 let minute = date_time.time.minute.number();
255 let second = date_time.time.second.number();
256 let mut s = alloc::format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}");
257 if second != 0 {
258 use core::fmt::Write;
259 let _infallible = write!(&mut s, ":{second:02}");
260 }
261 return serializer.serialize_str(&s);
263 }
264 serializer.serialize_u32(self.0)
265 }
266}
267
268#[cfg(feature = "serde")]
269impl<'de> serde::Deserialize<'de> for ZoneNameTimestamp {
270 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
271 where
272 D: serde::Deserializer<'de>,
273 {
274 #[cfg(feature = "alloc")]
275 if deserializer.is_human_readable() {
276 use serde::de::Error;
277 let e0 = D::Error::custom("invalid");
278 let e1 = |_| D::Error::custom("invalid");
279 let e2 = |_| D::Error::custom("invalid");
280 let e3 = |_| D::Error::custom("invalid");
281
282 let parts = alloc::borrow::Cow::<'de, str>::deserialize(deserializer)?;
283 if parts.len() != 16 {
284 return Err(e0);
285 }
286 let year = parts[0..4].parse::<i32>().map_err(e1)?;
287 let month = parts[5..7].parse::<u8>().map_err(e1)?;
288 let day = parts[8..10].parse::<u8>().map_err(e1)?;
289 let hour = parts[11..13].parse::<u8>().map_err(e1)?;
290 let minute = parts[14..16].parse::<u8>().map_err(e1)?;
291 return Ok(Self::from_zoned_date_time(ZonedDateTime {
292 date: icu_calendar::Date::try_new_iso(year, month, day).map_err(e2)?,
293 time: Time::try_new(hour, minute, 0, 0).map_err(e3)?,
294 zone: UtcOffset::zero(),
295 }));
296 }
297 u32::deserialize(deserializer).map(Self)
298 }
299}
300
301#[cfg(test)]
302mod test {
303 use super::*;
304
305 #[test]
306 fn test_packing() {
307 #[derive(Debug)]
308 struct TestCase {
309 input: &'static str,
310 output: &'static str,
311 }
312 for test_case in [
313 TestCase {
315 input: "1970-01-01T00:00Z",
316 output: "1970-01-01T00:00Z",
317 },
318 TestCase {
319 input: "1970-01-01T00:01Z",
320 output: "1970-01-01T00:00Z",
321 },
322 TestCase {
323 input: "1970-01-01T00:15Z",
324 output: "1970-01-01T00:15Z",
325 },
326 TestCase {
327 input: "1970-01-01T00:29Z",
328 output: "1970-01-01T00:15Z",
329 },
330 TestCase {
332 input: "1969-12-31T23:59Z",
333 output: "1970-01-01T00:00Z",
334 },
335 TestCase {
336 input: "1969-12-31T12:00Z",
337 output: "1970-01-01T00:00Z",
338 },
339 TestCase {
340 input: "1900-07-15T12:34Z",
341 output: "1970-01-01T00:00Z",
342 },
343 TestCase {
345 input: "2448-06-25T15:45Z",
346 output: "2448-06-25T15:45Z",
347 },
348 TestCase {
349 input: "2448-06-25T16:00Z",
350 output: "2448-06-25T15:45Z",
351 },
352 TestCase {
353 input: "2448-06-26T00:00Z",
354 output: "2448-06-25T15:45Z",
355 },
356 TestCase {
357 input: "2500-01-01T00:00Z",
358 output: "2448-06-25T15:45Z",
359 },
360 TestCase {
362 input: "2025-10-10T10:15+02",
363 output: "2025-10-10T08:15Z",
364 },
365 TestCase {
367 input: "2025-04-30T15:18:25Z",
368 output: "2025-04-30T15:15Z",
369 },
370 ] {
371 let znt = ZoneNameTimestamp::from_zoned_date_time(
372 ZonedDateTime::try_offset_only_from_str(test_case.input, Iso).unwrap(),
373 );
374 let actual = znt.to_zoned_date_time_iso();
375 assert_eq!(
376 ZonedDateTime::try_offset_only_from_str(test_case.output, Iso).unwrap(),
377 actual,
378 "{test_case:?}"
379 );
380 }
381 }
382}