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}