Skip to main content

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
7use crate::TimeZone;
8#[cfg(feature = "alloc")]
9use crate::provider::legacy::TimezoneVariantsOffsetsV1;
10use crate::provider::{TimezonePeriods, TimezonePeriodsV1};
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 const 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 const 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 const fn try_from_utf8(mut code_units: &[u8]) -> Result<Self, InvalidOffsetError> {
84        const fn try_get_time_component([tens, ones]: [u8; 2]) -> Option<i32> {
85            let Some(tens) = (tens as char).to_digit(10) else {
86                return None;
87            };
88            let Some(ones) = (ones as char).to_digit(10) else {
89                return None;
90            };
91            Some((tens * 10 + ones) as i32)
92        }
93
94        let offset_sign = match code_units {
95            [b'+', rest @ ..] => {
96                code_units = rest;
97                1
98            }
99            [b'-', rest @ ..] => {
100                code_units = rest;
101                -1
102            }
103            // Unicode minus ("\u{2212}" == [226, 136, 146])
104            [226, 136, 146, rest @ ..] => {
105                code_units = rest;
106                -1
107            }
108            [b'Z'] => return Ok(Self(0)),
109            _ => return Err(InvalidOffsetError),
110        };
111
112        let hours = match code_units {
113            &[h1, h2, ..] => try_get_time_component([h1, h2]),
114            _ => None,
115        };
116        let Some(hours) = hours else {
117            return Err(InvalidOffsetError);
118        };
119
120        let minutes = match code_units {
121            /* ±hh */
122            &[_, _] => Some(0),
123            /* ±hhmm, ±hh:mm */
124            &[_, _, m1, m2] | &[_, _, b':', m1, m2] => try_get_time_component([m1, m2]),
125            _ => None,
126        };
127
128        let Some(minutes @ ..60) = minutes else {
129            return Err(InvalidOffsetError);
130        };
131
132        Self::try_from_seconds(offset_sign * (hours * 60 + minutes) * 60)
133    }
134
135    /// Create a [`UtcOffset`] from a seconds input without checking bounds.
136    #[inline]
137    pub const fn from_seconds_unchecked(seconds: i32) -> Self {
138        Self(seconds)
139    }
140
141    /// Returns the raw offset value in seconds.
142    pub const fn to_seconds(self) -> i32 {
143        self.0
144    }
145
146    /// Whether the [`UtcOffset`] is non-negative.
147    pub fn is_non_negative(self) -> bool {
148        self.0 >= 0
149    }
150
151    /// Whether the [`UtcOffset`] is zero.
152    pub fn is_zero(self) -> bool {
153        self.0 == 0
154    }
155
156    /// Returns the hours part of if the [`UtcOffset`]
157    pub fn hours_part(self) -> i32 {
158        self.0 / 3600
159    }
160
161    /// Returns the minutes part of if the [`UtcOffset`].
162    pub fn minutes_part(self) -> u32 {
163        (self.0 % 3600 / 60).unsigned_abs()
164    }
165
166    /// Returns the seconds part of if the [`UtcOffset`].
167    pub fn seconds_part(self) -> u32 {
168        (self.0 % 60).unsigned_abs()
169    }
170}
171
172impl FromStr for UtcOffset {
173    type Err = InvalidOffsetError;
174
175    #[inline]
176    fn from_str(s: &str) -> Result<Self, Self::Err> {
177        Self::try_from_str(s)
178    }
179}
180
181#[derive(Debug)]
182enum OffsetData {
183    #[cfg(feature = "alloc")] // doesn't alloc, but ZeroMap are behind the alloc feature
184    Old(DataPayload<TimezoneVariantsOffsetsV1>),
185    New(DataPayload<TimezonePeriodsV1>),
186}
187
188#[derive(Debug)]
189enum OffsetDataBorrowed<'a> {
190    #[cfg(feature = "alloc")]
191    Old(&'a zerovec::ZeroMap2d<'a, TimeZone, ZoneNameTimestamp, VariantOffsets>),
192    New(&'a TimezonePeriods<'a>),
193}
194
195/// [`VariantOffsetsCalculator`] uses data from the [data provider] to calculate time zone offsets.
196///
197/// [data provider]: icu_provider
198#[derive(Debug)]
199#[deprecated(
200    since = "2.1.0",
201    note = "this API is a bad approximation of a time zone database"
202)]
203pub struct VariantOffsetsCalculator {
204    offset_period: OffsetData,
205}
206
207/// The borrowed version of a  [`VariantOffsetsCalculator`]
208#[derive(Debug)]
209#[deprecated(
210    since = "2.1.0",
211    note = "this API is a bad approximation of a time zone database"
212)]
213pub struct VariantOffsetsCalculatorBorrowed<'a> {
214    offset_period: OffsetDataBorrowed<'a>,
215}
216
217#[cfg(feature = "compiled_data")]
218#[allow(deprecated)]
219impl Default for VariantOffsetsCalculatorBorrowed<'static> {
220    fn default() -> Self {
221        VariantOffsetsCalculator::new()
222    }
223}
224
225#[allow(deprecated)]
226impl VariantOffsetsCalculator {
227    /// Constructs a `VariantOffsetsCalculator` using compiled data.
228    ///
229    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
230    ///
231    /// [📚 Help choosing a constructor](icu_provider::constructors)
232    #[cfg(feature = "compiled_data")]
233    #[inline]
234    #[expect(clippy::new_ret_no_self)]
235    pub const fn new() -> VariantOffsetsCalculatorBorrowed<'static> {
236        VariantOffsetsCalculatorBorrowed::new()
237    }
238
239    #[cfg(feature = "serde")]
240    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER, Self::new)]
241    pub fn try_new_with_buffer_provider(
242        provider: &(impl BufferProvider + ?Sized),
243    ) -> Result<Self, DataError> {
244        use icu_provider::buf::AsDeserializingBufferProvider;
245        {
246            Ok(Self {
247                offset_period: match DataProvider::<TimezonePeriodsV1>::load(
248                    &provider.as_deserializing(),
249                    Default::default(),
250                ) {
251                    Ok(payload) => OffsetData::New(payload.payload),
252                    Err(_e) => {
253                        #[cfg(feature = "alloc")]
254                        {
255                            OffsetData::Old(
256                                DataProvider::<TimezoneVariantsOffsetsV1>::load(
257                                    &provider.as_deserializing(),
258                                    Default::default(),
259                                )?
260                                .payload,
261                            )
262                        }
263                        #[cfg(not(feature = "alloc"))]
264                        return Err(_e);
265                    }
266                },
267            })
268        }
269    }
270
271    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
272    pub fn try_new_unstable(
273        provider: &(impl DataProvider<TimezonePeriodsV1> + ?Sized),
274    ) -> Result<Self, DataError> {
275        let offset_period = provider.load(Default::default())?.payload;
276        Ok(Self {
277            offset_period: OffsetData::New(offset_period),
278        })
279    }
280
281    /// Returns a borrowed version of the calculator that can be queried.
282    ///
283    /// This avoids a small potential indirection cost when querying.
284    pub fn as_borrowed(&self) -> VariantOffsetsCalculatorBorrowed<'_> {
285        VariantOffsetsCalculatorBorrowed {
286            offset_period: match self.offset_period {
287                OffsetData::New(ref payload) => OffsetDataBorrowed::New(payload.get()),
288                #[cfg(feature = "alloc")]
289                OffsetData::Old(ref payload) => OffsetDataBorrowed::Old(payload.get()),
290            },
291        }
292    }
293}
294
295#[allow(deprecated)]
296impl VariantOffsetsCalculatorBorrowed<'static> {
297    /// Constructs a `VariantOffsetsCalculatorBorrowed` using compiled data.
298    ///
299    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
300    ///
301    /// [📚 Help choosing a constructor](icu_provider::constructors)
302    #[cfg(feature = "compiled_data")]
303    #[inline]
304    pub const fn new() -> Self {
305        Self {
306            offset_period: OffsetDataBorrowed::New(
307                crate::provider::Baked::SINGLETON_TIMEZONE_PERIODS_V1,
308            ),
309        }
310    }
311
312    /// Cheaply converts a [`VariantOffsetsCalculatorBorrowed<'static>`] into a [`VariantOffsetsCalculator`].
313    ///
314    /// Note: Due to branching and indirection, using [`VariantOffsetsCalculator`] might inhibit some
315    /// compile-time optimizations that are possible with [`VariantOffsetsCalculatorBorrowed`].
316    pub fn static_to_owned(&self) -> VariantOffsetsCalculator {
317        VariantOffsetsCalculator {
318            offset_period: match self.offset_period {
319                OffsetDataBorrowed::New(p) => OffsetData::New(DataPayload::from_static_ref(p)),
320                #[cfg(feature = "alloc")]
321                OffsetDataBorrowed::Old(p) => OffsetData::Old(DataPayload::from_static_ref(p)),
322            },
323        }
324    }
325}
326
327#[allow(deprecated)]
328impl VariantOffsetsCalculatorBorrowed<'_> {
329    /// Calculate zone offsets from timezone and local datetime.
330    ///
331    /// # Examples
332    ///
333    /// ```
334    /// use icu::calendar::Date;
335    /// use icu::locale::subtags::subtag;
336    /// use icu::time::Time;
337    /// use icu::time::TimeZone;
338    /// use icu::time::zone::UtcOffset;
339    /// use icu::time::zone::VariantOffsetsCalculator;
340    /// use icu::time::zone::ZoneNameTimestamp;
341    ///
342    /// let zoc = VariantOffsetsCalculator::new();
343    ///
344    /// // America/Denver observes DST
345    /// let offsets = zoc
346    ///     .compute_offsets_from_time_zone_and_name_timestamp(
347    ///         TimeZone(subtag!("usden")),
348    ///         ZoneNameTimestamp::far_in_future(),
349    ///     )
350    ///     .unwrap();
351    /// assert_eq!(
352    ///     offsets.standard,
353    ///     UtcOffset::try_from_seconds(-7 * 3600).unwrap()
354    /// );
355    /// assert_eq!(
356    ///     offsets.daylight,
357    ///     Some(UtcOffset::try_from_seconds(-6 * 3600).unwrap())
358    /// );
359    ///
360    /// // America/Phoenix does not
361    /// let offsets = zoc
362    ///     .compute_offsets_from_time_zone_and_name_timestamp(
363    ///         TimeZone(subtag!("usphx")),
364    ///         ZoneNameTimestamp::far_in_future(),
365    ///     )
366    ///     .unwrap();
367    /// assert_eq!(
368    ///     offsets.standard,
369    ///     UtcOffset::try_from_seconds(-7 * 3600).unwrap()
370    /// );
371    /// assert_eq!(offsets.daylight, None);
372    /// ```
373    pub fn compute_offsets_from_time_zone_and_name_timestamp(
374        &self,
375        time_zone_id: TimeZone,
376        timestamp: ZoneNameTimestamp,
377    ) -> Option<VariantOffsets> {
378        match self.offset_period {
379            OffsetDataBorrowed::New(p) => p.get(time_zone_id, timestamp).map(|(os, _)| os),
380            #[cfg(feature = "alloc")]
381            OffsetDataBorrowed::Old(p) => {
382                use zerovec::ule::AsULE;
383                let mut offsets = None;
384                for (bytes, id) in p.get0(&time_zone_id)?.iter1_copied().rev() {
385                    if timestamp >= ZoneNameTimestamp::from_unaligned(*bytes) {
386                        offsets = Some(id);
387                        break;
388                    }
389                }
390                Some(offsets?)
391            }
392        }
393    }
394}
395
396#[deprecated(
397    since = "2.1.0",
398    note = "this API is a bad approximation of a time zone database"
399)]
400pub use crate::provider::VariantOffsets;
401
402#[test]
403#[allow(deprecated)]
404pub fn test_legacy_offsets_data() {
405    use icu_locale_core::subtags::subtag;
406    use icu_provider_blob::BlobDataProvider;
407
408    let c = VariantOffsetsCalculator::try_new_with_buffer_provider(
409        &BlobDataProvider::try_new_from_static_blob(
410            // icu4x-datagen --markers TimezoneVariantsOffsetsV1 --format blob
411            include_bytes!("../../tests/data/offset_periods_old.blob"),
412        )
413        .unwrap(),
414    )
415    .unwrap();
416
417    let tz = TimeZone(subtag!("aqcas"));
418
419    for t in [
420        ZoneNameTimestamp::from_epoch_seconds(0),
421        ZoneNameTimestamp::from_epoch_seconds(1255802400),
422        ZoneNameTimestamp::from_epoch_seconds(1267714800),
423        ZoneNameTimestamp::from_epoch_seconds(1319738400),
424        ZoneNameTimestamp::from_epoch_seconds(1329843600),
425        ZoneNameTimestamp::from_epoch_seconds(1477065600),
426        ZoneNameTimestamp::from_epoch_seconds(1520701200),
427        ZoneNameTimestamp::from_epoch_seconds(1538856000),
428        ZoneNameTimestamp::from_epoch_seconds(1552752000),
429        ZoneNameTimestamp::from_epoch_seconds(1570129200),
430        ZoneNameTimestamp::from_epoch_seconds(1583596800),
431        ZoneNameTimestamp::from_epoch_seconds(1615640400),
432        ZoneNameTimestamp::from_epoch_seconds(1647090000),
433        ZoneNameTimestamp::from_epoch_seconds(1678291200),
434    ] {
435        assert_eq!(
436            c.as_borrowed()
437                .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
438            VariantOffsetsCalculator::new()
439                .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
440            "{t:?}",
441        );
442    }
443}