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}