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#![allow(clippy::type_complexity)]
8
9//! 🚧 \[Unstable\] Data provider struct definitions for this ICU4X component.
10//!
11//! <div class="stab unstable">
12//! 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
13//! including in SemVer minor releases. While the serde representation of data structs is guaranteed
14//! to be stable, their Rust representation might not be. Use with caution.
15//! </div>
16//!
17//! Read more about data providers: [`icu_provider`]
18
19use 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)]
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_periods_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    TimezonePeriodsV1::INFO,
62];
63
64const SECONDS_TO_EIGHTS_OF_HOURS: i32 = 60 * 60 / 8;
65
66/// A time zone variant used to identify a display name in CLDR.
67#[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    /// The variant corresponding to `"standard"` in CLDR.
77    ///
78    /// The semantics vary from time zone to time zone. The time zone display
79    /// name of this variant may or may not be called "Standard Time".
80    Standard = 0,
81    /// The variant corresponding to `"daylight"` in CLDR.
82    ///
83    /// The semantics vary from time zone to time zone. The time zone display
84    /// name of this variant may or may not be called "Daylight Time".
85    Daylight = 1,
86}
87
88/// Metadata about a metazone membership
89#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
90#[non_exhaustive]
91pub enum MetazoneMembershipKind {
92    /// This zone is equivalent to the metazone's golden time zone.
93    BehavesLikeGolden,
94    /// This zone uses variants that the golden zone does not use.
95    /// This happens for example for London, Dublin, Troll (all in GMT), Windhoek (in WAT).
96    CustomVariants,
97    /// This zone uses different transitions than the golden zone.
98    /// This happens for example for Phoenix, Regina, Algiers, Brisbane (no DST),
99    /// or Chisinau (transitions at different times, not implemented yet).
100    CustomTransitions,
101}
102
103/// Represents the different offsets in use for a time zone
104// warning: stable (deprecated) type through reexport in crate::zone::offset
105#[non_exhaustive]
106#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
107pub struct VariantOffsets {
108    /// The standard offset.
109    pub standard: UtcOffset,
110    /// The daylight-saving offset, if used.
111    pub daylight: Option<UtcOffset>,
112}
113
114impl VariantOffsets {
115    /// Creates a new [`VariantOffsets`] from a [`UtcOffset`] representing standard time.
116    pub fn from_standard(standard: UtcOffset) -> Self {
117        Self {
118            standard,
119            daylight: None,
120        }
121    }
122}
123
124/// A [`VariantOffsets`] and a [`MetazoneMembershipKind`] packed into one byte.
125#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
126pub struct VariantOffsetsWithMetazoneMembershipKind {
127    /// The offsets. Currently uses 3 bits.
128    pub offsets: VariantOffsets,
129    /// Metazone membership metadata. Currently uses 2 bits.
130    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                    // Special bit pattern for value that appears in TZDB but is not
141                    // representable by our schema.
142                    -2670
143                } else {
144                    std as i32 * SECONDS_TO_EIGHTS_OF_HOURS
145                        + match std % 8 {
146                            // 7.5, 37.5, representing 10, 40
147                            1 | 5 => 150,
148                            -1 | -5 => -150,
149                            // 22.5, 52.5, representing 20, 50
150                            3 | 7 => -150,
151                            -3 | -7 => 150,
152                            // 0, 15, 30, 45
153                            _ => 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                // Special bit pattern for value that appears in TZDB but is not
190                // representable by our schema.
191                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                        // stored as 7.5, 37.5, truncating div
198                        offset / SECONDS_TO_EIGHTS_OF_HOURS
199                    }
200                    20 | 50 => {
201                        // stored as 22.5, 52.5, need to add one
202                        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
364/// Metazone ID in a compact format
365///
366/// <div class="stab unstable">
367/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
368/// including in SemVer minor releases. While the serde representation of data structs is guaranteed
369/// to be stable, their Rust representation might not be. Use with caution.
370/// </div>
371pub type MetazoneId = core::num::NonZeroU8;
372
373/// Data struct for the [`TimezonePeriodsV1`] marker.
374#[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    /// Index of `TimeZone`s into `list`.
379    pub index: ZeroTrieSimpleAscii<ZeroVec<'a, u8>>,
380    /// Each entry contains at least one period, which implicitly starts at the UNIX epoch.
381    /// This is stored in the first tuple element.
382    ///
383    /// If more periods are required the second tuple element contains them, along with their
384    /// starting timestamp. These entries are ordered chronologically.
385    ///
386    /// The values (`(u8, Option<MetazoneId>)`) are an index into the `offsets` list for the offset
387    /// that the zone observes in that period, and optionally whether it is part of a metazone.
388    pub list: VarZeroVec<
389        'a,
390        VarTupleULE<
391            (u8, NichedOption<MetazoneId, 1>),
392            ZeroSlice<(Timestamp24, u8, NichedOption<MetazoneId, 1>)>,
393        >,
394    >,
395
396    /// The deduplicated list of offsets.
397    ///
398    /// There are currently 99 unique VariantOffsetsWithMetazoneMembershipKind, so storing the index as a u8 is plenty enough.
399    pub offsets: ZeroVec<'a, VariantOffsetsWithMetazoneMembershipKind>,
400}
401
402/// Encodes ZoneNameTimestamp in 3 bytes by dropping the unused metadata
403#[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)] // JSON debug format
459                                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            // TODO(#6752): Add human-readable deserialization for this data
510            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/// Information about a metazone membership
527#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
528pub struct MetazoneInfo {
529    /// The metazone ID.
530    pub id: MetazoneId,
531    /// The kind.
532    pub kind: MetazoneMembershipKind,
533}
534
535impl TimezonePeriods<'_> {
536    /// Gets the information for a time zone at at timestamp
537    ///
538    /// If the timezone is in a metazone, returns the metazone ID as well as the offsets
539    /// that the metazone's golden zone currently uses.
540    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    // Given an index in `list`, returns the values at the `timestamp`
564    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    /// An ICU4X mapping to timezone offset data and metazones at a given period.
593    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                    // 7.5, 37.5, representing 10, 40
607                    1 | 5 => 150,
608                    -1 | -5 => -150,
609                    // 22.5, 52.5, representing 20, 50
610                    3 | 7 => -150,
611                    -3 | -7 => 150,
612                    // 0, 15, 30, 45
613                    _ => 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                    // stored as 7.5, 37.5, truncating div
630                    offset / SECONDS_TO_EIGHTS_OF_HOURS
631                }
632                20 | 50 => {
633                    // stored as 22.5, 52.5, need to add one
634                    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// remove in 3.0
654#[cfg(feature = "alloc")]
655pub(crate) mod legacy {
656    use super::*;
657    use zerovec::ZeroMap2d;
658
659    icu_provider::data_marker!(
660        /// The default mapping between period and offsets. The second level key is a wall-clock time encoded as
661        /// [`ZoneNameTimestamp`]. It represents when the offsets started to be used.
662        TimezoneVariantsOffsetsV1,
663        "timezone/variants/offsets/v1",
664        ZeroMap2d<'static, TimeZone, ZoneNameTimestamp, VariantOffsets>,
665        is_singleton = true
666    );
667}