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::provider::TimezoneVariantsOffsetsV1;
8use crate::TimeZone;
9use icu_provider::prelude::*;
10
11use displaydoc::Display;
12use zerovec::ZeroMap2d;
13
14use super::ZoneNameTimestamp;
15
16/// The time zone offset was invalid. Must be within ±18:00:00.
17#[derive(Display, Debug, Copy, Clone, PartialEq)]
18#[allow(clippy::exhaustive_structs)]
19pub struct InvalidOffsetError;
20
21/// An offset from Coordinated Universal Time (UTC).
22///
23/// Supports ±18:00:00.
24///
25/// **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.**
26#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, PartialOrd, Ord)]
27pub struct UtcOffset(i32);
28
29impl UtcOffset {
30    /// Attempt to create a [`UtcOffset`] from a seconds input.
31    ///
32    /// Returns [`InvalidOffsetError`] if the seconds are out of bounds.
33    pub fn try_from_seconds(seconds: i32) -> Result<Self, InvalidOffsetError> {
34        if seconds.unsigned_abs() > 18 * 60 * 60 {
35            Err(InvalidOffsetError)
36        } else {
37            Ok(Self(seconds))
38        }
39    }
40
41    /// Creates a [`UtcOffset`] of zero.
42    pub const fn zero() -> Self {
43        Self(0)
44    }
45
46    /// Parse a [`UtcOffset`] from bytes.
47    ///
48    /// The offset must range from UTC-12 to UTC+14.
49    ///
50    /// The string must be an ISO-8601 time zone designator:
51    /// e.g. Z
52    /// e.g. +05
53    /// e.g. +0500
54    /// e.g. +05:00
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use icu::time::zone::UtcOffset;
60    ///
61    /// let offset0: UtcOffset = UtcOffset::try_from_str("Z").unwrap();
62    /// let offset1: UtcOffset = UtcOffset::try_from_str("+05").unwrap();
63    /// let offset2: UtcOffset = UtcOffset::try_from_str("+0500").unwrap();
64    /// let offset3: UtcOffset = UtcOffset::try_from_str("-05:00").unwrap();
65    ///
66    /// let offset_err0 =
67    ///     UtcOffset::try_from_str("0500").expect_err("Invalid input");
68    /// let offset_err1 =
69    ///     UtcOffset::try_from_str("+05000").expect_err("Invalid input");
70    ///
71    /// assert_eq!(offset0.to_seconds(), 0);
72    /// assert_eq!(offset1.to_seconds(), 18000);
73    /// assert_eq!(offset2.to_seconds(), 18000);
74    /// assert_eq!(offset3.to_seconds(), -18000);
75    /// ```
76    #[inline]
77    pub fn try_from_str(s: &str) -> Result<Self, InvalidOffsetError> {
78        Self::try_from_utf8(s.as_bytes())
79    }
80
81    /// See [`Self::try_from_str`]
82    pub fn try_from_utf8(mut code_units: &[u8]) -> Result<Self, InvalidOffsetError> {
83        fn try_get_time_component([tens, ones]: [u8; 2]) -> Option<i32> {
84            Some(((tens as char).to_digit(10)? * 10 + (ones as char).to_digit(10)?) as i32)
85        }
86
87        let offset_sign = match code_units {
88            [b'+', rest @ ..] => {
89                code_units = rest;
90                1
91            }
92            [b'-', rest @ ..] => {
93                code_units = rest;
94                -1
95            }
96            // Unicode minus ("\u{2212}" == [226, 136, 146])
97            [226, 136, 146, rest @ ..] => {
98                code_units = rest;
99                -1
100            }
101            [b'Z'] => return Ok(Self(0)),
102            _ => return Err(InvalidOffsetError),
103        };
104
105        let hours = match code_units {
106            &[h1, h2, ..] => try_get_time_component([h1, h2]),
107            _ => None,
108        }
109        .ok_or(InvalidOffsetError)?;
110
111        let minutes = match code_units {
112            /* ±hh */
113            &[_, _] => Some(0),
114            /* ±hhmm, ±hh:mm */
115            &[_, _, m1, m2] | &[_, _, b':', m1, m2] => {
116                try_get_time_component([m1, m2]).filter(|&m| m < 60)
117            }
118            _ => None,
119        }
120        .ok_or(InvalidOffsetError)?;
121
122        Self::try_from_seconds(offset_sign * (hours * 60 + minutes) * 60)
123    }
124
125    /// Create a [`UtcOffset`] from a seconds input without checking bounds.
126    #[inline]
127    pub fn from_seconds_unchecked(seconds: i32) -> Self {
128        Self(seconds)
129    }
130
131    /// Returns the raw offset value in seconds.
132    pub fn to_seconds(self) -> i32 {
133        self.0
134    }
135
136    /// Whether the [`UtcOffset`] is non-negative.
137    pub fn is_non_negative(self) -> bool {
138        self.0 >= 0
139    }
140
141    /// Whether the [`UtcOffset`] is zero.
142    pub fn is_zero(self) -> bool {
143        self.0 == 0
144    }
145
146    /// Returns the hours part of if the [`UtcOffset`]
147    pub fn hours_part(self) -> i32 {
148        self.0 / 3600
149    }
150
151    /// Returns the minutes part of if the [`UtcOffset`].
152    pub fn minutes_part(self) -> u32 {
153        (self.0 % 3600 / 60).unsigned_abs()
154    }
155
156    /// Returns the seconds part of if the [`UtcOffset`].
157    pub fn seconds_part(self) -> u32 {
158        (self.0 % 60).unsigned_abs()
159    }
160}
161
162impl FromStr for UtcOffset {
163    type Err = InvalidOffsetError;
164
165    #[inline]
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        Self::try_from_str(s)
168    }
169}
170
171/// [`VariantOffsetsCalculator`] uses data from the [data provider] to calculate time zone offsets.
172///
173/// [data provider]: icu_provider
174#[derive(Debug)]
175pub struct VariantOffsetsCalculator {
176    pub(super) offset_period: DataPayload<TimezoneVariantsOffsetsV1>,
177}
178
179/// The borrowed version of a  [`VariantOffsetsCalculator`]
180#[derive(Debug)]
181pub struct VariantOffsetsCalculatorBorrowed<'a> {
182    pub(super) offset_period: &'a ZeroMap2d<'a, TimeZone, ZoneNameTimestamp, VariantOffsets>,
183}
184
185#[cfg(feature = "compiled_data")]
186impl Default for VariantOffsetsCalculatorBorrowed<'static> {
187    fn default() -> Self {
188        VariantOffsetsCalculator::new()
189    }
190}
191
192impl VariantOffsetsCalculator {
193    /// Constructs a `VariantOffsetsCalculator` using compiled data.
194    ///
195    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
196    ///
197    /// [📚 Help choosing a constructor](icu_provider::constructors)
198    #[cfg(feature = "compiled_data")]
199    #[inline]
200    #[allow(clippy::new_ret_no_self)]
201    pub const fn new() -> VariantOffsetsCalculatorBorrowed<'static> {
202        VariantOffsetsCalculatorBorrowed {
203            offset_period: crate::provider::Baked::SINGLETON_TIMEZONE_VARIANTS_OFFSETS_V1,
204        }
205    }
206
207    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
208        functions: [
209            new: skip,
210            try_new_with_buffer_provider,
211            try_new_unstable,
212            Self,
213        ]
214    );
215
216    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
217    pub fn try_new_unstable(
218        provider: &(impl DataProvider<TimezoneVariantsOffsetsV1> + ?Sized),
219    ) -> Result<Self, DataError> {
220        let offset_period = provider.load(Default::default())?.payload;
221        Ok(Self { offset_period })
222    }
223
224    /// Returns a borrowed version of the calculator that can be queried.
225    ///
226    /// This avoids a small potential indirection cost when querying.
227    pub fn as_borrowed(&self) -> VariantOffsetsCalculatorBorrowed {
228        VariantOffsetsCalculatorBorrowed {
229            offset_period: self.offset_period.get(),
230        }
231    }
232}
233
234impl VariantOffsetsCalculatorBorrowed<'static> {
235    /// Constructs a `VariantOffsetsCalculatorBorrowed` using compiled data.
236    ///
237    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
238    ///
239    /// [📚 Help choosing a constructor](icu_provider::constructors)
240    #[cfg(feature = "compiled_data")]
241    #[inline]
242    pub const fn new() -> Self {
243        Self {
244            offset_period: crate::provider::Baked::SINGLETON_TIMEZONE_VARIANTS_OFFSETS_V1,
245        }
246    }
247
248    /// Cheaply converts a [`VariantOffsetsCalculatorBorrowed<'static>`] into a [`VariantOffsetsCalculator`].
249    ///
250    /// Note: Due to branching and indirection, using [`VariantOffsetsCalculator`] might inhibit some
251    /// compile-time optimizations that are possible with [`VariantOffsetsCalculatorBorrowed`].
252    pub fn static_to_owned(&self) -> VariantOffsetsCalculator {
253        VariantOffsetsCalculator {
254            offset_period: DataPayload::from_static_ref(self.offset_period),
255        }
256    }
257}
258
259impl VariantOffsetsCalculatorBorrowed<'_> {
260    /// Calculate zone offsets from timezone and local datetime.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use icu::calendar::Date;
266    /// use icu::locale::subtags::subtag;
267    /// use icu::time::zone::UtcOffset;
268    /// use icu::time::zone::VariantOffsetsCalculator;
269    /// use icu::time::zone::ZoneNameTimestamp;
270    /// use icu::time::DateTime;
271    /// use icu::time::Time;
272    /// use icu::time::TimeZone;
273    ///
274    /// let zoc = VariantOffsetsCalculator::new();
275    ///
276    /// // America/Denver observes DST
277    /// let offsets = zoc
278    ///     .compute_offsets_from_time_zone_and_name_timestamp(
279    ///         TimeZone(subtag!("usden")),
280    ///         ZoneNameTimestamp::from_date_time_iso(DateTime {
281    ///             date: Date::try_new_iso(2024, 1, 1).unwrap(),
282    ///             time: Time::start_of_day(),
283    ///         }),
284    ///     )
285    ///     .unwrap();
286    /// assert_eq!(
287    ///     offsets.standard,
288    ///     UtcOffset::try_from_seconds(-7 * 3600).unwrap()
289    /// );
290    /// assert_eq!(
291    ///     offsets.daylight,
292    ///     Some(UtcOffset::try_from_seconds(-6 * 3600).unwrap())
293    /// );
294    ///
295    /// // America/Phoenix does not
296    /// let offsets = zoc
297    ///     .compute_offsets_from_time_zone_and_name_timestamp(
298    ///         TimeZone(subtag!("usphx")),
299    ///         ZoneNameTimestamp::from_date_time_iso(DateTime {
300    ///             date: Date::try_new_iso(2024, 1, 1).unwrap(),
301    ///             time: Time::start_of_day(),
302    ///         }),
303    ///     )
304    ///     .unwrap();
305    /// assert_eq!(
306    ///     offsets.standard,
307    ///     UtcOffset::try_from_seconds(-7 * 3600).unwrap()
308    /// );
309    /// assert_eq!(offsets.daylight, None);
310    /// ```
311    pub fn compute_offsets_from_time_zone_and_name_timestamp(
312        &self,
313        time_zone_id: TimeZone,
314        zone_name_timestamp: ZoneNameTimestamp,
315    ) -> Option<VariantOffsets> {
316        use zerovec::ule::AsULE;
317        match self.offset_period.get0(&time_zone_id) {
318            Some(cursor) => {
319                let mut offsets = None;
320                for (bytes, id) in cursor.iter1_copied() {
321                    if zone_name_timestamp
322                        .cmp(&ZoneNameTimestamp::from_unaligned(*bytes))
323                        .is_ge()
324                    {
325                        offsets = Some(id);
326                    } else {
327                        break;
328                    }
329                }
330                Some(offsets?)
331            }
332            None => None,
333        }
334    }
335}
336
337/// Represents the different offsets in use for a time zone
338#[non_exhaustive]
339#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
340pub struct VariantOffsets {
341    /// The standard offset.
342    pub standard: UtcOffset,
343    /// The daylight-saving offset, if used.
344    pub daylight: Option<UtcOffset>,
345}
346
347impl VariantOffsets {
348    /// Creates a new [`VariantOffsets`] from a [`UtcOffset`] representing standard time.
349    pub fn from_standard(standard: UtcOffset) -> Self {
350        Self {
351            standard,
352            daylight: None,
353        }
354    }
355}