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//!
15//! ## Time Zone
16//!
17//! The time zone ID corresponds to a time zone from the time zone database. The time zone ID
18//! usually corresponds to the largest city in the time zone.
19//!
20//! There are two mostly-interchangeable standards for time zone IDs:
21//!
22//! 1. IANA time zone IDs, like `"America/Chicago"`
23//! 2. BCP-47 time zone IDs, like `"uschi"`
24//!
25//! ICU4X uses BCP-47 time zone IDs for all of its APIs. To get a BCP-47 time zone from an
26//! IANA time zone, use [`IanaParser`].
27//!
28//! ## UTC Offset
29//!
30//! The UTC offset precisely states the time difference between the time zone in question and
31//! Coordinated Universal Time (UTC).
32//!
33//! In localized strings, it is often rendered as "UTC-6", meaning 6 hours less than UTC (some locales
34//! use the term "GMT" instead of "UTC").
35//!
36//! ## Timestamp
37//!
38//! Some time zones change names over time, such as when changing "metazone". For example, Portugal changed from
39//! "Western European Time" to "Central European Time" and back in the 1990s, without changing time zone ID
40//! (`Europe/Lisbon`, `ptlis`). Therefore, a timestamp is needed to resolve such generic time zone names.
41//!
42//! It is not required to set the timestamp on [`TimeZoneInfo`]. If it is not set, some string
43//! formats may be unsupported.
44//!
45//! # Obtaining time zone information
46//!
47//! 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)
48//! are available for this purpose. See our [`example`](https://github.com/unicode-org/icu4x/blob/main/components/icu/examples/chrono_jiff.rs).
49
50pub mod iana;
51mod offset;
52pub mod windows;
53mod zone_name_timestamp;
54
55#[doc(inline)]
56pub use offset::InvalidOffsetError;
57pub use offset::UtcOffset;
58pub use offset::VariantOffsets;
59#[allow(deprecated)]
60pub use offset::VariantOffsetsCalculator;
61#[allow(deprecated)]
62pub use offset::VariantOffsetsCalculatorBorrowed;
63
64#[doc(no_inline)]
65pub use iana::{IanaParser, IanaParserBorrowed};
66#[doc(no_inline)]
67pub use windows::{WindowsParser, WindowsParserBorrowed};
68
69pub use zone_name_timestamp::ZoneNameTimestamp;
70
71use crate::scaffold::IntoOption;
72use crate::DateTime;
73use core::fmt;
74use core::ops::Deref;
75use icu_calendar::Iso;
76use icu_locale_core::subtags::{subtag, Subtag};
77use icu_provider::prelude::yoke;
78use zerovec::ule::{AsULE, ULE};
79
80/// Time zone data model choices.
81pub mod models {
82    use super::*;
83    mod private {
84        pub trait Sealed {}
85    }
86
87    /// Trait encoding a particular data model for time zones.
88    ///
89    /// <div class="stab unstable">
90    /// 🚫 This trait is sealed; it cannot be implemented by user code. If an API requests an item that implements this
91    /// trait, please consider using a type from the implementors listed below.
92    /// </div>
93    pub trait TimeZoneModel: private::Sealed {
94        /// The zone variant, if required for this time zone model.
95        type TimeZoneVariant: IntoOption<TimeZoneVariant> + fmt::Debug + Copy;
96        /// The local time, if required for this time zone model.
97        type ZoneNameTimestamp: IntoOption<ZoneNameTimestamp> + fmt::Debug + Copy;
98    }
99
100    /// A time zone containing a time zone ID and optional offset.
101    #[derive(Debug, PartialEq, Eq)]
102    #[non_exhaustive]
103    pub struct Base;
104
105    impl private::Sealed for Base {}
106    impl TimeZoneModel for Base {
107        type TimeZoneVariant = ();
108        type ZoneNameTimestamp = ();
109    }
110
111    /// A time zone containing a time zone ID, optional offset, and local time.
112    #[derive(Debug, PartialEq, Eq)]
113    #[non_exhaustive]
114    pub struct AtTime;
115
116    impl private::Sealed for AtTime {}
117    impl TimeZoneModel for AtTime {
118        type TimeZoneVariant = ();
119        type ZoneNameTimestamp = ZoneNameTimestamp;
120    }
121
122    /// A time zone containing a time zone ID, optional offset, local time, and zone variant.
123    #[derive(Debug, PartialEq, Eq)]
124    #[non_exhaustive]
125    #[deprecated(
126        since = "2.1.0",
127        note = "creating a `TimeZoneInfo<Full>` is not required for formatting anymore. use `TimeZoneInfo<AtTime>`"
128    )]
129    pub struct Full;
130
131    #[allow(deprecated)]
132    impl private::Sealed for Full {}
133    #[allow(deprecated)]
134    impl TimeZoneModel for Full {
135        type TimeZoneVariant = TimeZoneVariant;
136        type ZoneNameTimestamp = ZoneNameTimestamp;
137    }
138}
139
140/// A CLDR time zone identity.
141///
142/// **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.**
143///
144/// This can be created directly from BCP-47 strings, or it can be parsed from IANA IDs.
145///
146/// CLDR uses difference equivalence classes than IANA. For example, `Europe/Oslo` is
147/// an alias to `Europe/Berlin` in IANA (because they agree since 1970), but these are
148/// different identities in CLDR, as we want to be able to say "Norway Time" and
149/// "Germany Time". On the other hand `Europe/Belfast` and `Europe/London` are the same
150/// CLDR identity ("UK Time").
151///
152/// See the docs on [`zone`](crate::zone) for more information.
153///
154/// ```
155/// use icu::locale::subtags::subtag;
156/// use icu::time::zone::{IanaParser, TimeZone};
157///
158/// let parser = IanaParser::new();
159/// assert_eq!(parser.parse("Europe/Oslo"), TimeZone(subtag!("noosl")));
160/// assert_eq!(parser.parse("Europe/Berlin"), TimeZone(subtag!("deber")));
161/// assert_eq!(parser.parse("Europe/Belfast"), TimeZone(subtag!("gblon")));
162/// assert_eq!(parser.parse("Europe/London"), TimeZone(subtag!("gblon")));
163/// ```
164#[repr(transparent)]
165#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, yoke::Yokeable, ULE, Hash)]
166#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
167#[cfg_attr(feature = "datagen", databake(path = icu_time::provider))]
168#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
169#[allow(clippy::exhaustive_structs)] // This is a stable newtype
170pub struct TimeZone(pub Subtag);
171
172impl TimeZone {
173    /// The synthetic `Etc/Unknown` time zone.
174    ///
175    /// This is the result of parsing unknown zones. It's important that such parsing does not
176    /// fail, as new zones are added all the time, and ICU4X might not be up to date.
177    pub const UNKNOWN: Self = Self(subtag!("unk"));
178
179    /// Whether this [`TimeZone`] equals [`TimeZone::UNKNOWN`].
180    pub const fn is_unknown(self) -> bool {
181        matches!(self, Self::UNKNOWN)
182    }
183}
184
185impl Deref for TimeZone {
186    type Target = Subtag;
187
188    fn deref(&self) -> &Self::Target {
189        &self.0
190    }
191}
192
193impl AsULE for TimeZone {
194    type ULE = Self;
195
196    #[inline]
197    fn to_unaligned(self) -> Self::ULE {
198        self
199    }
200
201    #[inline]
202    fn from_unaligned(unaligned: Self::ULE) -> Self {
203        unaligned
204    }
205}
206
207#[cfg(feature = "alloc")]
208impl<'a> zerovec::maps::ZeroMapKV<'a> for TimeZone {
209    type Container = zerovec::ZeroVec<'a, TimeZone>;
210    type Slice = zerovec::ZeroSlice<TimeZone>;
211    type GetType = TimeZone;
212    type OwnedType = TimeZone;
213}
214
215/// A utility type that can hold time zone information.
216///
217/// **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.**
218///
219/// See the docs on [`zone`](self) for more information.
220///
221/// # Examples
222///
223/// ```
224/// use icu::calendar::Date;
225/// use icu::locale::subtags::subtag;
226/// use icu::time::zone::IanaParser;
227/// use icu::time::zone::TimeZoneVariant;
228/// use icu::time::DateTime;
229/// use icu::time::Time;
230/// use icu::time::TimeZone;
231///
232/// // Parse the IANA ID
233/// let id = IanaParser::new().parse("America/Chicago");
234///
235/// // Alternatively, use the BCP47 ID directly
236/// let id = TimeZone(subtag!("uschi"));
237///
238/// // Create a TimeZoneInfo<Base> by associating the ID with an offset
239/// let time_zone = id.with_offset("-0600".parse().ok());
240///
241/// // Extend to a TimeZoneInfo<AtTime> by adding a local time
242/// let time_zone_at_time = time_zone.at_date_time_iso(DateTime {
243///     date: Date::try_new_iso(2023, 12, 2).unwrap(),
244///     time: Time::start_of_day(),
245/// });
246/// ```
247#[derive(Debug, PartialEq, Eq)]
248#[allow(clippy::exhaustive_structs)] // these four fields fully cover the needs of UTS 35
249pub struct TimeZoneInfo<Model: models::TimeZoneModel> {
250    id: TimeZone,
251    offset: Option<UtcOffset>,
252    zone_name_timestamp: Model::ZoneNameTimestamp,
253    variant: Model::TimeZoneVariant,
254}
255
256impl<Model: models::TimeZoneModel> Clone for TimeZoneInfo<Model> {
257    fn clone(&self) -> Self {
258        *self
259    }
260}
261
262impl<Model: models::TimeZoneModel> Copy for TimeZoneInfo<Model> {}
263
264impl<Model: models::TimeZoneModel> TimeZoneInfo<Model> {
265    /// The BCP47 time-zone identifier.
266    pub fn id(self) -> TimeZone {
267        self.id
268    }
269
270    /// The UTC offset, if known.
271    ///
272    /// This field is not enforced to be consistent with the time zone id.
273    pub fn offset(self) -> Option<UtcOffset> {
274        self.offset
275    }
276}
277
278impl<Model> TimeZoneInfo<Model>
279where
280    Model: models::TimeZoneModel<ZoneNameTimestamp = ZoneNameTimestamp>,
281{
282    /// The time at which to interpret the time zone.
283    pub fn zone_name_timestamp(self) -> ZoneNameTimestamp {
284        self.zone_name_timestamp
285    }
286}
287
288impl<Model> TimeZoneInfo<Model>
289where
290    Model: models::TimeZoneModel<TimeZoneVariant = TimeZoneVariant>,
291{
292    /// The time variant e.g. daylight or standard, if known.
293    ///
294    /// This field is not enforced to be consistent with the time zone id and offset.
295    pub fn variant(self) -> TimeZoneVariant {
296        self.variant
297    }
298}
299
300impl TimeZone {
301    /// Associates this [`TimeZone`] with a UTC offset, returning a [`TimeZoneInfo`].
302    pub const fn with_offset(self, mut offset: Option<UtcOffset>) -> TimeZoneInfo<models::Base> {
303        let mut id = self;
304
305        #[allow(clippy::identity_op, clippy::neg_multiply)]
306        let correct_offset = match self.0.as_str().as_bytes() {
307            b"utc" | b"gmt" => Some(UtcOffset::zero()),
308            b"utce01" => Some(UtcOffset::from_seconds_unchecked(1 * 60 * 60)),
309            b"utce02" => Some(UtcOffset::from_seconds_unchecked(2 * 60 * 60)),
310            b"utce03" => Some(UtcOffset::from_seconds_unchecked(3 * 60 * 60)),
311            b"utce04" => Some(UtcOffset::from_seconds_unchecked(4 * 60 * 60)),
312            b"utce05" => Some(UtcOffset::from_seconds_unchecked(5 * 60 * 60)),
313            b"utce06" => Some(UtcOffset::from_seconds_unchecked(6 * 60 * 60)),
314            b"utce07" => Some(UtcOffset::from_seconds_unchecked(7 * 60 * 60)),
315            b"utce08" => Some(UtcOffset::from_seconds_unchecked(8 * 60 * 60)),
316            b"utce09" => Some(UtcOffset::from_seconds_unchecked(9 * 60 * 60)),
317            b"utce10" => Some(UtcOffset::from_seconds_unchecked(10 * 60 * 60)),
318            b"utce11" => Some(UtcOffset::from_seconds_unchecked(11 * 60 * 60)),
319            b"utce12" => Some(UtcOffset::from_seconds_unchecked(12 * 60 * 60)),
320            b"utce13" => Some(UtcOffset::from_seconds_unchecked(13 * 60 * 60)),
321            b"utce14" => Some(UtcOffset::from_seconds_unchecked(14 * 60 * 60)),
322            b"utcw01" => Some(UtcOffset::from_seconds_unchecked(-1 * 60 * 60)),
323            b"utcw02" => Some(UtcOffset::from_seconds_unchecked(-2 * 60 * 60)),
324            b"utcw03" => Some(UtcOffset::from_seconds_unchecked(-3 * 60 * 60)),
325            b"utcw04" => Some(UtcOffset::from_seconds_unchecked(-4 * 60 * 60)),
326            b"utcw05" => Some(UtcOffset::from_seconds_unchecked(-5 * 60 * 60)),
327            b"utcw06" => Some(UtcOffset::from_seconds_unchecked(-6 * 60 * 60)),
328            b"utcw07" => Some(UtcOffset::from_seconds_unchecked(-7 * 60 * 60)),
329            b"utcw08" => Some(UtcOffset::from_seconds_unchecked(-8 * 60 * 60)),
330            b"utcw09" => Some(UtcOffset::from_seconds_unchecked(-9 * 60 * 60)),
331            b"utcw10" => Some(UtcOffset::from_seconds_unchecked(-10 * 60 * 60)),
332            b"utcw11" => Some(UtcOffset::from_seconds_unchecked(-11 * 60 * 60)),
333            b"utcw12" => Some(UtcOffset::from_seconds_unchecked(-12 * 60 * 60)),
334            _ => None,
335        };
336
337        match (correct_offset, offset) {
338            // The Etc/* zones have fixed defined offsets. By setting them here,
339            // they won't format as UTC+?.
340            (Some(c), None) => {
341                offset = Some(c);
342
343                // The Etc/GMT+X zones do not have display names, so they format
344                // exactly like UNKNOWN with the same offset. For the sake of
345                // equality, set the ID to UNKNOWN as well.
346                if id.0.as_str().len() > 3 {
347                    id = Self::UNKNOWN;
348                }
349            }
350            // Garbage offset for a fixed zone, now we know nothing
351            (Some(c), Some(o)) if c.to_seconds() != o.to_seconds() => {
352                offset = None;
353                id = Self::UNKNOWN;
354            }
355            _ => {}
356        }
357
358        TimeZoneInfo {
359            id,
360            offset,
361            zone_name_timestamp: (),
362            variant: (),
363        }
364    }
365
366    /// Converts this [`TimeZone`] into a [`TimeZoneInfo`] without an offset.
367    pub const fn without_offset(self) -> TimeZoneInfo<models::Base> {
368        self.with_offset(None)
369    }
370}
371
372impl TimeZoneInfo<models::Base> {
373    /// Creates a time zone info with no information.
374    pub const fn unknown() -> Self {
375        Self {
376            id: TimeZone::UNKNOWN,
377            offset: None,
378            zone_name_timestamp: (),
379            variant: (),
380        }
381    }
382
383    /// Creates a new [`TimeZoneInfo`] for the UTC time zone.
384    pub const fn utc() -> Self {
385        TimeZoneInfo {
386            id: TimeZone(subtag!("utc")),
387            offset: Some(UtcOffset::zero()),
388            zone_name_timestamp: (),
389            variant: (),
390        }
391    }
392
393    /// Sets the [`ZoneNameTimestamp`] field.
394    pub fn with_zone_name_timestamp(
395        self,
396        zone_name_timestamp: ZoneNameTimestamp,
397    ) -> TimeZoneInfo<models::AtTime> {
398        TimeZoneInfo {
399            offset: self.offset,
400            id: self.id,
401            zone_name_timestamp,
402            variant: (),
403        }
404    }
405
406    /// Sets the [`ZoneNameTimestamp`] to the given datetime.
407    ///
408    /// If the offset is knonw, the datetime is interpreted as a local time,
409    /// otherwise as UTC. This produces correct results for the vast majority
410    /// of cases, however close to metazone changes (Eastern Time -> Central Time)
411    /// it might be incorrect if the offset is not known.
412    ///
413    /// Also see [`Self::with_zone_name_timestamp`].
414    pub fn at_date_time_iso(self, date_time: DateTime<Iso>) -> TimeZoneInfo<models::AtTime> {
415        Self::with_zone_name_timestamp(
416            self,
417            ZoneNameTimestamp::from_zoned_date_time_iso(crate::ZonedDateTime {
418                date: date_time.date,
419                time: date_time.time,
420                // If we don't have an offset, interpret as UTC. This is incorrect during O(a couple of
421                // hours) since the UNIX epoch (a handful of transitions times the few hours this is too
422                // early/late).
423                zone: self.offset.unwrap_or(UtcOffset::zero()),
424            }),
425        )
426    }
427}
428
429impl TimeZoneInfo<models::AtTime> {
430    /// Sets a [`TimeZoneVariant`] on this time zone.
431    #[deprecated(
432        since = "2.1.0",
433        note = "creating a `TimeZoneInfo<Full>` is not required for formatting anymore"
434    )]
435    #[allow(deprecated)]
436    pub const fn with_variant(self, variant: TimeZoneVariant) -> TimeZoneInfo<models::Full> {
437        TimeZoneInfo {
438            offset: self.offset,
439            id: self.id,
440            zone_name_timestamp: self.zone_name_timestamp,
441            variant,
442        }
443    }
444
445    /// Sets the zone variant by calculating it using a [`VariantOffsetsCalculator`].
446    ///
447    /// If `offset()` is `None`, or if it doesn't match either of the
448    /// timezone's standard or daylight offset around [`zone_name_timestamp`](Self::zone_name_timestamp),
449    /// the variant will be set to [`TimeZoneVariant::Standard`] and the time zone
450    /// to [`TimeZone::UNKNOWN`].
451    ///
452    /// # Example
453    /// ```
454    /// use icu::calendar::Date;
455    /// use icu::locale::subtags::subtag;
456    /// use icu::time::zone::TimeZoneVariant;
457    /// use icu::time::zone::VariantOffsetsCalculator;
458    /// use icu::time::DateTime;
459    /// use icu::time::Time;
460    /// use icu::time::TimeZone;
461    ///
462    /// // Chicago at UTC-6
463    /// let info = TimeZone(subtag!("uschi"))
464    ///     .with_offset("-0600".parse().ok())
465    ///     .at_date_time_iso(DateTime {
466    ///         date: Date::try_new_iso(2023, 12, 2).unwrap(),
467    ///         time: Time::start_of_day(),
468    ///     })
469    ///     .infer_variant(VariantOffsetsCalculator::new());
470    ///
471    /// assert_eq!(info.variant(), TimeZoneVariant::Standard);
472    ///
473    /// // Chicago at at UTC-5
474    /// let info = TimeZone(subtag!("uschi"))
475    ///     .with_offset("-0500".parse().ok())
476    ///     .at_date_time_iso(DateTime {
477    ///         date: Date::try_new_iso(2023, 6, 2).unwrap(),
478    ///         time: Time::start_of_day(),
479    ///     })
480    ///     .infer_variant(VariantOffsetsCalculator::new());
481    ///
482    /// assert_eq!(info.variant(), TimeZoneVariant::Daylight);
483    ///
484    /// // Chicago at UTC-7
485    /// let info = TimeZone(subtag!("uschi"))
486    ///     .with_offset("-0700".parse().ok())
487    ///     .at_date_time_iso(DateTime {
488    ///         date: Date::try_new_iso(2023, 12, 2).unwrap(),
489    ///         time: Time::start_of_day(),
490    ///     })
491    ///     .infer_variant(VariantOffsetsCalculator::new());
492    ///
493    /// // Whatever it is, it's not Chicago
494    /// assert_eq!(info.id(), TimeZone::UNKNOWN);
495    /// assert_eq!(info.variant(), TimeZoneVariant::Standard);
496    /// ```
497    #[deprecated(
498        since = "2.1.0",
499        note = "creating a `TimeZoneInfo<Full>` is not required for formatting anymore"
500    )]
501    #[allow(deprecated)]
502    pub fn infer_variant(
503        self,
504        calculator: VariantOffsetsCalculatorBorrowed,
505    ) -> TimeZoneInfo<models::Full> {
506        let Some(offset) = self.offset else {
507            return TimeZone::UNKNOWN
508                .with_offset(self.offset)
509                .with_zone_name_timestamp(self.zone_name_timestamp)
510                .with_variant(TimeZoneVariant::Standard);
511        };
512        let Some(variant) = calculator
513            .compute_offsets_from_time_zone_and_name_timestamp(self.id, self.zone_name_timestamp)
514            .and_then(|os| {
515                if os.standard == offset {
516                    Some(TimeZoneVariant::Standard)
517                } else if os.daylight == Some(offset) {
518                    Some(TimeZoneVariant::Daylight)
519                } else {
520                    None
521                }
522            })
523        else {
524            return TimeZone::UNKNOWN
525                .with_offset(self.offset)
526                .with_zone_name_timestamp(self.zone_name_timestamp)
527                .with_variant(TimeZoneVariant::Standard);
528        };
529        self.with_variant(variant)
530    }
531}
532
533#[deprecated(
534    since = "2.1.0",
535    note = "TimeZoneVariants don't need to be constructed in user code"
536)]
537pub use crate::provider::TimeZoneVariant;
538
539impl TimeZoneVariant {
540    /// Creates a zone variant from a TZDB `isdst` flag, if it is known that the TZDB was built with
541    /// `DATAFORM=rearguard`.
542    ///
543    /// If it is known that the database was *not* built with `rearguard`, a caller can try to adjust
544    /// for the differences. This is a moving target, for example the known differences for 2025a are:
545    ///
546    /// * `Europe/Dublin` since 1968-10-27
547    /// * `Africa/Windhoek` between 1994-03-20 and 2017-10-24
548    /// * `Africa/Casablanca` and `Africa/El_Aaiun` since 2018-10-28
549    ///
550    /// If the TZDB build mode is unknown or variable, use [`TimeZoneInfo::infer_variant`].
551    #[deprecated(
552        since = "2.1.0",
553        note = "TimeZoneVariants don't need to be constructed in user code"
554    )]
555    pub const fn from_rearguard_isdst(isdst: bool) -> Self {
556        if isdst {
557            TimeZoneVariant::Daylight
558        } else {
559            TimeZoneVariant::Standard
560        }
561    }
562}
563
564#[test]
565fn test_zone_info_equality() {
566    // offset inferred
567    assert_eq!(
568        IanaParser::new().parse("Etc/GMT-8").with_offset(None),
569        TimeZone::UNKNOWN.with_offset(Some(UtcOffset::from_seconds_unchecked(8 * 60 * 60)))
570    );
571    assert_eq!(
572        IanaParser::new().parse("Etc/UTC").with_offset(None),
573        TimeZoneInfo::utc()
574    );
575    assert_eq!(
576        IanaParser::new().parse("Etc/GMT").with_offset(None),
577        IanaParser::new()
578            .parse("Etc/GMT")
579            .with_offset(Some(UtcOffset::zero()))
580    );
581
582    // bogus offset removed
583    assert_eq!(
584        IanaParser::new()
585            .parse("Etc/GMT-8")
586            .with_offset(Some(UtcOffset::from_seconds_unchecked(123))),
587        TimeZoneInfo::unknown()
588    );
589    assert_eq!(
590        IanaParser::new()
591            .parse("Etc/UTC")
592            .with_offset(Some(UtcOffset::from_seconds_unchecked(123))),
593        TimeZoneInfo::unknown(),
594    );
595    assert_eq!(
596        IanaParser::new()
597            .parse("Etc/GMT")
598            .with_offset(Some(UtcOffset::from_seconds_unchecked(123))),
599        TimeZoneInfo::unknown()
600    );
601}