icu_time/zone/
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//! Types for resolving and manipulating time zones.
6//!
7//! # Fields
8//!
9//! In ICU4X, a [`TimeZoneInfo`] consists of up to four different fields:
10//!
11//! 1. The time zone ID
12//! 2. The offset from UTC
13//! 3. A timestamp, as time zone names can change over time
14//! 4. The zone variant, representing concepts such as Standard, Summer, Daylight, and Ramadan time
15//!
16//! ## Time Zone
17//!
18//! The time zone ID corresponds to a time zone from the time zone database. The time zone ID
19//! usually corresponds to the largest city in the time zone.
20//!
21//! There are two mostly-interchangeable standards for time zone IDs:
22//!
23//! 1. IANA time zone IDs, like `"America/Chicago"`
24//! 2. BCP-47 time zone IDs, like `"uschi"`
25//!
26//! ICU4X uses BCP-47 time zone IDs for all of its APIs. To get a BCP-47 time zone from an
27//! IANA time zone, use [`IanaParser`].
28//!
29//! ## UTC Offset
30//!
31//! The UTC offset precisely states the time difference between the time zone in question and
32//! Coordinated Universal Time (UTC).
33//!
34//! In localized strings, it is often rendered as "UTC-6", meaning 6 hours less than UTC (some locales
35//! use the term "GMT" instead of "UTC").
36//!
37//! ## Timestamp
38//!
39//! Some time zones change names over time, such as when changing "metazone". For example, Portugal changed from
40//! "Western European Time" to "Central European Time" and back in the 1990s, without changing time zone ID
41//! (`Europe/Lisbon`, `ptlis`). Therefore, a timestamp is needed to resolve such generic time zone names.
42//!
43//! It is not required to set the timestamp on [`TimeZoneInfo`]. If it is not set, some string
44//! formats may be unsupported.
45//!
46//! ## Zone Variant
47//!
48//! Many zones use different names and offsets in the summer than in the winter. In ICU4X,
49//! this is called the _zone variant_.
50//!
51//! CLDR has two zone variants, named `"standard"` and `"daylight"`. However, the mapping of these
52//! variants to specific observed offsets varies from time zone to time zone, and they may not
53//! consistently represent winter versus summer time.
54//!
55//! Note: It is not required to set the zone variant on [`TimeZoneInfo`]. If it is not set, some string
56//! formats may be unsupported.
57//!
58//! # Obtaining time zone information
59//!
60//! This crate does not ship time zone offset information. Other Rust crates such as [`chrono_tz`](https://docs.rs/chrono-tz) or [`jiff`](https://docs.rs/jiff)
61//! are available for this purpose. See our [`example`](https://github.com/unicode-org/icu4x/blob/main/components/icu/examples/chrono_jiff.rs).
62
63pub mod iana;
64mod offset;
65pub mod windows;
66mod zone_name_timestamp;
67
68#[doc(inline)]
69pub use offset::InvalidOffsetError;
70pub use offset::UtcOffset;
71pub use offset::VariantOffsets;
72pub use offset::VariantOffsetsCalculator;
73pub use offset::VariantOffsetsCalculatorBorrowed;
74
75#[doc(no_inline)]
76pub use iana::{IanaParser, IanaParserBorrowed};
77#[doc(no_inline)]
78pub use windows::{WindowsParser, WindowsParserBorrowed};
79
80pub use zone_name_timestamp::ZoneNameTimestamp;
81
82use crate::scaffold::IntoOption;
83use crate::DateTime;
84use core::fmt;
85use core::ops::Deref;
86use icu_calendar::Iso;
87use icu_locale_core::subtags::{subtag, Subtag};
88use icu_provider::prelude::yoke;
89use zerovec::ule::{AsULE, ULE};
90use zerovec::{ZeroSlice, ZeroVec};
91
92/// Time zone data model choices.
93pub mod models {
94    use super::*;
95    mod private {
96        pub trait Sealed {}
97    }
98
99    /// Trait encoding a particular data model for time zones.
100    ///
101    /// <div class="stab unstable">
102    /// 🚫 This trait is sealed; it cannot be implemented by user code. If an API requests an item that implements this
103    /// trait, please consider using a type from the implementors listed below.
104    /// </div>
105    pub trait TimeZoneModel: private::Sealed {
106        /// The zone variant, if required for this time zone model.
107        type TimeZoneVariant: IntoOption<TimeZoneVariant> + fmt::Debug + Copy;
108        /// The local time, if required for this time zone model.
109        type ZoneNameTimestamp: IntoOption<ZoneNameTimestamp> + fmt::Debug + Copy;
110    }
111
112    /// A time zone containing a time zone ID and optional offset.
113    #[derive(Debug, PartialEq, Eq)]
114    #[non_exhaustive]
115    pub struct Base;
116
117    impl private::Sealed for Base {}
118    impl TimeZoneModel for Base {
119        type TimeZoneVariant = ();
120        type ZoneNameTimestamp = ();
121    }
122
123    /// A time zone containing a time zone ID, optional offset, and local time.
124    #[derive(Debug, PartialEq, Eq)]
125    #[non_exhaustive]
126    pub struct AtTime;
127
128    impl private::Sealed for AtTime {}
129    impl TimeZoneModel for AtTime {
130        type TimeZoneVariant = ();
131        type ZoneNameTimestamp = ZoneNameTimestamp;
132    }
133
134    /// A time zone containing a time zone ID, optional offset, local time, and zone variant.
135    #[derive(Debug, PartialEq, Eq)]
136    #[non_exhaustive]
137    pub struct Full;
138
139    impl private::Sealed for Full {}
140    impl TimeZoneModel for Full {
141        type TimeZoneVariant = TimeZoneVariant;
142        type ZoneNameTimestamp = ZoneNameTimestamp;
143    }
144}
145
146/// A CLDR time zone identity.
147///
148/// **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.**
149///
150/// This can be created directly from BCP-47 strings, or it can be parsed from IANA IDs.
151///
152/// CLDR uses difference equivalence classes than IANA. For example, `Europe/Oslo` is
153/// an alias to `Europe/Berlin` in IANA (because they agree since 1970), but these are
154/// different identities in CLDR, as we want to be able to say "Norway Time" and
155/// "Germany Time". On the other hand `Europe/Belfast` and `Europe/London` are the same
156/// CLDR identity ("UK Time").
157///
158/// See the docs on [`zone`](crate::zone) for more information.
159///
160/// ```
161/// use icu::locale::subtags::subtag;
162/// use icu::time::zone::{IanaParser, TimeZone};
163///
164/// let parser = IanaParser::new();
165/// assert_eq!(parser.parse("Europe/Oslo"), TimeZone(subtag!("noosl")));
166/// assert_eq!(parser.parse("Europe/Berlin"), TimeZone(subtag!("deber")));
167/// assert_eq!(parser.parse("Europe/Belfast"), TimeZone(subtag!("gblon")));
168/// assert_eq!(parser.parse("Europe/London"), TimeZone(subtag!("gblon")));
169/// ```
170#[repr(transparent)]
171#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, yoke::Yokeable, ULE, Hash)]
172#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
173#[cfg_attr(feature = "datagen", databake(path = icu_time::provider))]
174#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
175#[allow(clippy::exhaustive_structs)] // This is a stable newtype
176pub struct TimeZone(pub Subtag);
177
178impl TimeZone {
179    /// The synthetic `Etc/Unknown` time zone.
180    ///
181    /// This is the result of parsing unknown zones. It's important that such parsing does not
182    /// fail, as new zones are added all the time, and ICU4X might not be up to date.
183    pub const UNKNOWN: Self = Self(subtag!("unk"));
184
185    /// Whether this [`TimeZone`] equals [`TimeZone::UNKNOWN`].
186    pub const fn is_unknown(self) -> bool {
187        matches!(self, Self::UNKNOWN)
188    }
189}
190
191/// This module exists so we can cleanly reexport TimeZoneVariantULE from the provider module, whilst retaining a public stable TimeZoneVariant type.
192pub(crate) mod ule {
193    /// A time zone variant, such as Standard Time, or Daylight/Summer Time.
194    ///
195    /// This should not generally be constructed by client code. Instead, use
196    /// * [`TimeZoneVariant::from_rearguard_isdst`]
197    /// * [`TimeZoneInfo::infer_variant`](crate::TimeZoneInfo::infer_variant)
198    #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
199    #[zerovec::make_ule(TimeZoneVariantULE)]
200    #[repr(u8)]
201    #[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
202    #[cfg_attr(feature = "datagen", databake(path = icu_time))]
203    #[cfg_attr(feature = "serde", derive(serde::Deserialize))]
204    #[non_exhaustive]
205    pub enum TimeZoneVariant {
206        /// The variant corresponding to `"standard"` in CLDR.
207        ///
208        /// The semantics vary from time zone to time zone. The time zone display
209        /// name of this variant may or may not be called "Standard Time".
210        ///
211        /// This is the variant with the lower UTC offset.
212        Standard = 0,
213        /// The variant corresponding to `"daylight"` in CLDR.
214        ///
215        /// The semantics vary from time zone to time zone. The time zone display
216        /// name of this variant may or may not be called "Daylight Time".
217        ///
218        /// This is the variant with the higher UTC offset.
219        Daylight = 1,
220    }
221}
222pub use ule::TimeZoneVariant;
223
224impl Deref for TimeZone {
225    type Target = Subtag;
226
227    fn deref(&self) -> &Self::Target {
228        &self.0
229    }
230}
231
232impl AsULE for TimeZone {
233    type ULE = Self;
234
235    #[inline]
236    fn to_unaligned(self) -> Self::ULE {
237        self
238    }
239
240    #[inline]
241    fn from_unaligned(unaligned: Self::ULE) -> Self {
242        unaligned
243    }
244}
245
246impl<'a> zerovec::maps::ZeroMapKV<'a> for TimeZone {
247    type Container = ZeroVec<'a, TimeZone>;
248    type Slice = ZeroSlice<TimeZone>;
249    type GetType = TimeZone;
250    type OwnedType = TimeZone;
251}
252
253/// A utility type that can hold time zone information.
254///
255/// **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.**
256///
257/// See the docs on [`zone`](self) for more information.
258///
259/// # Examples
260///
261/// ```
262/// use icu::calendar::Date;
263/// use icu::locale::subtags::subtag;
264/// use icu::time::zone::IanaParser;
265/// use icu::time::zone::TimeZoneVariant;
266/// use icu::time::DateTime;
267/// use icu::time::Time;
268/// use icu::time::TimeZone;
269///
270/// // Parse the IANA ID
271/// let id = IanaParser::new().parse("America/Chicago");
272///
273/// // Alternatively, use the BCP47 ID directly
274/// let id = TimeZone(subtag!("uschi"));
275///
276/// // Create a TimeZoneInfo<Base> by associating the ID with an offset
277/// let time_zone = id.with_offset("-0600".parse().ok());
278///
279/// // Extend to a TimeZoneInfo<AtTime> by adding a local time
280/// let time_zone_at_time = time_zone.at_date_time_iso(DateTime {
281///     date: Date::try_new_iso(2023, 12, 2).unwrap(),
282///     time: Time::start_of_day(),
283/// });
284///
285/// // Extend to a TimeZoneInfo<Full> by adding a zone variant
286/// let time_zone_with_variant =
287///     time_zone_at_time.with_variant(TimeZoneVariant::Standard);
288/// ```
289#[derive(Debug, PartialEq, Eq)]
290#[allow(clippy::exhaustive_structs)] // these four fields fully cover the needs of UTS 35
291pub struct TimeZoneInfo<Model: models::TimeZoneModel> {
292    id: TimeZone,
293    offset: Option<UtcOffset>,
294    zone_name_timestamp: Model::ZoneNameTimestamp,
295    variant: Model::TimeZoneVariant,
296}
297
298impl<Model: models::TimeZoneModel> Clone for TimeZoneInfo<Model> {
299    fn clone(&self) -> Self {
300        *self
301    }
302}
303
304impl<Model: models::TimeZoneModel> Copy for TimeZoneInfo<Model> {}
305
306impl<Model: models::TimeZoneModel> TimeZoneInfo<Model> {
307    /// The BCP47 time-zone identifier.
308    pub fn id(self) -> TimeZone {
309        self.id
310    }
311
312    /// The UTC offset, if known.
313    ///
314    /// This field is not enforced to be consistent with the time zone id.
315    pub fn offset(self) -> Option<UtcOffset> {
316        self.offset
317    }
318}
319
320impl<Model> TimeZoneInfo<Model>
321where
322    Model: models::TimeZoneModel<ZoneNameTimestamp = ZoneNameTimestamp>,
323{
324    /// The time at which to interpret the time zone.
325    pub fn zone_name_timestamp(self) -> ZoneNameTimestamp {
326        self.zone_name_timestamp
327    }
328}
329
330impl<Model> TimeZoneInfo<Model>
331where
332    Model: models::TimeZoneModel<TimeZoneVariant = TimeZoneVariant>,
333{
334    /// The time variant e.g. daylight or standard, if known.
335    ///
336    /// This field is not enforced to be consistent with the time zone id and offset.
337    pub fn variant(self) -> TimeZoneVariant {
338        self.variant
339    }
340}
341
342impl TimeZone {
343    /// Associates this [`TimeZone`] with a UTC offset, returning a [`TimeZoneInfo`].
344    pub const fn with_offset(self, offset: Option<UtcOffset>) -> TimeZoneInfo<models::Base> {
345        TimeZoneInfo {
346            offset,
347            id: self,
348            zone_name_timestamp: (),
349            variant: (),
350        }
351    }
352
353    /// Converts this [`TimeZone`] into a [`TimeZoneInfo`] without an offset.
354    pub const fn without_offset(self) -> TimeZoneInfo<models::Base> {
355        TimeZoneInfo {
356            offset: None,
357            id: self,
358            zone_name_timestamp: (),
359            variant: (),
360        }
361    }
362}
363
364impl TimeZoneInfo<models::Base> {
365    /// Creates a time zone info with no information.
366    pub const fn unknown() -> Self {
367        TimeZone::UNKNOWN.with_offset(None)
368    }
369
370    /// Creates a new [`TimeZoneInfo`] for the UTC time zone.
371    pub const fn utc() -> Self {
372        TimeZone(subtag!("utc")).with_offset(Some(UtcOffset::zero()))
373    }
374
375    /// Sets the [`ZoneNameTimestamp`] field.
376    pub fn with_zone_name_timestamp(
377        self,
378        zone_name_timestamp: ZoneNameTimestamp,
379    ) -> TimeZoneInfo<models::AtTime> {
380        TimeZoneInfo {
381            offset: self.offset,
382            id: self.id,
383            zone_name_timestamp,
384            variant: (),
385        }
386    }
387
388    /// Sets the [`ZoneNameTimestamp`] to the given local datetime.
389    pub fn at_date_time_iso(self, date_time: DateTime<Iso>) -> TimeZoneInfo<models::AtTime> {
390        Self::with_zone_name_timestamp(self, ZoneNameTimestamp::from_date_time_iso(date_time))
391    }
392}
393
394impl TimeZoneInfo<models::AtTime> {
395    /// Sets a [`TimeZoneVariant`] on this time zone.
396    pub const fn with_variant(self, variant: TimeZoneVariant) -> TimeZoneInfo<models::Full> {
397        TimeZoneInfo {
398            offset: self.offset,
399            id: self.id,
400            zone_name_timestamp: self.zone_name_timestamp,
401            variant,
402        }
403    }
404
405    /// Sets the zone variant by calculating it using a [`VariantOffsetsCalculator`].
406    ///
407    /// If `offset()` is `None`, or if it doesn't match either of the
408    /// timezone's standard or daylight offset around `local_time()`,
409    /// the variant will be set to [`TimeZoneVariant::Standard`] and the time zone
410    /// to [`TimeZone::UNKNOWN`].
411    ///
412    /// # Example
413    /// ```
414    /// use icu::calendar::Date;
415    /// use icu::locale::subtags::subtag;
416    /// use icu::time::zone::TimeZoneVariant;
417    /// use icu::time::zone::VariantOffsetsCalculator;
418    /// use icu::time::DateTime;
419    /// use icu::time::Time;
420    /// use icu::time::TimeZone;
421    ///
422    /// // Chicago at UTC-6
423    /// let info = TimeZone(subtag!("uschi"))
424    ///     .with_offset("-0600".parse().ok())
425    ///     .at_date_time_iso(DateTime {
426    ///         date: Date::try_new_iso(2023, 12, 2).unwrap(),
427    ///         time: Time::start_of_day(),
428    ///     })
429    ///     .infer_variant(VariantOffsetsCalculator::new());
430    ///
431    /// assert_eq!(info.variant(), TimeZoneVariant::Standard);
432    ///
433    /// // Chicago at at UTC-5
434    /// let info = TimeZone(subtag!("uschi"))
435    ///     .with_offset("-0500".parse().ok())
436    ///     .at_date_time_iso(DateTime {
437    ///         date: Date::try_new_iso(2023, 6, 2).unwrap(),
438    ///         time: Time::start_of_day(),
439    ///     })
440    ///     .infer_variant(VariantOffsetsCalculator::new());
441    ///
442    /// assert_eq!(info.variant(), TimeZoneVariant::Daylight);
443    ///
444    /// // Chicago at UTC-7
445    /// let info = TimeZone(subtag!("uschi"))
446    ///     .with_offset("-0700".parse().ok())
447    ///     .at_date_time_iso(DateTime {
448    ///         date: Date::try_new_iso(2023, 12, 2).unwrap(),
449    ///         time: Time::start_of_day(),
450    ///     })
451    ///     .infer_variant(VariantOffsetsCalculator::new());
452    ///
453    /// // Whatever it is, it's not Chicago
454    /// assert_eq!(info.id(), TimeZone::UNKNOWN);
455    /// assert_eq!(info.variant(), TimeZoneVariant::Standard);
456    /// ```
457    pub fn infer_variant(
458        self,
459        calculator: VariantOffsetsCalculatorBorrowed,
460    ) -> TimeZoneInfo<models::Full> {
461        let Some(offset) = self.offset else {
462            return TimeZone::UNKNOWN
463                .with_offset(self.offset)
464                .with_zone_name_timestamp(self.zone_name_timestamp)
465                .with_variant(TimeZoneVariant::Standard);
466        };
467        let Some(variant) = calculator
468            .compute_offsets_from_time_zone_and_name_timestamp(self.id, self.zone_name_timestamp)
469            .and_then(|os| {
470                if os.standard == offset {
471                    Some(TimeZoneVariant::Standard)
472                } else if os.daylight == Some(offset) {
473                    Some(TimeZoneVariant::Daylight)
474                } else {
475                    None
476                }
477            })
478        else {
479            return TimeZone::UNKNOWN
480                .with_offset(self.offset)
481                .with_zone_name_timestamp(self.zone_name_timestamp)
482                .with_variant(TimeZoneVariant::Standard);
483        };
484        self.with_variant(variant)
485    }
486}
487
488impl TimeZoneVariant {
489    /// Creates a zone variant from a TZDB `isdst` flag, if it is known that the TZDB was built with
490    /// `DATAFORM=rearguard`.
491    ///
492    /// If it is known that the database was *not* built with `rearguard`, a caller can try to adjust
493    /// for the differences. This is a moving target, for example the known differences for 2025a are:
494    ///
495    /// * `Europe/Dublin` since 1968-10-27
496    /// * `Africa/Windhoek` between 1994-03-20 and 2017-10-24
497    /// * `Africa/Casablanca` and `Africa/El_Aaiun` since 2018-10-28
498    ///
499    /// If the TZDB build mode is unknown or variable, use [`TimeZoneInfo::infer_variant`].
500    pub const fn from_rearguard_isdst(isdst: bool) -> Self {
501        if isdst {
502            TimeZoneVariant::Daylight
503        } else {
504            TimeZoneVariant::Standard
505        }
506    }
507}