1use core::fmt;
6
7use icu_calendar::{types::RataDie, Date, Iso};
8use zerovec::{maps::ZeroMapKV, ule::AsULE, ZeroSlice, ZeroVec};
9
10const ZONE_NAME_EPOCH: RataDie = calendrical_calculations::iso::const_fixed_from_iso(1970, 1, 1);
12const QUARTER_HOURS_IN_DAY_I64: i64 = 24 * 4;
13const QUARTER_HOURS_IN_DAY_U32: u32 = 24 * 4;
14const MIN_QUARTER_HOURS_I64: i64 = 0;
15const MIN_QUARTER_HOURS_U32: u32 = 0;
16const MAX_QUARTER_HOURS_I64: i64 = 0xFFFFFF;
17const MAX_QUARTER_HOURS_U32: u32 = 0xFFFFFF;
18
19use crate::{DateTime, Hour, Minute, Nanosecond, Second, Time};
20
21#[derive(Debug, Copy, Clone)]
23struct ZoneNameTimestampParts {
24 quarter_hours_since_local_unix_epoch: u32,
27 metadata: u8,
32}
33
34impl ZoneNameTimestampParts {
35 fn date_time(self) -> DateTime<Iso> {
37 let qh = self.quarter_hours_since_local_unix_epoch;
38 let (days, remainder) = (
40 (qh / QUARTER_HOURS_IN_DAY_U32) as i64,
41 (qh % QUARTER_HOURS_IN_DAY_U32) as u8,
42 );
43 let (hours, minutes) = (remainder / 4, (remainder % 4) * 15);
44 DateTime {
45 date: Date::from_rata_die(ZONE_NAME_EPOCH + days, Iso),
46 time: Time {
47 hour: Hour::try_from(hours).unwrap_or_else(|_| {
48 debug_assert!(false, "ZoneNameTimestampParts: out of range: {self:?}");
49 Hour::zero()
50 }),
51 minute: Minute::try_from(minutes).unwrap_or_else(|_| {
52 debug_assert!(false, "ZoneNameTimestampParts: out of range: {self:?}");
53 Minute::zero()
54 }),
55 second: Second::zero(),
56 subsecond: Nanosecond::zero(),
57 },
58 }
59 }
60
61 fn from_saturating_date_time_with_metadata(date_time: DateTime<Iso>, metadata: u8) -> Self {
63 let qh_days = (date_time.date.to_rata_die() - ZONE_NAME_EPOCH) * QUARTER_HOURS_IN_DAY_I64;
65 let qh_hours = date_time.time.hour.number() * 4;
67 let qh_minutes = date_time.time.minute.number() / 15;
68 let qh_total = qh_days + (qh_hours as i64) + (qh_minutes as i64);
69 let qh_clamped = qh_total.clamp(MIN_QUARTER_HOURS_I64, MAX_QUARTER_HOURS_I64);
70 let qh_u32 = match u32::try_from(qh_clamped) {
71 Ok(x) => x,
72 Err(_) => {
73 debug_assert!(
74 false,
75 "ZoneNameTimestampParts: saturation invariants not upheld: {date_time:?}"
76 );
77 0
78 }
79 };
80 ZoneNameTimestampParts {
81 quarter_hours_since_local_unix_epoch: qh_u32,
82 metadata,
83 }
84 }
85}
86
87#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
142pub struct ZoneNameTimestamp(u32);
143
144impl ZoneNameTimestamp {
145 pub fn to_date_time_iso(self) -> DateTime<Iso> {
149 let parts = self.to_parts();
150 parts.date_time()
151 }
152
153 pub fn from_date_time_iso(date_time: DateTime<Iso>) -> Self {
192 let metadata = 0; let parts =
194 ZoneNameTimestampParts::from_saturating_date_time_with_metadata(date_time, metadata);
195 Self::from_parts(parts)
196 }
197
198 pub fn far_in_past() -> Self {
200 Self::from_parts(ZoneNameTimestampParts {
201 quarter_hours_since_local_unix_epoch: MIN_QUARTER_HOURS_U32,
202 metadata: 0, })
204 }
205
206 pub fn far_in_future() -> Self {
208 Self::from_parts(ZoneNameTimestampParts {
209 quarter_hours_since_local_unix_epoch: MAX_QUARTER_HOURS_U32,
210 metadata: 0, })
212 }
213
214 fn to_parts(self) -> ZoneNameTimestampParts {
215 let metadata = ((self.0 & 0xFF000000) >> 24) as u8;
216 let qh_recovered = self.0 & 0x00FFFFFF;
217 ZoneNameTimestampParts {
218 quarter_hours_since_local_unix_epoch: qh_recovered,
219 metadata,
220 }
221 }
222
223 fn from_parts(parts: ZoneNameTimestampParts) -> Self {
224 let metadata_shifted = (parts.metadata as u32) << 24;
225 debug_assert!(parts.quarter_hours_since_local_unix_epoch <= 0x00FFFFFF);
226 let qh_masked = parts.quarter_hours_since_local_unix_epoch & 0x00FFFFFF;
227 Self(metadata_shifted | qh_masked)
228 }
229}
230
231impl fmt::Debug for ZoneNameTimestamp {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 let parts = self.to_parts();
234 f.debug_struct("ZoneNameTimestamp")
235 .field("date_time", &parts.date_time())
236 .field("metadata", &parts.metadata)
237 .finish()
238 }
239}
240
241impl AsULE for ZoneNameTimestamp {
242 type ULE = <u32 as AsULE>::ULE;
243 #[inline]
244 fn to_unaligned(self) -> Self::ULE {
245 self.0.to_unaligned()
246 }
247 #[inline]
248 fn from_unaligned(unaligned: Self::ULE) -> Self {
249 Self(u32::from_unaligned(unaligned))
250 }
251}
252
253impl<'a> ZeroMapKV<'a> for ZoneNameTimestamp {
254 type Container = ZeroVec<'a, Self>;
255 type Slice = ZeroSlice<Self>;
256 type GetType = <Self as AsULE>::ULE;
257 type OwnedType = Self;
258}
259
260#[cfg(feature = "serde")]
261impl serde::Serialize for ZoneNameTimestamp {
262 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
263 where
264 S: serde::Serializer,
265 {
266 #[cfg(feature = "alloc")]
267 if serializer.is_human_readable() {
268 let date_time = self.to_date_time_iso();
269 let year = date_time.date.extended_year();
270 let month = date_time.date.month().month_number();
271 let day = date_time.date.day_of_month().0;
272 let hour = date_time.time.hour.number();
273 let minute = date_time.time.minute.number();
274 return serializer.serialize_str(&alloc::format!(
276 "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}"
277 ));
278 }
279 serializer.serialize_u32(self.0)
280 }
281}
282
283#[cfg(feature = "serde")]
284impl<'de> serde::Deserialize<'de> for ZoneNameTimestamp {
285 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286 where
287 D: serde::Deserializer<'de>,
288 {
289 #[cfg(feature = "alloc")]
290 if deserializer.is_human_readable() {
291 use serde::de::Error;
292 let e0 = D::Error::custom("invalid");
293 let e1 = |_| D::Error::custom("invalid");
294 let e2 = |_| D::Error::custom("invalid");
295 let e3 = |_| D::Error::custom("invalid");
296
297 let parts = alloc::borrow::Cow::<'de, str>::deserialize(deserializer)?;
298 if parts.len() != 16 {
299 return Err(e0);
300 }
301 let year = parts[0..4].parse::<i32>().map_err(e1)?;
302 let month = parts[5..7].parse::<u8>().map_err(e1)?;
303 let day = parts[8..10].parse::<u8>().map_err(e1)?;
304 let hour = parts[11..13].parse::<u8>().map_err(e1)?;
305 let minute = parts[14..16].parse::<u8>().map_err(e1)?;
306 return Ok(Self::from_date_time_iso(DateTime {
307 date: Date::try_new_iso(year, month, day).map_err(e2)?,
308 time: Time::try_new(hour, minute, 0, 0).map_err(e3)?,
309 }));
310 }
311 u32::deserialize(deserializer).map(Self)
312 }
313}
314
315#[cfg(test)]
316mod test {
317 use super::*;
318
319 #[test]
320 fn test_packing() {
321 #[derive(Debug)]
322 struct TestCase {
323 input: DateTime<Iso>,
324 output: DateTime<Iso>,
325 }
326 for test_case in [
327 TestCase {
329 input: "1970-01-01T00:00".parse().unwrap(),
330 output: "1970-01-01T00:00".parse().unwrap(),
331 },
332 TestCase {
333 input: "1970-01-01T00:01".parse().unwrap(),
334 output: "1970-01-01T00:00".parse().unwrap(),
335 },
336 TestCase {
337 input: "1970-01-01T00:15".parse().unwrap(),
338 output: "1970-01-01T00:15".parse().unwrap(),
339 },
340 TestCase {
341 input: "1970-01-01T00:29".parse().unwrap(),
342 output: "1970-01-01T00:15".parse().unwrap(),
343 },
344 TestCase {
346 input: "1969-12-31T23:59".parse().unwrap(),
347 output: "1970-01-01T00:00".parse().unwrap(),
348 },
349 TestCase {
350 input: "1969-12-31T12:00".parse().unwrap(),
351 output: "1970-01-01T00:00".parse().unwrap(),
352 },
353 TestCase {
354 input: "1900-07-15T12:34".parse().unwrap(),
355 output: "1970-01-01T00:00".parse().unwrap(),
356 },
357 TestCase {
359 input: "2448-06-25T15:45".parse().unwrap(),
360 output: "2448-06-25T15:45".parse().unwrap(),
361 },
362 TestCase {
363 input: "2448-06-25T16:00".parse().unwrap(),
364 output: "2448-06-25T15:45".parse().unwrap(),
365 },
366 TestCase {
367 input: "2448-06-26T00:00".parse().unwrap(),
368 output: "2448-06-25T15:45".parse().unwrap(),
369 },
370 TestCase {
371 input: "2500-01-01T00:00".parse().unwrap(),
372 output: "2448-06-25T15:45".parse().unwrap(),
373 },
374 TestCase {
376 input: "2025-04-30T15:18:25".parse().unwrap(),
377 output: "2025-04-30T15:15".parse().unwrap(),
378 },
379 ] {
380 let znt = ZoneNameTimestamp::from_date_time_iso(test_case.input);
381 let actual = znt.to_date_time_iso();
382 assert_eq!(test_case.output, actual, "{test_case:?}");
383 }
384 }
385
386 #[test]
387 fn test_metadata_noop() {
388 let raw = (0x12345678u32).to_unaligned();
389 let znt = ZoneNameTimestamp::from_unaligned(raw);
390 let roundtrip_znt = ZoneNameTimestamp::from_date_time_iso(znt.to_date_time_iso());
391 let roundtrip_raw = roundtrip_znt.to_unaligned();
392
393 assert_eq!(raw.0[0..3], roundtrip_raw.0[0..3]);
395 assert_eq!(raw.0[3], 0x12);
396 assert_eq!(roundtrip_raw.0[3], 0);
397 }
398}