icu_time/provider/
mod.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5// Provider structs must be stable
6#![allow(clippy::exhaustive_structs, clippy::exhaustive_enums)]
7
8//! 🚧 \[Unstable\] Data provider struct definitions for this ICU4X component.
9//!
10//! <div class="stab unstable">
11//! 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
12//! including in SemVer minor releases. While the serde representation of data structs is guaranteed
13//! to be stable, their Rust representation might not be. Use with caution.
14//! </div>
15//!
16//! Read more about data providers: [`icu_provider`]
17
18use crate::zone::{UtcOffset, VariantOffsets, ZoneNameTimestamp};
19#[cfg(feature = "datagen")]
20use icu_provider::prelude::*;
21use zerovec::maps::ZeroMapKV;
22use zerovec::ule::AsULE;
23use zerovec::{ZeroMap2d, ZeroSlice, ZeroVec};
24
25pub use crate::zone::ule::TimeZoneVariantULE;
26pub use crate::zone::TimeZone;
27pub mod iana;
28pub mod windows;
29
30#[cfg(feature = "compiled_data")]
31#[derive(Debug)]
32/// Baked data
33///
34/// <div class="stab unstable">
35/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
36/// including in SemVer minor releases. In particular, the `DataProvider` implementations are only
37/// guaranteed to match with this version's `*_unstable` providers. Use with caution.
38/// </div>
39pub 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_variants_offsets_v1!(Baked);
53};
54
55#[cfg(feature = "datagen")]
56/// The latest minimum set of markers required by this component.
57pub const MARKERS: &[DataMarkerInfo] = &[
58    iana::TimezoneIdentifiersIanaExtendedV1::INFO,
59    iana::TimezoneIdentifiersIanaCoreV1::INFO,
60    windows::TimezoneIdentifiersWindowsV1::INFO,
61    TimezoneVariantsOffsetsV1::INFO,
62];
63
64const SECONDS_TO_EIGHTS_OF_HOURS: i32 = 60 * 60 / 8;
65
66impl AsULE for VariantOffsets {
67    type ULE = [i8; 2];
68
69    fn from_unaligned([std, dst]: Self::ULE) -> Self {
70        fn decode(encoded: i8) -> i32 {
71            encoded as i32 * SECONDS_TO_EIGHTS_OF_HOURS
72                + match encoded % 8 {
73                    // 7.5, 37.5, representing 10, 40
74                    1 | 5 => 150,
75                    -1 | -5 => -150,
76                    // 22.5, 52.5, representing 20, 50
77                    3 | 7 => -150,
78                    -3 | -7 => 150,
79                    // 0, 15, 30, 45
80                    _ => 0,
81                }
82        }
83
84        Self {
85            standard: UtcOffset::from_seconds_unchecked(decode(std)),
86            daylight: (dst != 0).then(|| UtcOffset::from_seconds_unchecked(decode(std + dst))),
87        }
88    }
89
90    fn to_unaligned(self) -> Self::ULE {
91        fn encode(offset: i32) -> i8 {
92            debug_assert_eq!(offset.abs() % 60, 0);
93            let scaled = match offset.abs() / 60 % 60 {
94                0 | 15 | 30 | 45 => offset / SECONDS_TO_EIGHTS_OF_HOURS,
95                10 | 40 => {
96                    // stored as 7.5, 37.5, truncating div
97                    offset / SECONDS_TO_EIGHTS_OF_HOURS
98                }
99                20 | 50 => {
100                    // stored as 22.5, 52.5, need to add one
101                    offset / SECONDS_TO_EIGHTS_OF_HOURS + offset.signum()
102                }
103                _ => {
104                    debug_assert!(false, "{offset:?}");
105                    offset / SECONDS_TO_EIGHTS_OF_HOURS
106                }
107            };
108            debug_assert!(i8::MIN as i32 <= scaled && scaled <= i8::MAX as i32);
109            scaled as i8
110        }
111        [
112            encode(self.standard.to_seconds()),
113            self.daylight
114                .map(|d| encode(d.to_seconds() - self.standard.to_seconds()))
115                .unwrap_or_default(),
116        ]
117    }
118}
119
120#[test]
121fn offsets_ule() {
122    #[track_caller]
123    fn assert_round_trip(offset: UtcOffset) {
124        let variants = VariantOffsets::from_standard(offset);
125        assert_eq!(
126            variants,
127            VariantOffsets::from_unaligned(VariantOffsets::to_unaligned(variants))
128        );
129    }
130
131    assert_round_trip(UtcOffset::try_from_str("+01:00").unwrap());
132    assert_round_trip(UtcOffset::try_from_str("+01:15").unwrap());
133    assert_round_trip(UtcOffset::try_from_str("+01:30").unwrap());
134    assert_round_trip(UtcOffset::try_from_str("+01:45").unwrap());
135
136    assert_round_trip(UtcOffset::try_from_str("+01:10").unwrap());
137    assert_round_trip(UtcOffset::try_from_str("+01:20").unwrap());
138    assert_round_trip(UtcOffset::try_from_str("+01:40").unwrap());
139    assert_round_trip(UtcOffset::try_from_str("+01:50").unwrap());
140
141    assert_round_trip(UtcOffset::try_from_str("-01:00").unwrap());
142    assert_round_trip(UtcOffset::try_from_str("-01:15").unwrap());
143    assert_round_trip(UtcOffset::try_from_str("-01:30").unwrap());
144    assert_round_trip(UtcOffset::try_from_str("-01:45").unwrap());
145
146    assert_round_trip(UtcOffset::try_from_str("-01:10").unwrap());
147    assert_round_trip(UtcOffset::try_from_str("-01:20").unwrap());
148    assert_round_trip(UtcOffset::try_from_str("-01:40").unwrap());
149    assert_round_trip(UtcOffset::try_from_str("-01:50").unwrap());
150}
151
152impl<'a> ZeroMapKV<'a> for VariantOffsets {
153    type Container = ZeroVec<'a, Self>;
154    type Slice = ZeroSlice<Self>;
155    type GetType = <Self as AsULE>::ULE;
156    type OwnedType = Self;
157}
158
159#[cfg(all(feature = "alloc", feature = "serde"))]
160impl serde::Serialize for VariantOffsets {
161    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162    where
163        S: serde::Serializer,
164    {
165        if serializer.is_human_readable() {
166            serializer.serialize_str(&if let Some(dst) = self.daylight {
167                alloc::format!(
168                    "{:+02}:{:02}/{:+02}:{:02}",
169                    self.standard.hours_part(),
170                    self.standard.minutes_part(),
171                    dst.hours_part(),
172                    dst.minutes_part(),
173                )
174            } else {
175                alloc::format!(
176                    "{:+02}:{:02}",
177                    self.standard.hours_part(),
178                    self.standard.minutes_part(),
179                )
180            })
181        } else {
182            self.to_unaligned().serialize(serializer)
183        }
184    }
185}
186
187#[cfg(feature = "serde")]
188impl<'de> serde::Deserialize<'de> for VariantOffsets {
189    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
190    where
191        D: serde::Deserializer<'de>,
192    {
193        use serde::de::Error;
194        if deserializer.is_human_readable() {
195            let raw = <&str>::deserialize(deserializer)?;
196            Ok(if let Some((std, dst)) = raw.split_once('/') {
197                Self {
198                    standard: UtcOffset::try_from_str(std)
199                        .map_err(|_| D::Error::custom("invalid offset"))?,
200                    daylight: Some(
201                        UtcOffset::try_from_str(dst)
202                            .map_err(|_| D::Error::custom("invalid offset"))?,
203                    ),
204                }
205            } else {
206                Self {
207                    standard: UtcOffset::try_from_str(raw)
208                        .map_err(|_| D::Error::custom("invalid offset"))?,
209                    daylight: None,
210                }
211            })
212        } else {
213            <_>::deserialize(deserializer).map(Self::from_unaligned)
214        }
215    }
216}
217
218icu_provider::data_marker!(
219    /// The default mapping between period and offsets. The second level key is a wall-clock time encoded as
220    /// [`ZoneNameTimestamp`]. It represents when the offsets started to be used.
221    ///
222    /// The values are the standard offset, and the daylight offset *relative to the standard offset*. As such,
223    /// if the second value is 0, there is no daylight time.
224    TimezoneVariantsOffsetsV1,
225    "timezone/variants/offsets/v1",
226    ZeroMap2d<'static, TimeZone, ZoneNameTimestamp, VariantOffsets>,
227    is_singleton = true
228);