1#![allow(clippy::exhaustive_structs, clippy::exhaustive_enums)]
7#![allow(clippy::type_complexity)]
8
9use crate::zone::{UtcOffset, ZoneNameTimestamp};
20use icu_provider::prelude::*;
21use zerotrie::ZeroTrieSimpleAscii;
22use zerovec::ule::vartuple::VarTupleULE;
23use zerovec::ule::{AsULE, NichedOption, RawBytesULE};
24use zerovec::{VarZeroVec, ZeroSlice, ZeroVec};
25
26pub use crate::zone::TimeZone;
27pub mod iana;
28pub mod windows;
29
30#[cfg(feature = "compiled_data")]
31#[derive(Debug)]
32pub struct Baked;
40
41#[cfg(feature = "compiled_data")]
42#[allow(unused_imports)]
43const _: () = {
44 use icu_time_data::*;
45 pub mod icu {
46 pub use crate as time;
47 }
48 make_provider!(Baked);
49 impl_timezone_identifiers_iana_extended_v1!(Baked);
50 impl_timezone_identifiers_iana_core_v1!(Baked);
51 impl_timezone_identifiers_windows_v1!(Baked);
52 impl_timezone_periods_v1!(Baked);
53};
54
55#[cfg(feature = "datagen")]
56pub const MARKERS: &[DataMarkerInfo] = &[
58 iana::TimezoneIdentifiersIanaExtendedV1::INFO,
59 iana::TimezoneIdentifiersIanaCoreV1::INFO,
60 windows::TimezoneIdentifiersWindowsV1::INFO,
61 TimezonePeriodsV1::INFO,
62];
63
64const SECONDS_TO_EIGHTS_OF_HOURS: i32 = 60 * 60 / 8;
65
66#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
68#[zerovec::make_ule(TimeZoneVariantULE)]
69#[repr(u8)]
70#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
71#[cfg_attr(feature = "datagen", databake(path = icu_time::provider))]
72#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
73#[cfg_attr(not(feature = "alloc"), zerovec::skip_derive(ZeroMapKV))]
74#[non_exhaustive]
75pub enum TimeZoneVariant {
76 Standard = 0,
81 Daylight = 1,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
90#[non_exhaustive]
91pub enum MetazoneMembershipKind {
92 BehavesLikeGolden,
94 CustomVariants,
97 CustomTransitions,
101}
102
103#[non_exhaustive]
106#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
107pub struct VariantOffsets {
108 pub standard: UtcOffset,
110 pub daylight: Option<UtcOffset>,
112}
113
114impl VariantOffsets {
115 pub fn from_standard(standard: UtcOffset) -> Self {
117 Self {
118 standard,
119 daylight: None,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
126pub struct VariantOffsetsWithMetazoneMembershipKind {
127 pub offsets: VariantOffsets,
129 pub mzmsk: MetazoneMembershipKind,
131}
132
133impl AsULE for VariantOffsetsWithMetazoneMembershipKind {
134 type ULE = [i8; 2];
135
136 fn from_unaligned([std, dst]: Self::ULE) -> Self {
137 Self {
138 offsets: VariantOffsets {
139 standard: UtcOffset::from_seconds_unchecked(if std == i8::MAX {
140 -2670
143 } else {
144 std as i32 * SECONDS_TO_EIGHTS_OF_HOURS
145 + match std % 8 {
146 1 | 5 => 150,
148 -1 | -5 => -150,
149 3 | 7 => -150,
151 -3 | -7 => 150,
152 _ => 0,
154 }
155 }),
156 daylight: match dst as u8 & 0b0011_1111 {
157 0 => None,
158 1 => Some(0),
159 2 => Some(1800),
160 3 => Some(3600),
161 4 => Some(5400),
162 5 => Some(7200),
163 6 => Some(-3600),
164 x => {
165 debug_assert!(false, "unknown DST encoding {x}");
166 None
167 }
168 }
169 .map(|d| {
170 UtcOffset::from_seconds_unchecked(std as i32 * SECONDS_TO_EIGHTS_OF_HOURS + d)
171 }),
172 },
173 mzmsk: match (dst as u8 & 0b1100_0000) >> 6 {
174 0b00 => MetazoneMembershipKind::BehavesLikeGolden,
175 0b10 => MetazoneMembershipKind::CustomTransitions,
176 0b01 => MetazoneMembershipKind::CustomVariants,
177 x => {
178 debug_assert!(false, "unknown MetazoneMembershipKind encoding {x}");
179 MetazoneMembershipKind::BehavesLikeGolden
180 }
181 },
182 }
183 }
184
185 fn to_unaligned(self) -> Self::ULE {
186 let offset = self.offsets.standard.to_seconds();
187 [
188 if offset == -2670 {
189 i8::MAX
192 } else {
193 debug_assert_eq!(offset.abs() % 60, 0);
194 let scaled = match offset.abs() / 60 % 60 {
195 0 | 15 | 30 | 45 => offset / SECONDS_TO_EIGHTS_OF_HOURS,
196 10 | 40 => {
197 offset / SECONDS_TO_EIGHTS_OF_HOURS
199 }
200 20 | 50 => {
201 offset / SECONDS_TO_EIGHTS_OF_HOURS + offset.signum()
203 }
204 _ => {
205 debug_assert!(false, "{offset:?}");
206 offset / SECONDS_TO_EIGHTS_OF_HOURS
207 }
208 };
209 debug_assert!(i8::MIN as i32 <= scaled && scaled < i8::MAX as i32);
210 scaled as i8
211 },
212 match self
213 .offsets
214 .daylight
215 .map(|o| o.to_seconds() - self.offsets.standard.to_seconds())
216 {
217 None => 0,
218 Some(0) => 1,
219 Some(1800) => 2,
220 Some(3600) => 3,
221 Some(5400) => 4,
222 Some(7200) => 5,
223 Some(-3600) => 6,
224 Some(x) => {
225 debug_assert!(false, "unhandled DST value {x}");
226 0
227 }
228 } | (match self.mzmsk {
229 MetazoneMembershipKind::BehavesLikeGolden => 0b00u8,
230 MetazoneMembershipKind::CustomTransitions => 0b10,
231 MetazoneMembershipKind::CustomVariants => 0b01,
232 } << 6) as i8,
233 ]
234 }
235}
236
237#[cfg(all(feature = "alloc", feature = "serde"))]
238impl serde::Serialize for VariantOffsetsWithMetazoneMembershipKind {
239 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
240 where
241 S: serde::Serializer,
242 {
243 self.to_unaligned().serialize(serializer)
244 }
245}
246
247#[cfg(feature = "serde")]
248impl<'de> serde::Deserialize<'de> for VariantOffsetsWithMetazoneMembershipKind {
249 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
250 where
251 D: serde::Deserializer<'de>,
252 {
253 <_>::deserialize(deserializer).map(Self::from_unaligned)
254 }
255}
256
257#[test]
258fn offsets_ule() {
259 #[track_caller]
260 fn assert_round_trip(offset: UtcOffset) {
261 let variants = VariantOffsets::from_standard(offset);
262 assert_eq!(
263 variants,
264 VariantOffsets::from_unaligned(VariantOffsets::to_unaligned(variants))
265 );
266 }
267
268 assert_round_trip(UtcOffset::try_from_str("+01:00").unwrap());
269 assert_round_trip(UtcOffset::try_from_str("+01:15").unwrap());
270 assert_round_trip(UtcOffset::try_from_str("+01:30").unwrap());
271 assert_round_trip(UtcOffset::try_from_str("+01:45").unwrap());
272
273 assert_round_trip(UtcOffset::try_from_str("+01:10").unwrap());
274 assert_round_trip(UtcOffset::try_from_str("+01:20").unwrap());
275 assert_round_trip(UtcOffset::try_from_str("+01:40").unwrap());
276 assert_round_trip(UtcOffset::try_from_str("+01:50").unwrap());
277
278 assert_round_trip(UtcOffset::try_from_str("-01:00").unwrap());
279 assert_round_trip(UtcOffset::try_from_str("-01:15").unwrap());
280 assert_round_trip(UtcOffset::try_from_str("-01:30").unwrap());
281 assert_round_trip(UtcOffset::try_from_str("-01:45").unwrap());
282
283 assert_round_trip(UtcOffset::try_from_str("-01:10").unwrap());
284 assert_round_trip(UtcOffset::try_from_str("-01:20").unwrap());
285 assert_round_trip(UtcOffset::try_from_str("-01:40").unwrap());
286 assert_round_trip(UtcOffset::try_from_str("-01:50").unwrap());
287}
288
289#[cfg(feature = "alloc")]
290impl<'a> zerovec::maps::ZeroMapKV<'a> for VariantOffsets {
291 type Container = ZeroVec<'a, Self>;
292 type Slice = ZeroSlice<Self>;
293 type GetType = <Self as AsULE>::ULE;
294 type OwnedType = Self;
295}
296
297#[cfg(all(feature = "alloc", feature = "serde"))]
298impl serde::Serialize for VariantOffsets {
299 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
300 where
301 S: serde::Serializer,
302 {
303 if serializer.is_human_readable() {
304 use alloc::fmt::Write;
305 let mut r = alloc::format!(
306 "{:+02}:{:02}",
307 self.standard.hours_part(),
308 self.standard.minutes_part(),
309 );
310 if self.standard.seconds_part() != 0 {
311 let _infallible = write!(&mut r, ":{:02}", self.standard.seconds_part());
312 }
313 if let Some(dst) = self.daylight {
314 let _infallible = write!(
315 &mut r,
316 "/{:+02}:{:02}",
317 dst.hours_part(),
318 dst.minutes_part(),
319 );
320
321 if dst.seconds_part() != 0 {
322 let _infallible = write!(&mut r, ":{:02}", dst.seconds_part());
323 }
324 }
325
326 serializer.serialize_str(&r)
327 } else {
328 self.to_unaligned().serialize(serializer)
329 }
330 }
331}
332
333#[cfg(feature = "serde")]
334impl<'de> serde::Deserialize<'de> for VariantOffsets {
335 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
336 where
337 D: serde::Deserializer<'de>,
338 {
339 use serde::de::Error;
340 if deserializer.is_human_readable() {
341 let raw = <&str>::deserialize(deserializer)?;
342 Ok(if let Some((std, dst)) = raw.split_once('/') {
343 Self {
344 standard: UtcOffset::try_from_str(std)
345 .map_err(|_| D::Error::custom("invalid offset"))?,
346 daylight: Some(
347 UtcOffset::try_from_str(dst)
348 .map_err(|_| D::Error::custom("invalid offset"))?,
349 ),
350 }
351 } else {
352 Self {
353 standard: UtcOffset::try_from_str(raw)
354 .map_err(|_| D::Error::custom("invalid offset"))?,
355 daylight: None,
356 }
357 })
358 } else {
359 <_>::deserialize(deserializer).map(Self::from_unaligned)
360 }
361 }
362}
363
364pub type MetazoneId = core::num::NonZeroU8;
372
373#[derive(PartialEq, Debug, Clone, yoke::Yokeable, zerofrom::ZeroFrom)]
375#[cfg_attr(feature = "datagen", derive(databake::Bake))]
376#[cfg_attr(feature = "datagen", databake(path = icu_time::provider))]
377pub struct TimezonePeriods<'a> {
378 pub index: ZeroTrieSimpleAscii<ZeroVec<'a, u8>>,
380 pub list: VarZeroVec<
389 'a,
390 VarTupleULE<
391 (u8, NichedOption<MetazoneId, 1>),
392 ZeroSlice<(Timestamp24, u8, NichedOption<MetazoneId, 1>)>,
393 >,
394 >,
395
396 pub offsets: ZeroVec<'a, VariantOffsetsWithMetazoneMembershipKind>,
400}
401
402#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
404#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
405#[cfg_attr(feature = "datagen", derive(serde::Serialize))]
406pub struct Timestamp24(pub ZoneNameTimestamp);
407
408impl AsULE for Timestamp24 {
409 type ULE = RawBytesULE<3>;
410 #[inline]
411 fn to_unaligned(self) -> Self::ULE {
412 let RawBytesULE([a, b, c, _]) = self.0.to_unaligned();
413 RawBytesULE([a, b, c])
414 }
415 #[inline]
416 fn from_unaligned(RawBytesULE([a, b, c]): Self::ULE) -> Self {
417 Self(ZoneNameTimestamp::from_unaligned(RawBytesULE([a, b, c, 0])))
418 }
419}
420
421#[cfg(feature = "serde")]
422#[derive(serde::Deserialize)]
423#[cfg_attr(feature = "datagen", derive(serde::Serialize))]
424struct TimeZonePeriodsSerde<'a> {
425 #[serde(borrow)]
426 pub index: ZeroTrieSimpleAscii<ZeroVec<'a, u8>>,
427 #[serde(borrow)]
428 pub list: VarZeroVec<
429 'a,
430 VarTupleULE<
431 (u8, NichedOption<MetazoneId, 1>),
432 ZeroSlice<(Timestamp24, u8, NichedOption<MetazoneId, 1>)>,
433 >,
434 >,
435
436 pub offsets: ZeroVec<'a, VariantOffsetsWithMetazoneMembershipKind>,
437}
438
439#[cfg(feature = "datagen")]
440impl serde::Serialize for TimezonePeriods<'_> {
441 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
442 where
443 S: serde::Serializer,
444 {
445 use serde::ser::SerializeMap;
446 if serializer.is_human_readable() {
447 let mut map = serializer.serialize_map(None)?;
448 for (tz, idx) in self.index.iter() {
449 if let Some(value) = self.list.get(idx) {
450 map.serialize_entry(
451 &tz,
452 &[ZoneNameTimestamp::far_in_past()]
453 .into_iter()
454 .chain(value.variable.iter().map(|(t, _, _)| t.0))
455 .map(|t| {
456 use icu_locale_core::subtags::Subtag;
457
458 #[allow(clippy::unwrap_used)] let (os, mz_info) = self
460 .get(TimeZone(Subtag::try_from_str(&tz).unwrap()), t)
461 .unwrap();
462 (
463 t,
464 (
465 os,
466 mz_info.map(|i| {
467 (
468 i.id,
469 match i.kind {
470 MetazoneMembershipKind::BehavesLikeGolden => {
471 [].as_slice()
472 }
473 MetazoneMembershipKind::CustomVariants => {
474 &["custom variants"]
475 }
476 MetazoneMembershipKind::CustomTransitions => {
477 &["custom transitions"]
478 }
479 },
480 )
481 }),
482 ),
483 )
484 })
485 .collect::<alloc::collections::BTreeMap<_, _>>(),
486 )?;
487 }
488 }
489 map.end()
490 } else {
491 TimeZonePeriodsSerde {
492 list: self.list.clone(),
493 index: self.index.clone(),
494 offsets: self.offsets.clone(),
495 }
496 .serialize(serializer)
497 }
498 }
499}
500
501#[cfg(feature = "serde")]
502impl<'de> serde::Deserialize<'de> for TimezonePeriods<'de> {
503 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
504 where
505 D: serde::Deserializer<'de>,
506 {
507 use serde::de::Error;
508 if deserializer.is_human_readable() {
509 Err(D::Error::custom("not yet supported; see icu4x#6752"))
511 } else {
512 let TimeZonePeriodsSerde {
513 index,
514 list,
515 offsets,
516 } = TimeZonePeriodsSerde::deserialize(deserializer)?;
517 Ok(Self {
518 index,
519 list,
520 offsets,
521 })
522 }
523 }
524}
525
526#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
528pub struct MetazoneInfo {
529 pub id: MetazoneId,
531 pub kind: MetazoneMembershipKind,
533}
534
535impl TimezonePeriods<'_> {
536 pub fn get(
541 &self,
542 time_zone_id: TimeZone,
543 timestamp: ZoneNameTimestamp,
544 ) -> Option<(VariantOffsets, Option<MetazoneInfo>)> {
545 let (os_idx, NichedOption(mz)) =
546 self.find_period(self.index.get(time_zone_id.as_str())?, timestamp)?;
547
548 let os = self.offsets.get(os_idx as usize)?;
549
550 let Some(mz) = mz else {
551 return Some((os.offsets, None));
552 };
553
554 Some((
555 os.offsets,
556 Some(MetazoneInfo {
557 id: mz,
558 kind: os.mzmsk,
559 }),
560 ))
561 }
562
563 fn find_period(
565 &self,
566 idx: usize,
567 timestamp: ZoneNameTimestamp,
568 ) -> Option<(u8, NichedOption<MetazoneId, 1>)> {
569 use zerovec::ule::vartuple::VarTupleULE;
570 use zerovec::ule::AsULE;
571 let &VarTupleULE {
572 sized: first,
573 variable: ref rest,
574 } = self.list.get(idx)?;
575
576 let i = match rest.binary_search_by(|(t, ..)| t.cmp(&Timestamp24(timestamp))) {
577 Err(0) => return Some(<(u8, NichedOption<MetazoneId, 1>)>::from_unaligned(first)),
578 Err(i) => i - 1,
579 Ok(i) => i,
580 };
581 let (_, os, mz) = rest.get(i)?;
582 Some((os, mz))
583 }
584}
585
586icu_provider::data_struct!(
587 TimezonePeriods<'_>,
588 #[cfg(feature = "datagen")]
589);
590
591icu_provider::data_marker!(
592 TimezonePeriodsV1,
594 TimezonePeriods<'static>,
595 is_singleton = true,
596 has_checksum = true
597);
598
599impl AsULE for VariantOffsets {
600 type ULE = [i8; 2];
601
602 fn from_unaligned([std, dst]: Self::ULE) -> Self {
603 fn decode(encoded: i8) -> i32 {
604 encoded as i32 * SECONDS_TO_EIGHTS_OF_HOURS
605 + match encoded % 8 {
606 1 | 5 => 150,
608 -1 | -5 => -150,
609 3 | 7 => -150,
611 -3 | -7 => 150,
612 _ => 0,
614 }
615 }
616
617 Self {
618 standard: UtcOffset::from_seconds_unchecked(decode(std)),
619 daylight: (dst != 0).then(|| UtcOffset::from_seconds_unchecked(decode(std + dst))),
620 }
621 }
622
623 fn to_unaligned(self) -> Self::ULE {
624 fn encode(offset: i32) -> i8 {
625 debug_assert_eq!(offset.abs() % 60, 0);
626 let scaled = match offset.abs() / 60 % 60 {
627 0 | 15 | 30 | 45 => offset / SECONDS_TO_EIGHTS_OF_HOURS,
628 10 | 40 => {
629 offset / SECONDS_TO_EIGHTS_OF_HOURS
631 }
632 20 | 50 => {
633 offset / SECONDS_TO_EIGHTS_OF_HOURS + offset.signum()
635 }
636 _ => {
637 debug_assert!(false, "{offset:?}");
638 offset / SECONDS_TO_EIGHTS_OF_HOURS
639 }
640 };
641 debug_assert!(i8::MIN as i32 <= scaled && scaled <= i8::MAX as i32);
642 scaled as i8
643 }
644 [
645 encode(self.standard.to_seconds()),
646 self.daylight
647 .map(|d| encode(d.to_seconds() - self.standard.to_seconds()))
648 .unwrap_or_default(),
649 ]
650 }
651}
652
653#[cfg(feature = "alloc")]
655pub(crate) mod legacy {
656 use super::*;
657 use zerovec::ZeroMap2d;
658
659 icu_provider::data_marker!(
660 TimezoneVariantsOffsetsV1,
663 "timezone/variants/offsets/v1",
664 ZeroMap2d<'static, TimeZone, ZoneNameTimestamp, VariantOffsets>,
665 is_singleton = true
666 );
667}