icu_time/zone/
offset.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
5use core::str::FromStr;
6
7#[cfg(feature = "alloc")]
8use crate::provider::legacy::TimezoneVariantsOffsetsV1;
9use crate::provider::{TimezonePeriods, TimezonePeriodsV1};
10use crate::TimeZone;
11use icu_provider::prelude::*;
12
13use displaydoc::Display;
14
15use super::ZoneNameTimestamp;
16
17/// The time zone offset was invalid. Must be within ±18:00:00.
18#[derive(Display, Debug, Copy, Clone, PartialEq)]
19#[allow(clippy::exhaustive_structs)]
20pub struct InvalidOffsetError;
21
22/// An offset from Coordinated Universal Time (UTC).
23///
24/// Supports ±18:00:00.
25///
26/// **The primary definition of this type is in the [`icu_time`](https://docs.rs/icu_time) crate. Other ICU4X crates re-export it for convenience.**
27#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, PartialOrd, Ord)]
28pub struct UtcOffset(i32);
29
30impl UtcOffset {
31    /// Attempt to create a [`UtcOffset`] from a seconds input.
32    ///
33    /// Returns [`InvalidOffsetError`] if the seconds are out of bounds.
34    pub fn try_from_seconds(seconds: i32) -> Result<Self, InvalidOffsetError> {
35        if seconds.unsigned_abs() > 18 * 60 * 60 {
36            Err(InvalidOffsetError)
37        } else {
38            Ok(Self(seconds))
39        }
40    }
41
42    /// Creates a [`UtcOffset`] of zero.
43    pub const fn zero() -> Self {
44        Self(0)
45    }
46
47    /// Parse a [`UtcOffset`] from bytes.
48    ///
49    /// The offset must range from UTC-12 to UTC+14.
50    ///
51    /// The string must be an ISO-8601 time zone designator:
52    /// e.g. Z
53    /// e.g. +05
54    /// e.g. +0500
55    /// e.g. +05:00
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use icu::time::zone::UtcOffset;
61    ///
62    /// let offset0: UtcOffset = UtcOffset::try_from_str("Z").unwrap();
63    /// let offset1: UtcOffset = UtcOffset::try_from_str("+05").unwrap();
64    /// let offset2: UtcOffset = UtcOffset::try_from_str("+0500").unwrap();
65    /// let offset3: UtcOffset = UtcOffset::try_from_str("-05:00").unwrap();
66    ///
67    /// let offset_err0 =
68    ///     UtcOffset::try_from_str("0500").expect_err("Invalid input");
69    /// let offset_err1 =
70    ///     UtcOffset::try_from_str("+05000").expect_err("Invalid input");
71    ///
72    /// assert_eq!(offset0.to_seconds(), 0);
73    /// assert_eq!(offset1.to_seconds(), 18000);
74    /// assert_eq!(offset2.to_seconds(), 18000);
75    /// assert_eq!(offset3.to_seconds(), -18000);
76    /// ```
77    #[inline]
78    pub fn try_from_str(s: &str) -> Result<Self, InvalidOffsetError> {
79        Self::try_from_utf8(s.as_bytes())
80    }
81
82    /// See [`Self::try_from_str`]
83    pub fn try_from_utf8(mut code_units: &[u8]) -> Result<Self, InvalidOffsetError> {
84        fn try_get_time_component([tens, ones]: [u8; 2]) -> Option<i32> {
85            Some(((tens as char).to_digit(10)? * 10 + (ones as char).to_digit(10)?) as i32)
86        }
87
88        let offset_sign = match code_units {
89            [b'+', rest @ ..] => {
90                code_units = rest;
91                1
92            }
93            [b'-', rest @ ..] => {
94                code_units = rest;
95                -1
96            }
97            // Unicode minus ("\u{2212}" == [226, 136, 146])
98            [226, 136, 146, rest @ ..] => {
99                code_units = rest;
100                -1
101            }
102            [b'Z'] => return Ok(Self(0)),
103            _ => return Err(InvalidOffsetError),
104        };
105
106        let hours = match code_units {
107            &[h1, h2, ..] => try_get_time_component([h1, h2]),
108            _ => None,
109        }
110        .ok_or(InvalidOffsetError)?;
111
112        let minutes = match code_units {
113            /* ±hh */
114            &[_, _] => Some(0),
115            /* ±hhmm, ±hh:mm */
116            &[_, _, m1, m2] | &[_, _, b':', m1, m2] => {
117                try_get_time_component([m1, m2]).filter(|&m| m < 60)
118            }
119            _ => None,
120        }
121        .ok_or(InvalidOffsetError)?;
122
123        Self::try_from_seconds(offset_sign * (hours * 60 + minutes) * 60)
124    }
125
126    /// Create a [`UtcOffset`] from a seconds input without checking bounds.
127    #[inline]
128    pub const fn from_seconds_unchecked(seconds: i32) -> Self {
129        Self(seconds)
130    }
131
132    /// Returns the raw offset value in seconds.
133    pub const fn to_seconds(self) -> i32 {
134        self.0
135    }
136
137    /// Whether the [`UtcOffset`] is non-negative.
138    pub fn is_non_negative(self) -> bool {
139        self.0 >= 0
140    }
141
142    /// Whether the [`UtcOffset`] is zero.
143    pub fn is_zero(self) -> bool {
144        self.0 == 0
145    }
146
147    /// Returns the hours part of if the [`UtcOffset`]
148    pub fn hours_part(self) -> i32 {
149        self.0 / 3600
150    }
151
152    /// Returns the minutes part of if the [`UtcOffset`].
153    pub fn minutes_part(self) -> u32 {
154        (self.0 % 3600 / 60).unsigned_abs()
155    }
156
157    /// Returns the seconds part of if the [`UtcOffset`].
158    pub fn seconds_part(self) -> u32 {
159        (self.0 % 60).unsigned_abs()
160    }
161}
162
163impl FromStr for UtcOffset {
164    type Err = InvalidOffsetError;
165
166    #[inline]
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        Self::try_from_str(s)
169    }
170}
171
172#[derive(Debug)]
173enum OffsetData {
174    #[cfg(feature = "alloc")] // doesn't alloc, but ZeroMap are behind the alloc feature
175    Old(DataPayload<TimezoneVariantsOffsetsV1>),
176    New(DataPayload<TimezonePeriodsV1>),
177}
178
179#[derive(Debug)]
180enum OffsetDataBorrowed<'a> {
181    #[cfg(feature = "alloc")]
182    Old(&'a zerovec::ZeroMap2d<'a, TimeZone, ZoneNameTimestamp, VariantOffsets>),
183    New(&'a TimezonePeriods<'a>),
184}
185
186/// [`VariantOffsetsCalculator`] uses data from the [data provider] to calculate time zone offsets.
187///
188/// [data provider]: icu_provider
189#[derive(Debug)]
190#[deprecated(
191    since = "2.1.0",
192    note = "this API is a bad approximation of a time zone database"
193)]
194pub struct VariantOffsetsCalculator {
195    offset_period: OffsetData,
196}
197
198/// The borrowed version of a  [`VariantOffsetsCalculator`]
199#[derive(Debug)]
200#[deprecated(
201    since = "2.1.0",
202    note = "this API is a bad approximation of a time zone database"
203)]
204pub struct VariantOffsetsCalculatorBorrowed<'a> {
205    offset_period: OffsetDataBorrowed<'a>,
206}
207
208#[cfg(feature = "compiled_data")]
209#[allow(deprecated)]
210impl Default for VariantOffsetsCalculatorBorrowed<'static> {
211    fn default() -> Self {
212        VariantOffsetsCalculator::new()
213    }
214}
215
216#[allow(deprecated)]
217impl VariantOffsetsCalculator {
218    /// Constructs a `VariantOffsetsCalculator` using compiled data.
219    ///
220    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
221    ///
222    /// [📚 Help choosing a constructor](icu_provider::constructors)
223    #[cfg(feature = "compiled_data")]
224    #[inline]
225    #[expect(clippy::new_ret_no_self)]
226    pub const fn new() -> VariantOffsetsCalculatorBorrowed<'static> {
227        VariantOffsetsCalculatorBorrowed::new()
228    }
229
230    #[cfg(feature = "serde")]
231    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER, Self::new)]
232    pub fn try_new_with_buffer_provider(
233        provider: &(impl icu_provider::buf::BufferProvider + ?Sized),
234    ) -> Result<Self, DataError> {
235        use icu_provider::buf::AsDeserializingBufferProvider;
236        {
237            Ok(Self {
238                offset_period: match DataProvider::<TimezonePeriodsV1>::load(
239                    &provider.as_deserializing(),
240                    Default::default(),
241                ) {
242                    Ok(payload) => OffsetData::New(payload.payload),
243                    Err(_e) => {
244                        #[cfg(feature = "alloc")]
245                        {
246                            OffsetData::Old(
247                                DataProvider::<TimezoneVariantsOffsetsV1>::load(
248                                    &provider.as_deserializing(),
249                                    Default::default(),
250                                )?
251                                .payload,
252                            )
253                        }
254                        #[cfg(not(feature = "alloc"))]
255                        return Err(_e);
256                    }
257                },
258            })
259        }
260    }
261
262    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
263    pub fn try_new_unstable(
264        provider: &(impl DataProvider<TimezonePeriodsV1> + ?Sized),
265    ) -> Result<Self, DataError> {
266        let offset_period = provider.load(Default::default())?.payload;
267        Ok(Self {
268            offset_period: OffsetData::New(offset_period),
269        })
270    }
271
272    /// Returns a borrowed version of the calculator that can be queried.
273    ///
274    /// This avoids a small potential indirection cost when querying.
275    pub fn as_borrowed(&self) -> VariantOffsetsCalculatorBorrowed<'_> {
276        VariantOffsetsCalculatorBorrowed {
277            offset_period: match self.offset_period {
278                OffsetData::New(ref payload) => OffsetDataBorrowed::New(payload.get()),
279                #[cfg(feature = "alloc")]
280                OffsetData::Old(ref payload) => OffsetDataBorrowed::Old(payload.get()),
281            },
282        }
283    }
284}
285
286#[allow(deprecated)]
287impl VariantOffsetsCalculatorBorrowed<'static> {
288    /// Constructs a `VariantOffsetsCalculatorBorrowed` using compiled data.
289    ///
290    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
291    ///
292    /// [📚 Help choosing a constructor](icu_provider::constructors)
293    #[cfg(feature = "compiled_data")]
294    #[inline]
295    pub const fn new() -> Self {
296        Self {
297            offset_period: OffsetDataBorrowed::New(
298                crate::provider::Baked::SINGLETON_TIMEZONE_PERIODS_V1,
299            ),
300        }
301    }
302
303    /// Cheaply converts a [`VariantOffsetsCalculatorBorrowed<'static>`] into a [`VariantOffsetsCalculator`].
304    ///
305    /// Note: Due to branching and indirection, using [`VariantOffsetsCalculator`] might inhibit some
306    /// compile-time optimizations that are possible with [`VariantOffsetsCalculatorBorrowed`].
307    pub fn static_to_owned(&self) -> VariantOffsetsCalculator {
308        VariantOffsetsCalculator {
309            offset_period: match self.offset_period {
310                OffsetDataBorrowed::New(p) => OffsetData::New(DataPayload::from_static_ref(p)),
311                #[cfg(feature = "alloc")]
312                OffsetDataBorrowed::Old(p) => OffsetData::Old(DataPayload::from_static_ref(p)),
313            },
314        }
315    }
316}
317
318#[allow(deprecated)]
319impl VariantOffsetsCalculatorBorrowed<'_> {
320    /// Calculate zone offsets from timezone and local datetime.
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use icu::calendar::Date;
326    /// use icu::locale::subtags::subtag;
327    /// use icu::time::zone::UtcOffset;
328    /// use icu::time::zone::VariantOffsetsCalculator;
329    /// use icu::time::zone::ZoneNameTimestamp;
330    /// use icu::time::Time;
331    /// use icu::time::TimeZone;
332    ///
333    /// let zoc = VariantOffsetsCalculator::new();
334    ///
335    /// // America/Denver observes DST
336    /// let offsets = zoc
337    ///     .compute_offsets_from_time_zone_and_name_timestamp(
338    ///         TimeZone(subtag!("usden")),
339    ///         ZoneNameTimestamp::far_in_future(),
340    ///     )
341    ///     .unwrap();
342    /// assert_eq!(
343    ///     offsets.standard,
344    ///     UtcOffset::try_from_seconds(-7 * 3600).unwrap()
345    /// );
346    /// assert_eq!(
347    ///     offsets.daylight,
348    ///     Some(UtcOffset::try_from_seconds(-6 * 3600).unwrap())
349    /// );
350    ///
351    /// // America/Phoenix does not
352    /// let offsets = zoc
353    ///     .compute_offsets_from_time_zone_and_name_timestamp(
354    ///         TimeZone(subtag!("usphx")),
355    ///         ZoneNameTimestamp::far_in_future(),
356    ///     )
357    ///     .unwrap();
358    /// assert_eq!(
359    ///     offsets.standard,
360    ///     UtcOffset::try_from_seconds(-7 * 3600).unwrap()
361    /// );
362    /// assert_eq!(offsets.daylight, None);
363    /// ```
364    pub fn compute_offsets_from_time_zone_and_name_timestamp(
365        &self,
366        time_zone_id: TimeZone,
367        timestamp: ZoneNameTimestamp,
368    ) -> Option<VariantOffsets> {
369        match self.offset_period {
370            OffsetDataBorrowed::New(p) => p.get(time_zone_id, timestamp).map(|(os, _)| os),
371            #[cfg(feature = "alloc")]
372            OffsetDataBorrowed::Old(p) => {
373                use zerovec::ule::AsULE;
374                let mut offsets = None;
375                for (bytes, id) in p.get0(&time_zone_id)?.iter1_copied().rev() {
376                    if timestamp >= ZoneNameTimestamp::from_unaligned(*bytes) {
377                        offsets = Some(id);
378                        break;
379                    }
380                }
381                Some(offsets?)
382            }
383        }
384    }
385}
386
387#[deprecated(
388    since = "2.1.0",
389    note = "this API is a bad approximation of a time zone database"
390)]
391pub use crate::provider::VariantOffsets;
392
393#[test]
394#[allow(deprecated)]
395pub fn test_legacy_offsets_data() {
396    use crate::ZonedDateTime;
397    use icu_locale_core::subtags::subtag;
398    use icu_provider_blob::BlobDataProvider;
399
400    let c = VariantOffsetsCalculator::try_new_with_buffer_provider(
401        &BlobDataProvider::try_new_from_static_blob(
402            // icu4x-datagen --markers TimezoneVariantsOffsetsV1 --format blob
403            include_bytes!("../../tests/data/offset_periods_old.blob"),
404        )
405        .unwrap(),
406    )
407    .unwrap();
408
409    let tz = TimeZone(subtag!("aqcas"));
410
411    for timestamp in [
412        "1970-01-01 00:00Z",
413        "2009-10-17 18:00Z",
414        "2010-03-04 15:00Z",
415        "2011-10-27 18:00Z",
416        "2012-02-21 17:00Z",
417        "2016-10-21 16:00Z",
418        "2018-03-10 17:00Z",
419        "2018-10-06 20:00Z",
420        "2019-03-16 16:00Z",
421        "2019-10-03 19:00Z",
422        "2020-03-07 16:00Z",
423        "2021-03-13 13:00Z",
424        "2022-03-12 13:00Z",
425        "2023-03-08 16:00Z",
426    ] {
427        let t = ZoneNameTimestamp::from_zoned_date_time_iso(
428            ZonedDateTime::try_offset_only_from_str(timestamp, icu_calendar::Iso).unwrap(),
429        );
430
431        assert_eq!(
432            c.as_borrowed()
433                .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
434            VariantOffsetsCalculator::new()
435                .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
436            "{timestamp:?}",
437        );
438    }
439}