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}