jiff/tz/mod.rs
1/*!
2Routines for interacting with time zones and the zoneinfo database.
3
4The main type in this module is [`TimeZone`]. For most use cases, you may not
5even need to interact with this type at all. For example, this code snippet
6converts a civil datetime to a zone aware datetime:
7
8```
9use jiff::civil::date;
10
11let zdt = date(2024, 7, 10).at(20, 48, 0, 0).in_tz("America/New_York")?;
12assert_eq!(zdt.to_string(), "2024-07-10T20:48:00-04:00[America/New_York]");
13
14# Ok::<(), Box<dyn std::error::Error>>(())
15```
16
17And this example parses a zone aware datetime from a string:
18
19```
20use jiff::Zoned;
21
22let zdt: Zoned = "2024-07-10 20:48[america/new_york]".parse()?;
23assert_eq!(zdt.year(), 2024);
24assert_eq!(zdt.month(), 7);
25assert_eq!(zdt.day(), 10);
26assert_eq!(zdt.hour(), 20);
27assert_eq!(zdt.minute(), 48);
28assert_eq!(zdt.offset().seconds(), -4 * 60 * 60);
29assert_eq!(zdt.time_zone().iana_name(), Some("America/New_York"));
30
31# Ok::<(), Box<dyn std::error::Error>>(())
32```
33
34Yet, neither of the above examples require uttering [`TimeZone`]. This is
35because the datetime types in this crate provide higher level abstractions for
36working with time zone identifiers. Nevertheless, sometimes it is useful to
37work with a `TimeZone` directly. For example, if one has a `TimeZone`, then
38conversion from a [`Timestamp`] to a [`Zoned`] is infallible:
39
40```
41use jiff::{tz::TimeZone, Timestamp, Zoned};
42
43let tz = TimeZone::get("America/New_York")?;
44let ts = Timestamp::UNIX_EPOCH;
45let zdt = ts.to_zoned(tz);
46assert_eq!(zdt.to_string(), "1969-12-31T19:00:00-05:00[America/New_York]");
47
48# Ok::<(), Box<dyn std::error::Error>>(())
49```
50
51# The [IANA Time Zone Database]
52
53Since a time zone is a set of rules for determining the civil time, via an
54offset from UTC, in a particular geographic region, a database is required to
55represent the full complexity of these rules in practice. The standard database
56is widespread use is the [IANA Time Zone Database]. On Unix systems, this is
57typically found at `/usr/share/zoneinfo`, and Jiff will read it automatically.
58On Windows systems, there is no canonical Time Zone Database installation, and
59so Jiff embeds it into the compiled artifact. (This does not happen on Unix
60by default.)
61
62See the [`TimeZoneDatabase`] for more information.
63
64# The system or "local" time zone
65
66In many cases, the operating system manages a "default" time zone. It might,
67for example, be how the `date` program converts a Unix timestamp to a time that
68is "local" to you.
69
70Unfortunately, there is no universal approach to discovering a system's default
71time zone. Instead, Jiff uses heuristics like reading `/etc/localtime` on Unix,
72and calling [`GetDynamicTimeZoneInformation`] on Windows. But in all cases,
73Jiff will always use the IANA Time Zone Database for implementing time zone
74transition rules. (For example, Windows specific APIs for time zone transitions
75are not supported by Jiff.)
76
77Moreover, Jiff supports reading the `TZ` environment variable, as specified
78by POSIX, on all systems.
79
80TO get the system's default time zone, use [`TimeZone::system`].
81
82[IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
83[`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
84*/
85
86use crate::{
87 civil::DateTime,
88 error::{err, Error, ErrorContext},
89 util::{array_str::ArrayStr, sync::Arc},
90 Timestamp, Zoned,
91};
92
93#[cfg(feature = "alloc")]
94use self::posix::ReasonablePosixTimeZone;
95
96pub use self::{
97 db::{db, TimeZoneDatabase, TimeZoneName, TimeZoneNameIter},
98 offset::{Dst, Offset, OffsetArithmetic, OffsetConflict, OffsetRound},
99};
100
101#[cfg(feature = "tzdb-concatenated")]
102mod concatenated;
103mod db;
104mod offset;
105#[cfg(feature = "alloc")]
106pub(crate) mod posix;
107#[cfg(feature = "tz-system")]
108mod system;
109#[cfg(all(test, feature = "alloc"))]
110mod testdata;
111#[cfg(feature = "alloc")]
112mod tzif;
113// See module comment for WIP status. :-(
114#[cfg(test)]
115mod zic;
116
117/// A representation of a [time zone].
118///
119/// A time zone is a set of rules for determining the civil time, via an offset
120/// from UTC, in a particular geographic region. In many cases, the offset
121/// in a particular time zone can vary over the course of a year through
122/// transitions into and out of [daylight saving time].
123///
124/// A `TimeZone` can be one of three possible representations:
125///
126/// * An identifier from the [IANA Time Zone Database] and the rules associated
127/// with that identifier.
128/// * A fixed offset where there are never any time zone transitions.
129/// * A [POSIX TZ] string that specifies a standard offset and an optional
130/// daylight saving time offset along with a rule for when DST is in effect.
131/// The rule applies for every year. Since POSIX TZ strings cannot capture the
132/// full complexity of time zone rules, they generally should not be used.
133///
134/// The most practical and useful representation is an IANA time zone. Namely,
135/// it enjoys broad support and its database is regularly updated to reflect
136/// real changes in time zone rules throughout the world. On Unix systems,
137/// the time zone database is typically found at `/usr/share/zoneinfo`. For
138/// more information on how Jiff interacts with The Time Zone Database, see
139/// [`TimeZoneDatabase`].
140///
141/// In typical usage, users of Jiff shouldn't need to reference a `TimeZone`
142/// directly. Instead, there are convenience APIs on datetime types that accept
143/// IANA time zone identifiers and do automatic database lookups for you. For
144/// example, to convert a timestamp to a zone aware datetime:
145///
146/// ```
147/// use jiff::Timestamp;
148///
149/// let ts = Timestamp::from_second(1_456_789_123)?;
150/// let zdt = ts.in_tz("America/New_York")?;
151/// assert_eq!(zdt.to_string(), "2016-02-29T18:38:43-05:00[America/New_York]");
152///
153/// # Ok::<(), Box<dyn std::error::Error>>(())
154/// ```
155///
156/// Or to convert a civil datetime to a zoned datetime corresponding to a
157/// precise instant in time:
158///
159/// ```
160/// use jiff::civil::date;
161///
162/// let dt = date(2024, 7, 15).at(21, 27, 0, 0);
163/// let zdt = dt.in_tz("America/New_York")?;
164/// assert_eq!(zdt.to_string(), "2024-07-15T21:27:00-04:00[America/New_York]");
165///
166/// # Ok::<(), Box<dyn std::error::Error>>(())
167/// ```
168///
169/// Or even converted a zoned datetime from one time zone to another:
170///
171/// ```
172/// use jiff::civil::date;
173///
174/// let dt = date(2024, 7, 15).at(21, 27, 0, 0);
175/// let zdt1 = dt.in_tz("America/New_York")?;
176/// let zdt2 = zdt1.in_tz("Israel")?;
177/// assert_eq!(zdt2.to_string(), "2024-07-16T04:27:00+03:00[Israel]");
178///
179/// # Ok::<(), Box<dyn std::error::Error>>(())
180/// ```
181///
182/// # The system time zone
183///
184/// The system time zone can be retrieved via [`TimeZone::system`]. If it
185/// couldn't be detected or if the `tz-system` crate feature is not enabled,
186/// then [`TimeZone::UTC`] is returned. `TimeZone::system` is what's used
187/// internally for retrieving the current zoned datetime via [`Zoned::now`].
188///
189/// While there is no platform independent way to detect your system's
190/// "default" time zone, Jiff employs best-effort heuristics to determine it.
191/// (For example, by examining `/etc/localtime` on Unix systems.) When the
192/// heuristics fail, Jiff will emit a `WARN` level log. It can be viewed by
193/// installing a `log` compatible logger, such as [`env_logger`].
194///
195/// # Custom time zones
196///
197/// At present, Jiff doesn't provide any APIs for manually constructing a
198/// custom time zone. However, [`TimeZone::tzif`] is provided for reading
199/// any valid TZif formatted data, as specified by [RFC 8536]. This provides
200/// an interoperable way of utilizing custom time zone rules.
201///
202/// # A `TimeZone` is immutable
203///
204/// Once a `TimeZone` is created, it is immutable. That is, its underlying
205/// time zone transition rules will never change. This is true for system time
206/// zones or even if the IANA Time Zone Database it was loaded from changes on
207/// disk. The only way such changes can be observed is by re-requesting the
208/// `TimeZone` from a `TimeZoneDatabase`. (Or, in the case of the system time
209/// zone, by calling `TimeZone::system`.)
210///
211/// # A `TimeZone` is cheap to clone
212///
213/// A `TimeZone` can be cheaply cloned. It uses automic reference counting
214/// internally. When `alloc` is disabled, cloning a `TimeZone` is still cheap
215/// because POSIX time zones and TZif time zones are unsupported. Therefore,
216/// cloning a time zone does a deep copy (since automic reference counting is
217/// not available), but the data being copied is small.
218///
219/// # Time zone equality
220///
221/// `TimeZone` provides an imperfect notion of equality. That is, when two time
222/// zones are equal, then it is guaranteed for them to have the same rules.
223/// However, two time zones may compare unequal and yet still have the same
224/// rules.
225///
226/// The equality semantics are as follows:
227///
228/// * Two fixed offset time zones are equal when their offsets are equal.
229/// * Two POSIX time zones are equal when their original rule strings are
230/// byte-for-byte identical.
231/// * Two IANA time zones are equal when their identifiers are equal _and_
232/// checksums of their rules are equal.
233/// * In all other cases, time zones are unequal.
234///
235/// Time zone equality is, for example, used in APIs like [`Zoned::since`]
236/// when asking for spans with calendar units. Namely, since days can be of
237/// different lengths in different time zones, `Zoned::since` will return an
238/// error when the two zoned datetimes are in different time zones and when
239/// the caller requests units greater than hours.
240///
241/// # Dealing with ambiguity
242///
243/// The principal job of a `TimeZone` is to provide two different
244/// transformations:
245///
246/// * A conversion from a [`Timestamp`] to a civil time (also known as local,
247/// naive or plain time). This conversion is always unambiguous. That is,
248/// there is always precisely one representation of civil time for any
249/// particular instant in time for a particular time zone.
250/// * A conversion from a [`civil::DateTime`](crate::civil::DateTime) to an
251/// instant in time. This conversion is sometimes ambiguous in that a civil
252/// time might have either never appear on the clocks in a particular
253/// time zone (a gap), or in that the civil time may have been repeated on the
254/// clocks in a particular time zone (a fold). Typically, a transition to
255/// daylight saving time is a gap, while a transition out of daylight saving
256/// time is a fold.
257///
258/// The timestamp-to-civil time conversion is done via
259/// [`TimeZone::to_datetime`], or its lower level counterpart,
260/// [`TimeZone::to_offset`]. The civil time-to-timestamp conversion is done
261/// via one of the following routines:
262///
263/// * [`TimeZone::to_zoned`] conveniently returns a [`Zoned`] and automatically
264/// uses the [`Disambiguation::Compatible`] strategy if the given civil
265/// datetime is ambiguous in the time zone.
266/// * [`TimeZone::to_ambiguous_zoned`] returns a potentially ambiguous
267/// zoned datetime, [`AmbiguousZoned`], and provides fine-grained control over
268/// how to resolve ambiguity, if it occurs.
269/// * [`TimeZone::to_timestamp`] is like `TimeZone::to_zoned`, but returns
270/// a [`Timestamp`] instead.
271/// * [`TimeZone::to_ambiguous_timestamp`] is like
272/// `TimeZone::to_ambiguous_zoned`, but returns an [`AmbiguousTimestamp`]
273/// instead.
274///
275/// Here is an example where we explore the different disambiguation strategies
276/// for a fold in time, where in this case, the 1 o'clock hour is repeated:
277///
278/// ```
279/// use jiff::{civil::date, tz::TimeZone};
280///
281/// let tz = TimeZone::get("America/New_York")?;
282/// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
283/// // It's ambiguous, so asking for an unambiguous instant presents an error!
284/// assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
285/// // Gives you the earlier time in a fold, i.e., before DST ends:
286/// assert_eq!(
287/// tz.to_ambiguous_zoned(dt).earlier()?.to_string(),
288/// "2024-11-03T01:30:00-04:00[America/New_York]",
289/// );
290/// // Gives you the later time in a fold, i.e., after DST ends.
291/// // Notice the offset change from the previous example!
292/// assert_eq!(
293/// tz.to_ambiguous_zoned(dt).later()?.to_string(),
294/// "2024-11-03T01:30:00-05:00[America/New_York]",
295/// );
296/// // "Just give me something reasonable"
297/// assert_eq!(
298/// tz.to_ambiguous_zoned(dt).compatible()?.to_string(),
299/// "2024-11-03T01:30:00-04:00[America/New_York]",
300/// );
301///
302/// # Ok::<(), Box<dyn std::error::Error>>(())
303/// ```
304///
305/// # Serde integration
306///
307/// At present, a `TimeZone` does not implement Serde's `Serialize` or
308/// `Deserialize` traits directly. Nor does it implement `std::fmt::Display`
309/// or `std::str::FromStr`. The reason for this is that it's not totally
310/// clear if there is one single obvious behavior. Moreover, some `TimeZone`
311/// values do not have an obvious succinct serialized representation. (For
312/// example, when `/etc/localtime` on a Unix system is your system's time zone,
313/// and it isn't a symlink to a TZif file in `/usr/share/zoneinfo`. In which
314/// case, an IANA time zone identifier cannot easily be deduced by Jiff.)
315///
316/// Instead, Jiff offers helpers for use with Serde's [`with` attribute] via
317/// the [`fmt::serde`](crate::fmt::serde) module:
318///
319/// ```
320/// use jiff::tz::TimeZone;
321///
322/// #[derive(Debug, serde::Deserialize, serde::Serialize)]
323/// struct Record {
324/// #[serde(with = "jiff::fmt::serde::tz::optional")]
325/// tz: Option<TimeZone>,
326/// }
327///
328/// let json = r#"{"tz":"America/Nuuk"}"#;
329/// let got: Record = serde_json::from_str(&json)?;
330/// assert_eq!(got.tz, Some(TimeZone::get("America/Nuuk")?));
331/// assert_eq!(serde_json::to_string(&got)?, json);
332///
333/// # Ok::<(), Box<dyn std::error::Error>>(())
334/// ```
335///
336/// Alternatively, you may use the
337/// [`fmt::temporal::DateTimeParser::parse_time_zone`](crate::fmt::temporal::DateTimeParser::parse_time_zone)
338/// or
339/// [`fmt::temporal::DateTimePrinter::print_time_zone`](crate::fmt::temporal::DateTimePrinter::print_time_zone)
340/// routines to parse or print `TimeZone` values without using Serde.
341///
342/// [time zone]: https://en.wikipedia.org/wiki/Time_zone
343/// [daylight saving time]: https://en.wikipedia.org/wiki/Daylight_saving_time
344/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
345/// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
346/// [`env_logger`]: https://docs.rs/env_logger
347/// [RFC 8536]: https://datatracker.ietf.org/doc/html/rfc8536
348/// [`with` attribute]: https://serde.rs/field-attrs.html#with
349#[derive(Clone, Eq, PartialEq)]
350pub struct TimeZone {
351 kind: Option<Arc<TimeZoneKind>>,
352}
353
354impl TimeZone {
355 /// The UTC time zone.
356 ///
357 /// The offset of this time is `0` and never has any transitions.
358 pub const UTC: TimeZone = TimeZone { kind: None };
359
360 /// Returns the system configured time zone, if available.
361 ///
362 /// Detection of a system's default time zone is generally heuristic
363 /// based and platform specific.
364 ///
365 /// If callers need to know whether discovery of the system time zone
366 /// failed, then use [`TimeZone::try_system`].
367 ///
368 /// # Fallback behavior
369 ///
370 /// If the system's default time zone could not be determined, or if
371 /// the `tz-system` crate feature is not enabled, then this returns
372 /// [`TimeZone::unknown`]. A `WARN` level log will also be emitted with
373 /// a message explaining why time zone detection failed. The fallback to
374 /// an unknown time zone is a practical trade-off, is what most other
375 /// systems tend to do and is also recommended by [relevant standards such
376 /// as freedesktop.org][freedesktop-org-localtime].
377 ///
378 /// An unknown time zone _behaves_ like [`TimeZone::UTC`], but will
379 /// print as `Etc/Unknown` when converting a `Zoned` to a string.
380 ///
381 /// If you would instead like to fall back to UTC instead
382 /// of the special "unknown" time zone, then you can do
383 /// `TimeZone::try_system().unwrap_or(TimeZone::UTC)`.
384 ///
385 /// # Platform behavior
386 ///
387 /// This section is a "best effort" explanation of how the time zone is
388 /// detected on supported platforms. The behavior is subject to change.
389 ///
390 /// On all platforms, the `TZ` environment variable overrides any other
391 /// heuristic, and provides a way for end users to set the time zone for
392 /// specific use cases. In general, Jiff respects the [POSIX TZ] rules.
393 /// Here are some examples:
394 ///
395 /// * `TZ=America/New_York` for setting a time zone via an IANA Time Zone
396 /// Database Identifier.
397 /// * `TZ=/usr/share/zoneinfo/America/New_York` for setting a time zone
398 /// by providing a file path to a TZif file directly.
399 /// * `TZ=EST5EDT,M3.2.0,M11.1.0` for setting a time zone via a daylight
400 /// saving time transition rule.
401 ///
402 /// Otherwise, when `TZ` isn't set, then:
403 ///
404 /// On Unix non-Android systems, this inspects `/etc/localtime`. If it's
405 /// a symbolic link to an entry in `/usr/share/zoneinfo`, then the suffix
406 /// is considered an IANA Time Zone Database identifier. Otherwise,
407 /// `/etc/localtime` is read as a TZif file directly.
408 ///
409 /// On Android systems, this inspects the `persist.sys.timezone` property.
410 ///
411 /// On Windows, the system time zone is determined via
412 /// [`GetDynamicTimeZoneInformation`]. The result is then mapped to an
413 /// IANA Time Zone Database identifier via Unicode's
414 /// [CLDR XML data].
415 ///
416 /// [freedesktop-org-localtime]: https://www.freedesktop.org/software/systemd/man/latest/localtime.html
417 /// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
418 /// [`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
419 /// [CLDR XML data]: https://github.com/unicode-org/cldr/raw/main/common/supplemental/windowsZones.xml
420 #[inline]
421 pub fn system() -> TimeZone {
422 match TimeZone::try_system() {
423 Ok(tz) => tz,
424 Err(_err) => {
425 warn!(
426 "failed to get system time zone, \
427 falling back to `Etc/Unknown` \
428 (which behaves like UTC): {_err}",
429 );
430 TimeZone::unknown()
431 }
432 }
433 }
434
435 /// Returns the system configured time zone, if available.
436 ///
437 /// If the system's default time zone could not be determined, or if the
438 /// `tz-system` crate feature is not enabled, then this returns an error.
439 ///
440 /// Detection of a system's default time zone is generally heuristic
441 /// based and platform specific.
442 ///
443 /// Note that callers should generally prefer using [`TimeZone::system`].
444 /// If a system time zone could not be found, then it falls
445 /// back to [`TimeZone::UTC`] automatically. This is often
446 /// what is recommended by [relevant standards such as
447 /// freedesktop.org][freedesktop-org-localtime]. Conversely, this routine
448 /// is useful if detection of a system's default time zone is critical.
449 ///
450 /// # Platform behavior
451 ///
452 /// This section is a "best effort" explanation of how the time zone is
453 /// detected on supported platforms. The behavior is subject to change.
454 ///
455 /// On all platforms, the `TZ` environment variable overrides any other
456 /// heuristic, and provides a way for end users to set the time zone for
457 /// specific use cases. In general, Jiff respects the [POSIX TZ] rules.
458 /// Here are some examples:
459 ///
460 /// * `TZ=America/New_York` for setting a time zone via an IANA Time Zone
461 /// Database Identifier.
462 /// * `TZ=/usr/share/zoneinfo/America/New_York` for setting a time zone
463 /// by providing a file path to a TZif file directly.
464 /// * `TZ=EST5EDT,M3.2.0,M11.1.0` for setting a time zone via a daylight
465 /// saving time transition rule.
466 ///
467 /// Otherwise, when `TZ` isn't set, then:
468 ///
469 /// On Unix systems, this inspects `/etc/localtime`. If it's a symbolic
470 /// link to an entry in `/usr/share/zoneinfo`, then the suffix is
471 /// considered an IANA Time Zone Database identifier. Otherwise,
472 /// `/etc/localtime` is read as a TZif file directly.
473 ///
474 /// On Windows, the system time zone is determined via
475 /// [`GetDynamicTimeZoneInformation`]. The result is then mapped to an
476 /// IANA Time Zone Database identifier via Unicode's
477 /// [CLDR XML data].
478 ///
479 /// [freedesktop-org-localtime]: https://www.freedesktop.org/software/systemd/man/latest/localtime.html
480 /// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
481 /// [`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
482 /// [CLDR XML data]: https://github.com/unicode-org/cldr/raw/main/common/supplemental/windowsZones.xml
483 #[inline]
484 pub fn try_system() -> Result<TimeZone, Error> {
485 #[cfg(not(feature = "tz-system"))]
486 {
487 Err(err!(
488 "failed to get system time zone since 'tz-system' \
489 crate feature is not enabled",
490 ))
491 }
492 #[cfg(feature = "tz-system")]
493 {
494 self::system::get(db())
495 }
496 }
497
498 /// A convenience function for performing a time zone database lookup for
499 /// the given time zone identifier. It uses the default global time zone
500 /// database via [`tz::db()`](db()).
501 ///
502 /// # Errors
503 ///
504 /// This returns an error if the given time zone identifier could not be
505 /// found in the default [`TimeZoneDatabase`].
506 ///
507 /// # Example
508 ///
509 /// ```
510 /// use jiff::{tz::TimeZone, Timestamp};
511 ///
512 /// let tz = TimeZone::get("Japan")?;
513 /// assert_eq!(
514 /// tz.to_datetime(Timestamp::UNIX_EPOCH).to_string(),
515 /// "1970-01-01T09:00:00",
516 /// );
517 ///
518 /// # Ok::<(), Box<dyn std::error::Error>>(())
519 /// ```
520 #[inline]
521 pub fn get(time_zone_name: &str) -> Result<TimeZone, Error> {
522 db().get(time_zone_name)
523 }
524
525 /// Returns a time zone with a fixed offset.
526 ///
527 /// A fixed offset will never have any transitions and won't follow any
528 /// particular time zone rules. In general, one should avoid using fixed
529 /// offset time zones unless you have a specific need for them. Otherwise,
530 /// IANA time zones via [`TimeZone::get`] should be preferred, as they
531 /// more accurately model the actual time zone transitions rules used in
532 /// practice.
533 ///
534 /// # Example
535 ///
536 /// ```
537 /// use jiff::{tz::{self, TimeZone}, Timestamp};
538 ///
539 /// let tz = TimeZone::fixed(tz::offset(10));
540 /// assert_eq!(
541 /// tz.to_datetime(Timestamp::UNIX_EPOCH).to_string(),
542 /// "1970-01-01T10:00:00",
543 /// );
544 ///
545 /// # Ok::<(), Box<dyn std::error::Error>>(())
546 /// ```
547 #[inline]
548 pub fn fixed(offset: Offset) -> TimeZone {
549 if offset == Offset::UTC {
550 return TimeZone::UTC;
551 }
552 let fixed = TimeZoneFixed::new(offset);
553 let kind = TimeZoneKind::Fixed(fixed);
554 TimeZone { kind: Some(Arc::new(kind)) }
555 }
556
557 /// Creates a time zone from a [POSIX TZ] rule string.
558 ///
559 /// A POSIX time zone provides a way to tersely define a single daylight
560 /// saving time transition rule (or none at all) that applies for all
561 /// years.
562 ///
563 /// Users should avoid using this kind of time zone unless there is a
564 /// specific need for it. Namely, POSIX time zones cannot capture the full
565 /// complexity of time zone transition rules in the real world. (See the
566 /// example below.)
567 ///
568 /// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
569 ///
570 /// # Errors
571 ///
572 /// This returns an error if the given POSIX time zone string is invalid.
573 ///
574 /// # Example
575 ///
576 /// This example demonstrates how a POSIX time zone may be historically
577 /// inaccurate:
578 ///
579 /// ```
580 /// use jiff::{civil::date, tz::TimeZone};
581 ///
582 /// // The tzdb entry for America/New_York.
583 /// let iana = TimeZone::get("America/New_York")?;
584 /// // The POSIX TZ string for New York DST that went into effect in 2007.
585 /// let posix = TimeZone::posix("EST5EDT,M3.2.0,M11.1.0")?;
586 ///
587 /// // New York entered DST on April 2, 2006 at 2am:
588 /// let dt = date(2006, 4, 2).at(2, 0, 0, 0);
589 /// // The IANA tzdb entry correctly reports it as ambiguous:
590 /// assert!(iana.to_ambiguous_timestamp(dt).is_ambiguous());
591 /// // But the POSIX time zone does not:
592 /// assert!(!posix.to_ambiguous_timestamp(dt).is_ambiguous());
593 ///
594 /// # Ok::<(), Box<dyn std::error::Error>>(())
595 /// ```
596 #[cfg(feature = "alloc")]
597 pub fn posix(posix_tz_string: &str) -> Result<TimeZone, Error> {
598 let iana_tz = posix::IanaTz::parse_v3plus(posix_tz_string)?;
599 let reasonable = iana_tz.into_tz();
600 Ok(TimeZone::from_reasonable_posix_tz(reasonable))
601 }
602
603 /// Creates a time zone from a POSIX tz. Expose so that other parts of Jiff
604 /// can create a `TimeZone` from a POSIX tz. (Kinda sloppy to be honest.)
605 #[cfg(feature = "alloc")]
606 pub(crate) fn from_reasonable_posix_tz(
607 posix: ReasonablePosixTimeZone,
608 ) -> TimeZone {
609 let posix = TimeZonePosix { posix };
610 let kind = TimeZoneKind::Posix(posix);
611 TimeZone { kind: Some(Arc::new(kind)) }
612 }
613
614 /// Creates a time zone from TZif binary data, whose format is specified
615 /// in [RFC 8536]. All versions of TZif (up through version 4) are
616 /// supported.
617 ///
618 /// This constructor is typically not used, and instead, one should rely
619 /// on time zone lookups via time zone identifiers with routines like
620 /// [`TimeZone::get`]. However, this constructor does provide one way
621 /// of using custom time zones with Jiff.
622 ///
623 /// The name given should be a IANA time zone database identifier.
624 ///
625 /// [RFC 8536]: https://datatracker.ietf.org/doc/html/rfc8536
626 ///
627 /// # Errors
628 ///
629 /// This returns an error if the given data was not recognized as valid
630 /// TZif.
631 #[cfg(feature = "alloc")]
632 pub fn tzif(name: &str, data: &[u8]) -> Result<TimeZone, Error> {
633 use alloc::string::ToString;
634
635 let tzif = TimeZoneTzif::new(Some(name.to_string()), data)?;
636 let kind = TimeZoneKind::Tzif(tzif);
637 Ok(TimeZone { kind: Some(Arc::new(kind)) })
638 }
639
640 /// Returns a `TimeZone` that is specifially marked as "unknown."
641 ///
642 /// This corresponds to the Unicode CLDR identifier `Etc/Unknown`, which
643 /// is guaranteed to never be a valid IANA time zone identifier (as of
644 /// the `2025a` release of tzdb).
645 ///
646 /// This type of `TimeZone` is used in circumstances where one wants to
647 /// signal that discovering a time zone failed for some reason, but that
648 /// execution can reasonably continue. For example, [`TimeZone::system`]
649 /// returns this type of time zone when the system time zone could not be
650 /// discovered.
651 ///
652 /// # Example
653 ///
654 /// Jiff permits an "unknown" time zone to losslessly be transmitted
655 /// through serialization:
656 ///
657 /// ```
658 /// use jiff::{civil::date, tz::TimeZone, Zoned};
659 ///
660 /// let tz = TimeZone::unknown();
661 /// let zdt = date(2025, 2, 1).at(17, 0, 0, 0).to_zoned(tz)?;
662 /// assert_eq!(zdt.to_string(), "2025-02-01T17:00:00Z[Etc/Unknown]");
663 /// let got: Zoned = "2025-02-01T17:00:00Z[Etc/Unknown]".parse()?;
664 /// assert_eq!(got, zdt);
665 ///
666 /// # Ok::<(), Box<dyn std::error::Error>>(())
667 /// ```
668 ///
669 /// Note that not all systems support this. Some systems will reject
670 /// `Etc/Unknown` because it is not a valid IANA time zone identifier and
671 /// does not have an entry in the IANA time zone database. However, Jiff
672 /// takes this approach because it surfaces an error condition in detecting
673 /// the end user's time zone. Callers not wanting an "unknown" time zone
674 /// can use `TimeZone::try_system().unwrap_or(TimeZone::UTC)` instead of
675 /// `TimeZone::system`. (Where the latter falls back to the "unknown" time
676 /// zone when a system configured time zone could not be found.)
677 pub fn unknown() -> TimeZone {
678 let kind = TimeZoneKind::Unknown;
679 TimeZone { kind: Some(Arc::new(kind)) }
680 }
681
682 /// This creates an unnamed TZif-backed `TimeZone`.
683 ///
684 /// At present, the only way for an unnamed TZif-backed `TimeZone` to be
685 /// created is when the system time zone has no identifiable name. For
686 /// example, when `/etc/localtime` is hard-linked to a TZif file instead
687 /// of being symlinked. In this case, there is no cheap and unambiguous
688 /// way to determine the time zone name. So we just let it be unnamed.
689 /// Since this is the only such case, and hopefully will only ever be the
690 /// only such case, we consider such unnamed TZif-back `TimeZone` values
691 /// as being the "system" time zone.
692 ///
693 /// When this is used to construct a `TimeZone`, the `TimeZone::name`
694 /// method will be "Local". This is... pretty unfortunate. I'm not sure
695 /// what else to do other than to make `TimeZone::name` return an
696 /// `Option<&str>`. But... we use it in a bunch of places and it just
697 /// seems bad for a time zone to not have a name.
698 ///
699 /// OK, because of the above, I renamed `TimeZone::name` to
700 /// `TimeZone::diagnostic_name`. This should make it clearer that you can't
701 /// really use the name to do anything interesting. This also makes more
702 /// sense for POSIX TZ strings too.
703 ///
704 /// In any case, this routine stays unexported because I don't want TZif
705 /// backed `TimeZone` values to proliferate. If you have a legitimate use
706 /// case otherwise, please file an issue. It will require API design.
707 ///
708 /// # Errors
709 ///
710 /// This returns an error if the given TZif data is invalid.
711 #[cfg(feature = "tz-system")]
712 fn tzif_system(data: &[u8]) -> Result<TimeZone, Error> {
713 let tzif = TimeZoneTzif::new(None, data)?;
714 let kind = TimeZoneKind::Tzif(tzif);
715 Ok(TimeZone { kind: Some(Arc::new(kind)) })
716 }
717
718 #[inline]
719 pub(crate) fn diagnostic_name(&self) -> DiagnosticName<'_> {
720 DiagnosticName(self)
721 }
722
723 /// Returns true if and only if this `TimeZone` can be succinctly
724 /// serialized.
725 ///
726 /// Basically, this is only `false` when this `TimeZone` was created from
727 /// a `/etc/localtime` for which a valid IANA time zone identifier could
728 /// not be extracted. It is also `false` when [`TimeZone::is_unknown`]
729 /// is `true`.
730 #[cfg(feature = "serde")]
731 #[inline]
732 pub(crate) fn has_succinct_serialization(&self) -> bool {
733 let Some(ref kind) = self.kind else { return true };
734 match **kind {
735 TimeZoneKind::Unknown => true,
736 TimeZoneKind::Fixed(_) => true,
737 #[cfg(feature = "alloc")]
738 TimeZoneKind::Posix(_) => true,
739 #[cfg(feature = "alloc")]
740 TimeZoneKind::Tzif(ref tz) => tz.name().is_some(),
741 }
742 }
743
744 /// When this time zone was loaded from an IANA time zone database entry,
745 /// then this returns the canonicalized name for that time zone.
746 ///
747 /// # Example
748 ///
749 /// ```
750 /// use jiff::tz::TimeZone;
751 ///
752 /// let tz = TimeZone::get("america/NEW_YORK")?;
753 /// assert_eq!(tz.iana_name(), Some("America/New_York"));
754 ///
755 /// # Ok::<(), Box<dyn std::error::Error>>(())
756 /// ```
757 #[inline]
758 pub fn iana_name(&self) -> Option<&str> {
759 let Some(ref kind) = self.kind else { return Some("UTC") };
760 match **kind {
761 #[cfg(feature = "alloc")]
762 TimeZoneKind::Tzif(ref tz) => tz.name(),
763 // Note that while `Etc/Unknown` looks like an IANA time zone
764 // identifier, it is specifically and explicitly NOT an IANA time
765 // zone identifier. So we do not return it here if we have an
766 // unknown time zone identifier.
767 _ => None,
768 }
769 }
770
771 /// Returns true if and only if this time zone is unknown.
772 ///
773 /// This has the special internal identifier of `Etc/Unknown`, and this
774 /// is what will be used when converting a `Zoned` to a string.
775 ///
776 /// Note that while `Etc/Unknown` looks like an IANA time zone identifier,
777 /// it is specifically and explicitly not one. It is reserved and is
778 /// guaranteed to never be an IANA time zone identifier.
779 ///
780 /// An unknown time zone can be created via [`TimeZone::unknown`]. It is
781 /// also returned by [`TimeZone::system`] when a system configured time
782 /// zone could not be found.
783 ///
784 /// # Example
785 ///
786 /// ```
787 /// use jiff::tz::TimeZone;
788 ///
789 /// let tz = TimeZone::unknown();
790 /// assert_eq!(tz.iana_name(), None);
791 /// assert!(tz.is_unknown());
792 /// ```
793 #[inline]
794 pub fn is_unknown(&self) -> bool {
795 let Some(ref kind) = self.kind else { return false };
796 matches!(**kind, TimeZoneKind::Unknown)
797 }
798
799 /// When this time zone is a POSIX time zone, return it.
800 ///
801 /// This doesn't attempt to convert other time zones that are representable
802 /// as POSIX time zones to POSIX time zones (e.g., fixed offset time
803 /// zones). Instead, this only returns something when the actual
804 /// representation of the time zone is a POSIX time zone.
805 #[cfg(feature = "alloc")]
806 #[inline]
807 pub(crate) fn posix_tz(&self) -> Option<&ReasonablePosixTimeZone> {
808 let Some(ref kind) = self.kind else { return None };
809 match **kind {
810 #[cfg(feature = "alloc")]
811 TimeZoneKind::Posix(ref tz) => Some(&tz.posix),
812 _ => None,
813 }
814 }
815
816 /// Returns the civil datetime corresponding to the given timestamp in this
817 /// time zone.
818 ///
819 /// This operation is always unambiguous. That is, for any instant in time
820 /// supported by Jiff (that is, a `Timestamp`), there is always precisely
821 /// one civil datetime corresponding to that instant.
822 ///
823 /// Note that this is considered a lower level routine. Consider working
824 /// with zoned datetimes instead, and use [`Zoned::datetime`] to get its
825 /// civil time if necessary.
826 ///
827 /// # Example
828 ///
829 /// ```
830 /// use jiff::{tz::TimeZone, Timestamp};
831 ///
832 /// let tz = TimeZone::get("Europe/Rome")?;
833 /// assert_eq!(
834 /// tz.to_datetime(Timestamp::UNIX_EPOCH).to_string(),
835 /// "1970-01-01T01:00:00",
836 /// );
837 ///
838 /// # Ok::<(), Box<dyn std::error::Error>>(())
839 /// ```
840 ///
841 /// As mentioned above, consider using `Zoned` instead:
842 ///
843 /// ```
844 /// use jiff::{tz::TimeZone, Timestamp};
845 ///
846 /// let zdt = Timestamp::UNIX_EPOCH.in_tz("Europe/Rome")?;
847 /// assert_eq!(zdt.datetime().to_string(), "1970-01-01T01:00:00");
848 ///
849 /// # Ok::<(), Box<dyn std::error::Error>>(())
850 /// ```
851 #[inline]
852 pub fn to_datetime(&self, timestamp: Timestamp) -> DateTime {
853 self.to_offset(timestamp).to_datetime(timestamp)
854 }
855
856 /// Returns the offset corresponding to the given timestamp in this time
857 /// zone.
858 ///
859 /// This operation is always unambiguous. That is, for any instant in time
860 /// supported by Jiff (that is, a `Timestamp`), there is always precisely
861 /// one offset corresponding to that instant.
862 ///
863 /// Given an offset, one can use APIs like [`Offset::to_datetime`] to
864 /// create a civil datetime from a timestamp.
865 ///
866 /// This also returns whether this timestamp is considered to be in
867 /// "daylight saving time," as well as the abbreviation for the time zone
868 /// at this time.
869 ///
870 /// # Example
871 ///
872 /// ```
873 /// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
874 ///
875 /// let tz = TimeZone::get("America/New_York")?;
876 ///
877 /// // A timestamp in DST in New York.
878 /// let ts = Timestamp::from_second(1_720_493_204)?;
879 /// let offset = tz.to_offset(ts);
880 /// assert_eq!(offset, tz::offset(-4));
881 /// assert_eq!(offset.to_datetime(ts).to_string(), "2024-07-08T22:46:44");
882 ///
883 /// // A timestamp *not* in DST in New York.
884 /// let ts = Timestamp::from_second(1_704_941_204)?;
885 /// let offset = tz.to_offset(ts);
886 /// assert_eq!(offset, tz::offset(-5));
887 /// assert_eq!(offset.to_datetime(ts).to_string(), "2024-01-10T21:46:44");
888 ///
889 /// # Ok::<(), Box<dyn std::error::Error>>(())
890 /// ```
891 #[inline]
892 pub fn to_offset(&self, _timestamp: Timestamp) -> Offset {
893 let Some(ref kind) = self.kind else {
894 return Offset::UTC;
895 };
896 match **kind {
897 TimeZoneKind::Unknown => Offset::UTC,
898 TimeZoneKind::Fixed(ref tz) => tz.to_offset(),
899 #[cfg(feature = "alloc")]
900 TimeZoneKind::Posix(ref tz) => tz.to_offset(_timestamp),
901 #[cfg(feature = "alloc")]
902 TimeZoneKind::Tzif(ref tz) => tz.to_offset(_timestamp),
903 }
904 }
905
906 /// Returns the offset information corresponding to the given timestamp in
907 /// this time zone. This includes the offset along with daylight saving
908 /// time status and a time zone abbreviation.
909 ///
910 /// This is like [`TimeZone::to_offset`], but returns the aforementioned
911 /// extra data in addition to the offset. This data may, in some cases, be
912 /// more expensive to compute.
913 ///
914 /// # Example
915 ///
916 /// ```
917 /// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
918 ///
919 /// let tz = TimeZone::get("America/New_York")?;
920 ///
921 /// // A timestamp in DST in New York.
922 /// let ts = Timestamp::from_second(1_720_493_204)?;
923 /// let info = tz.to_offset_info(ts);
924 /// assert_eq!(info.offset(), tz::offset(-4));
925 /// assert_eq!(info.dst(), Dst::Yes);
926 /// assert_eq!(info.abbreviation(), "EDT");
927 /// assert_eq!(
928 /// info.offset().to_datetime(ts).to_string(),
929 /// "2024-07-08T22:46:44",
930 /// );
931 ///
932 /// // A timestamp *not* in DST in New York.
933 /// let ts = Timestamp::from_second(1_704_941_204)?;
934 /// let info = tz.to_offset_info(ts);
935 /// assert_eq!(info.offset(), tz::offset(-5));
936 /// assert_eq!(info.dst(), Dst::No);
937 /// assert_eq!(info.abbreviation(), "EST");
938 /// assert_eq!(
939 /// info.offset().to_datetime(ts).to_string(),
940 /// "2024-01-10T21:46:44",
941 /// );
942 ///
943 /// # Ok::<(), Box<dyn std::error::Error>>(())
944 /// ```
945 #[inline]
946 pub fn to_offset_info<'t>(
947 &'t self,
948 _timestamp: Timestamp,
949 ) -> TimeZoneOffsetInfo<'t> {
950 let Some(ref kind) = self.kind else {
951 return TimeZoneOffsetInfo {
952 offset: Offset::UTC,
953 dst: Dst::No,
954 abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
955 };
956 };
957 match **kind {
958 TimeZoneKind::Unknown => {
959 TimeZoneOffsetInfo {
960 offset: Offset::UTC,
961 dst: Dst::No,
962 // It'd be kinda nice if this were just `ERR` to
963 // indicate an error, but I can't find any precedent
964 // for that. And CLDR says `Etc/Unknown` should behave
965 // like UTC, so... I guess we use UTC here.
966 abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
967 }
968 }
969 TimeZoneKind::Fixed(ref tz) => tz.to_offset_info(),
970 #[cfg(feature = "alloc")]
971 TimeZoneKind::Posix(ref tz) => tz.to_offset_info(_timestamp),
972 #[cfg(feature = "alloc")]
973 TimeZoneKind::Tzif(ref tz) => tz.to_offset_info(_timestamp),
974 }
975 }
976
977 /// If this time zone is a fixed offset, then this returns the offset.
978 /// If this time zone is not a fixed offset, then an error is returned.
979 ///
980 /// If you just need an offset for a given timestamp, then you can use
981 /// [`TimeZone::to_offset`]. Or, if you need an offset for a civil
982 /// datetime, then you can use [`TimeZone::to_ambiguous_timestamp`] or
983 /// [`TimeZone::to_ambiguous_zoned`], although the result may be ambiguous.
984 ///
985 /// Generally, this routine is useful when you need to know whether the
986 /// time zone is fixed, and you want to get the offset without having to
987 /// specify a timestamp. This is sometimes required for interoperating with
988 /// other datetime systems that need to distinguish between time zones that
989 /// are fixed and time zones that are based on rules such as those found in
990 /// the IANA time zone database.
991 ///
992 /// # Example
993 ///
994 /// ```
995 /// use jiff::tz::{Offset, TimeZone};
996 ///
997 /// let tz = TimeZone::get("America/New_York")?;
998 /// // A named time zone is not a fixed offset
999 /// // and so cannot be converted to an offset
1000 /// // without a timestamp or civil datetime.
1001 /// assert_eq!(
1002 /// tz.to_fixed_offset().unwrap_err().to_string(),
1003 /// "cannot convert non-fixed IANA time zone \
1004 /// to offset without timestamp or civil datetime",
1005 /// );
1006 ///
1007 /// let tz = TimeZone::UTC;
1008 /// // UTC is a fixed offset and so can be converted
1009 /// // without a timestamp.
1010 /// assert_eq!(tz.to_fixed_offset()?, Offset::UTC);
1011 ///
1012 /// // And of course, creating a time zone from a
1013 /// // fixed offset results in a fixed offset time
1014 /// // zone too:
1015 /// let tz = TimeZone::fixed(jiff::tz::offset(-10));
1016 /// assert_eq!(tz.to_fixed_offset()?, jiff::tz::offset(-10));
1017 ///
1018 /// # Ok::<(), Box<dyn std::error::Error>>(())
1019 /// ```
1020 #[inline]
1021 pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
1022 let Some(ref kind) = self.kind else { return Ok(Offset::UTC) };
1023 #[allow(irrefutable_let_patterns)]
1024 match **kind {
1025 TimeZoneKind::Unknown => Ok(Offset::UTC),
1026 TimeZoneKind::Fixed(ref tz) => Ok(tz.to_offset()),
1027 #[cfg(feature = "alloc")]
1028 _ => Err(err!(
1029 "cannot convert non-fixed {kind} time zone to offset \
1030 without timestamp or civil datetime",
1031 kind = self.kind_description(),
1032 )),
1033 }
1034 }
1035
1036 /// Converts a civil datetime to a [`Zoned`] in this time zone.
1037 ///
1038 /// The given civil datetime may be ambiguous in this time zone. A civil
1039 /// datetime is ambiguous when either of the following occurs:
1040 ///
1041 /// * When the civil datetime falls into a "gap." That is, when there is a
1042 /// jump forward in time where a span of time does not appear on the clocks
1043 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
1044 /// into daylight saving time.
1045 /// * When the civil datetime falls into a "fold." That is, when there is
1046 /// a jump backward in time where a span of time is _repeated_ on the
1047 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1048 /// backward out of daylight saving time.
1049 ///
1050 /// This routine automatically resolves both of the above ambiguities via
1051 /// the [`Disambiguation::Compatible`] strategy. That in, the case of a
1052 /// gap, the time after the gap is used. In the case of a fold, the first
1053 /// repetition of the clock time is used.
1054 ///
1055 /// # Example
1056 ///
1057 /// This example shows how disambiguation works:
1058 ///
1059 /// ```
1060 /// use jiff::{civil::date, tz::TimeZone};
1061 ///
1062 /// let tz = TimeZone::get("America/New_York")?;
1063 ///
1064 /// // This demonstrates disambiguation behavior for a gap.
1065 /// let zdt = tz.to_zoned(date(2024, 3, 10).at(2, 30, 0, 0))?;
1066 /// assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]");
1067 /// // This demonstrates disambiguation behavior for a fold.
1068 /// // Notice the offset: the -04 corresponds to the time while
1069 /// // still in DST. The second repetition of the 1 o'clock hour
1070 /// // occurs outside of DST, in "standard" time, with the offset -5.
1071 /// let zdt = tz.to_zoned(date(2024, 11, 3).at(1, 30, 0, 0))?;
1072 /// assert_eq!(zdt.to_string(), "2024-11-03T01:30:00-04:00[America/New_York]");
1073 ///
1074 /// # Ok::<(), Box<dyn std::error::Error>>(())
1075 /// ```
1076 #[inline]
1077 pub fn to_zoned(&self, dt: DateTime) -> Result<Zoned, Error> {
1078 self.to_ambiguous_zoned(dt).compatible()
1079 }
1080
1081 /// Converts a civil datetime to a possibly ambiguous zoned datetime in
1082 /// this time zone.
1083 ///
1084 /// The given civil datetime may be ambiguous in this time zone. A civil
1085 /// datetime is ambiguous when either of the following occurs:
1086 ///
1087 /// * When the civil datetime falls into a "gap." That is, when there is a
1088 /// jump forward in time where a span of time does not appear on the clocks
1089 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
1090 /// into daylight saving time.
1091 /// * When the civil datetime falls into a "fold." That is, when there is
1092 /// a jump backward in time where a span of time is _repeated_ on the
1093 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1094 /// backward out of daylight saving time.
1095 ///
1096 /// Unlike [`TimeZone::to_zoned`], this method does not do any automatic
1097 /// disambiguation. Instead, callers are expected to use the methods on
1098 /// [`AmbiguousZoned`] to resolve any ambiguity, if it occurs.
1099 ///
1100 /// # Example
1101 ///
1102 /// This example shows how to return an error when the civil datetime given
1103 /// is ambiguous:
1104 ///
1105 /// ```
1106 /// use jiff::{civil::date, tz::TimeZone};
1107 ///
1108 /// let tz = TimeZone::get("America/New_York")?;
1109 ///
1110 /// // This is not ambiguous:
1111 /// let dt = date(2024, 3, 10).at(1, 0, 0, 0);
1112 /// assert_eq!(
1113 /// tz.to_ambiguous_zoned(dt).unambiguous()?.to_string(),
1114 /// "2024-03-10T01:00:00-05:00[America/New_York]",
1115 /// );
1116 /// // But this is a gap, and thus ambiguous! So an error is returned.
1117 /// let dt = date(2024, 3, 10).at(2, 0, 0, 0);
1118 /// assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
1119 /// // And so is this, because it's a fold.
1120 /// let dt = date(2024, 11, 3).at(1, 0, 0, 0);
1121 /// assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
1122 ///
1123 /// # Ok::<(), Box<dyn std::error::Error>>(())
1124 /// ```
1125 #[inline]
1126 pub fn to_ambiguous_zoned(&self, dt: DateTime) -> AmbiguousZoned {
1127 self.clone().into_ambiguous_zoned(dt)
1128 }
1129
1130 /// Converts a civil datetime to a possibly ambiguous zoned datetime in
1131 /// this time zone, and does so by assuming ownership of this `TimeZone`.
1132 ///
1133 /// This is identical to [`TimeZone::to_ambiguous_zoned`], but it avoids
1134 /// a `TimeZone::clone()` call. (Which are cheap, but not completely free.)
1135 ///
1136 /// # Example
1137 ///
1138 /// This example shows how to create a `Zoned` value from a `TimeZone`
1139 /// and a `DateTime` without cloning the `TimeZone`:
1140 ///
1141 /// ```
1142 /// use jiff::{civil::date, tz::TimeZone};
1143 ///
1144 /// let tz = TimeZone::get("America/New_York")?;
1145 /// let dt = date(2024, 3, 10).at(1, 0, 0, 0);
1146 /// assert_eq!(
1147 /// tz.into_ambiguous_zoned(dt).unambiguous()?.to_string(),
1148 /// "2024-03-10T01:00:00-05:00[America/New_York]",
1149 /// );
1150 ///
1151 /// # Ok::<(), Box<dyn std::error::Error>>(())
1152 /// ```
1153 #[inline]
1154 pub fn into_ambiguous_zoned(self, dt: DateTime) -> AmbiguousZoned {
1155 self.to_ambiguous_timestamp(dt).into_ambiguous_zoned(self)
1156 }
1157
1158 /// Converts a civil datetime to a [`Timestamp`] in this time zone.
1159 ///
1160 /// The given civil datetime may be ambiguous in this time zone. A civil
1161 /// datetime is ambiguous when either of the following occurs:
1162 ///
1163 /// * When the civil datetime falls into a "gap." That is, when there is a
1164 /// jump forward in time where a span of time does not appear on the clocks
1165 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
1166 /// into daylight saving time.
1167 /// * When the civil datetime falls into a "fold." That is, when there is
1168 /// a jump backward in time where a span of time is _repeated_ on the
1169 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1170 /// backward out of daylight saving time.
1171 ///
1172 /// This routine automatically resolves both of the above ambiguities via
1173 /// the [`Disambiguation::Compatible`] strategy. That in, the case of a
1174 /// gap, the time after the gap is used. In the case of a fold, the first
1175 /// repetition of the clock time is used.
1176 ///
1177 /// This routine is identical to [`TimeZone::to_zoned`], except it returns
1178 /// a `Timestamp` instead of a zoned datetime. The benefit of this
1179 /// method is that it never requires cloning or consuming ownership of a
1180 /// `TimeZone`, and it doesn't require construction of `Zoned` which has
1181 /// a small but non-zero cost. (This is partially because a `Zoned` value
1182 /// contains a `TimeZone`, but of course, a `Timestamp` does not.)
1183 ///
1184 /// # Example
1185 ///
1186 /// This example shows how disambiguation works:
1187 ///
1188 /// ```
1189 /// use jiff::{civil::date, tz::TimeZone};
1190 ///
1191 /// let tz = TimeZone::get("America/New_York")?;
1192 ///
1193 /// // This demonstrates disambiguation behavior for a gap.
1194 /// let ts = tz.to_timestamp(date(2024, 3, 10).at(2, 30, 0, 0))?;
1195 /// assert_eq!(ts.to_string(), "2024-03-10T07:30:00Z");
1196 /// // This demonstrates disambiguation behavior for a fold.
1197 /// // Notice the offset: the -04 corresponds to the time while
1198 /// // still in DST. The second repetition of the 1 o'clock hour
1199 /// // occurs outside of DST, in "standard" time, with the offset -5.
1200 /// let ts = tz.to_timestamp(date(2024, 11, 3).at(1, 30, 0, 0))?;
1201 /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z");
1202 ///
1203 /// # Ok::<(), Box<dyn std::error::Error>>(())
1204 /// ```
1205 #[inline]
1206 pub fn to_timestamp(&self, dt: DateTime) -> Result<Timestamp, Error> {
1207 self.to_ambiguous_timestamp(dt).compatible()
1208 }
1209
1210 /// Converts a civil datetime to a possibly ambiguous timestamp in
1211 /// this time zone.
1212 ///
1213 /// The given civil datetime may be ambiguous in this time zone. A civil
1214 /// datetime is ambiguous when either of the following occurs:
1215 ///
1216 /// * When the civil datetime falls into a "gap." That is, when there is a
1217 /// jump forward in time where a span of time does not appear on the clocks
1218 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
1219 /// into daylight saving time.
1220 /// * When the civil datetime falls into a "fold." That is, when there is
1221 /// a jump backward in time where a span of time is _repeated_ on the
1222 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1223 /// backward out of daylight saving time.
1224 ///
1225 /// Unlike [`TimeZone::to_timestamp`], this method does not do any
1226 /// automatic disambiguation. Instead, callers are expected to use the
1227 /// methods on [`AmbiguousTimestamp`] to resolve any ambiguity, if it
1228 /// occurs.
1229 ///
1230 /// This routine is identical to [`TimeZone::to_ambiguous_zoned`], except
1231 /// it returns an `AmbiguousTimestamp` instead of a `AmbiguousZoned`. The
1232 /// benefit of this method is that it never requires cloning or consuming
1233 /// ownership of a `TimeZone`, and it doesn't require construction of
1234 /// `Zoned` which has a small but non-zero cost. (This is partially because
1235 /// a `Zoned` value contains a `TimeZone`, but of course, a `Timestamp`
1236 /// does not.)
1237 ///
1238 /// # Example
1239 ///
1240 /// This example shows how to return an error when the civil datetime given
1241 /// is ambiguous:
1242 ///
1243 /// ```
1244 /// use jiff::{civil::date, tz::TimeZone};
1245 ///
1246 /// let tz = TimeZone::get("America/New_York")?;
1247 ///
1248 /// // This is not ambiguous:
1249 /// let dt = date(2024, 3, 10).at(1, 0, 0, 0);
1250 /// assert_eq!(
1251 /// tz.to_ambiguous_timestamp(dt).unambiguous()?.to_string(),
1252 /// "2024-03-10T06:00:00Z",
1253 /// );
1254 /// // But this is a gap, and thus ambiguous! So an error is returned.
1255 /// let dt = date(2024, 3, 10).at(2, 0, 0, 0);
1256 /// assert!(tz.to_ambiguous_timestamp(dt).unambiguous().is_err());
1257 /// // And so is this, because it's a fold.
1258 /// let dt = date(2024, 11, 3).at(1, 0, 0, 0);
1259 /// assert!(tz.to_ambiguous_timestamp(dt).unambiguous().is_err());
1260 ///
1261 /// # Ok::<(), Box<dyn std::error::Error>>(())
1262 /// ```
1263 #[inline]
1264 pub fn to_ambiguous_timestamp(&self, dt: DateTime) -> AmbiguousTimestamp {
1265 let ambiguous_kind = match self.kind {
1266 None => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
1267 Some(ref kind) => match **kind {
1268 TimeZoneKind::Unknown => {
1269 AmbiguousOffset::Unambiguous { offset: Offset::UTC }
1270 }
1271 TimeZoneKind::Fixed(ref tz) => {
1272 AmbiguousOffset::Unambiguous { offset: tz.to_offset() }
1273 }
1274 #[cfg(feature = "alloc")]
1275 TimeZoneKind::Posix(ref tz) => tz.to_ambiguous_kind(dt),
1276 #[cfg(feature = "alloc")]
1277 TimeZoneKind::Tzif(ref tz) => tz.to_ambiguous_kind(dt),
1278 },
1279 };
1280 AmbiguousTimestamp::new(dt, ambiguous_kind)
1281 }
1282
1283 /// Returns an iterator of time zone transitions preceding the given
1284 /// timestamp. The iterator returned yields [`TimeZoneTransition`]
1285 /// elements.
1286 ///
1287 /// The order of the iterator returned moves backward through time. If
1288 /// there is a previous transition, then the timestamp of that transition
1289 /// is guaranteed to be strictly less than the timestamp given.
1290 ///
1291 /// This is a low level API that you generally shouldn't need. It's
1292 /// useful in cases where you need to know something about the specific
1293 /// instants at which time zone transitions occur. For example, an embedded
1294 /// device might need to be explicitly programmed with daylight saving
1295 /// time transitions. APIs like this enable callers to explore those
1296 /// transitions.
1297 ///
1298 /// A time zone transition refers to a specific point in time when the
1299 /// offset from UTC for a particular geographical region changes. This
1300 /// is usually a result of daylight saving time, but it can also occur
1301 /// when a geographic region changes its permanent offset from UTC.
1302 ///
1303 /// The iterator returned is not guaranteed to yield any elements. For
1304 /// example, this occurs with a fixed offset time zone. Logically, it
1305 /// would also be possible for the iterator to be infinite, except that
1306 /// eventually the timestamp would overflow Jiff's minimum timestamp
1307 /// value, at which point, iteration stops.
1308 ///
1309 /// # Example: time since the previous transition
1310 ///
1311 /// This example shows how much time has passed since the previous time
1312 /// zone transition:
1313 ///
1314 /// ```
1315 /// use jiff::{Unit, Zoned};
1316 ///
1317 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1318 /// let trans = now.time_zone().preceding(now.timestamp()).next().unwrap();
1319 /// let prev_at = trans.timestamp().to_zoned(now.time_zone().clone());
1320 /// let span = now.since((Unit::Year, &prev_at))?;
1321 /// assert_eq!(format!("{span:#}"), "1mo 27d 17h 25m");
1322 ///
1323 /// # Ok::<(), Box<dyn std::error::Error>>(())
1324 /// ```
1325 ///
1326 /// # Example: show the 5 previous time zone transitions
1327 ///
1328 /// This shows how to find the 5 preceding time zone transitions (from a
1329 /// particular datetime) for a particular time zone:
1330 ///
1331 /// ```
1332 /// use jiff::{tz::offset, Zoned};
1333 ///
1334 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1335 /// let transitions = now
1336 /// .time_zone()
1337 /// .preceding(now.timestamp())
1338 /// .take(5)
1339 /// .map(|t| (
1340 /// t.timestamp().to_zoned(now.time_zone().clone()),
1341 /// t.offset(),
1342 /// t.abbreviation().to_string(),
1343 /// ))
1344 /// .collect::<Vec<_>>();
1345 /// assert_eq!(transitions, vec![
1346 /// ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1347 /// ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1348 /// ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1349 /// ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1350 /// ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1351 /// ]);
1352 ///
1353 /// # Ok::<(), Box<dyn std::error::Error>>(())
1354 /// ```
1355 #[inline]
1356 pub fn preceding<'t>(
1357 &'t self,
1358 timestamp: Timestamp,
1359 ) -> TimeZonePrecedingTransitions<'t> {
1360 TimeZonePrecedingTransitions { tz: self, cur: timestamp }
1361 }
1362
1363 /// Returns an iterator of time zone transitions following the given
1364 /// timestamp. The iterator returned yields [`TimeZoneTransition`]
1365 /// elements.
1366 ///
1367 /// The order of the iterator returned moves forward through time. If
1368 /// there is a following transition, then the timestamp of that transition
1369 /// is guaranteed to be strictly greater than the timestamp given.
1370 ///
1371 /// This is a low level API that you generally shouldn't need. It's
1372 /// useful in cases where you need to know something about the specific
1373 /// instants at which time zone transitions occur. For example, an embedded
1374 /// device might need to be explicitly programmed with daylight saving
1375 /// time transitions. APIs like this enable callers to explore those
1376 /// transitions.
1377 ///
1378 /// A time zone transition refers to a specific point in time when the
1379 /// offset from UTC for a particular geographical region changes. This
1380 /// is usually a result of daylight saving time, but it can also occur
1381 /// when a geographic region changes its permanent offset from UTC.
1382 ///
1383 /// The iterator returned is not guaranteed to yield any elements. For
1384 /// example, this occurs with a fixed offset time zone. Logically, it
1385 /// would also be possible for the iterator to be infinite, except that
1386 /// eventually the timestamp would overflow Jiff's maximum timestamp
1387 /// value, at which point, iteration stops.
1388 ///
1389 /// # Example: time until the next transition
1390 ///
1391 /// This example shows how much time is left until the next time zone
1392 /// transition:
1393 ///
1394 /// ```
1395 /// use jiff::{Unit, Zoned};
1396 ///
1397 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1398 /// let trans = now.time_zone().following(now.timestamp()).next().unwrap();
1399 /// let next_at = trans.timestamp().to_zoned(now.time_zone().clone());
1400 /// let span = now.until((Unit::Year, &next_at))?;
1401 /// assert_eq!(format!("{span:#}"), "2mo 8d 7h 35m");
1402 ///
1403 /// # Ok::<(), Box<dyn std::error::Error>>(())
1404 /// ```
1405 ///
1406 /// # Example: show the 5 next time zone transitions
1407 ///
1408 /// This shows how to find the 5 following time zone transitions (from a
1409 /// particular datetime) for a particular time zone:
1410 ///
1411 /// ```
1412 /// use jiff::{tz::offset, Zoned};
1413 ///
1414 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1415 /// let transitions = now
1416 /// .time_zone()
1417 /// .following(now.timestamp())
1418 /// .take(5)
1419 /// .map(|t| (
1420 /// t.timestamp().to_zoned(now.time_zone().clone()),
1421 /// t.offset(),
1422 /// t.abbreviation().to_string(),
1423 /// ))
1424 /// .collect::<Vec<_>>();
1425 /// assert_eq!(transitions, vec![
1426 /// ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1427 /// ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1428 /// ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1429 /// ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1430 /// ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1431 /// ]);
1432 ///
1433 /// # Ok::<(), Box<dyn std::error::Error>>(())
1434 /// ```
1435 #[inline]
1436 pub fn following<'t>(
1437 &'t self,
1438 timestamp: Timestamp,
1439 ) -> TimeZoneFollowingTransitions<'t> {
1440 TimeZoneFollowingTransitions { tz: self, cur: timestamp }
1441 }
1442
1443 /// Used by the "preceding transitions" iterator.
1444 #[inline]
1445 fn previous_transition(
1446 &self,
1447 _timestamp: Timestamp,
1448 ) -> Option<TimeZoneTransition> {
1449 match **self.kind.as_ref()? {
1450 TimeZoneKind::Unknown => None,
1451 TimeZoneKind::Fixed(_) => None,
1452 #[cfg(feature = "alloc")]
1453 TimeZoneKind::Posix(ref tz) => tz.previous_transition(_timestamp),
1454 #[cfg(feature = "alloc")]
1455 TimeZoneKind::Tzif(ref tz) => tz.previous_transition(_timestamp),
1456 }
1457 }
1458
1459 /// Used by the "following transitions" iterator.
1460 #[inline]
1461 fn next_transition(
1462 &self,
1463 _timestamp: Timestamp,
1464 ) -> Option<TimeZoneTransition> {
1465 match **self.kind.as_ref()? {
1466 TimeZoneKind::Unknown => None,
1467 TimeZoneKind::Fixed(_) => None,
1468 #[cfg(feature = "alloc")]
1469 TimeZoneKind::Posix(ref tz) => tz.next_transition(_timestamp),
1470 #[cfg(feature = "alloc")]
1471 TimeZoneKind::Tzif(ref tz) => tz.next_transition(_timestamp),
1472 }
1473 }
1474
1475 /// Returns a short description about the kind of this time zone.
1476 ///
1477 /// This is useful in error messages.
1478 fn kind_description(&self) -> &str {
1479 let Some(ref kind) = self.kind else {
1480 return "UTC";
1481 };
1482 match **kind {
1483 TimeZoneKind::Unknown => "Etc/Unknown",
1484 TimeZoneKind::Fixed(_) => "fixed",
1485 #[cfg(feature = "alloc")]
1486 TimeZoneKind::Posix(_) => "POSIX",
1487 #[cfg(feature = "alloc")]
1488 TimeZoneKind::Tzif(_) => "IANA",
1489 }
1490 }
1491}
1492
1493impl core::fmt::Debug for TimeZone {
1494 #[inline]
1495 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1496 let field: &dyn core::fmt::Debug = match self.kind {
1497 None => &"UTC",
1498 Some(ref kind) => match &**kind {
1499 TimeZoneKind::Unknown => &"Etc/Unknown",
1500 TimeZoneKind::Fixed(ref tz) => tz,
1501 #[cfg(feature = "alloc")]
1502 TimeZoneKind::Posix(ref tz) => tz,
1503 #[cfg(feature = "alloc")]
1504 TimeZoneKind::Tzif(ref tz) => tz,
1505 },
1506 };
1507 f.debug_tuple("TimeZone").field(field).finish()
1508 }
1509}
1510
1511#[derive(Debug, Eq, PartialEq)]
1512#[cfg_attr(not(feature = "alloc"), derive(Clone))]
1513enum TimeZoneKind {
1514 // It would be nice if we could represent this similarly to
1515 // `TimeZone::UTC`. That is, without putting it behind an `Arc`. But I
1516 // didn't see an easy way to do that while retaining the single-word size
1517 // of `TimeZone` without pointer tagging, since `Arc` only gives the
1518 // compiler a single niche value. Plus, it should be exceptionally rare
1519 // for a unknown time zone to be used anyway. It's generally an error
1520 // condition.
1521 Unknown,
1522 Fixed(TimeZoneFixed),
1523 #[cfg(feature = "alloc")]
1524 Posix(TimeZonePosix),
1525 #[cfg(feature = "alloc")]
1526 Tzif(TimeZoneTzif),
1527}
1528
1529#[derive(Clone)]
1530struct TimeZoneFixed {
1531 offset: Offset,
1532}
1533
1534impl TimeZoneFixed {
1535 #[inline]
1536 fn new(offset: Offset) -> TimeZoneFixed {
1537 TimeZoneFixed { offset }
1538 }
1539
1540 #[inline]
1541 fn to_offset(&self) -> Offset {
1542 self.offset
1543 }
1544
1545 #[inline]
1546 fn to_offset_info(&self) -> TimeZoneOffsetInfo<'_> {
1547 let abbreviation =
1548 TimeZoneAbbreviation::Owned(self.offset.to_array_str());
1549 TimeZoneOffsetInfo { offset: self.offset, dst: Dst::No, abbreviation }
1550 }
1551}
1552
1553impl core::fmt::Debug for TimeZoneFixed {
1554 #[inline]
1555 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1556 f.debug_tuple("Fixed").field(&self.to_offset()).finish()
1557 }
1558}
1559
1560impl core::fmt::Display for TimeZoneFixed {
1561 #[inline]
1562 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1563 core::fmt::Display::fmt(&self.offset, f)
1564 }
1565}
1566
1567impl Eq for TimeZoneFixed {}
1568
1569impl PartialEq for TimeZoneFixed {
1570 #[inline]
1571 fn eq(&self, rhs: &TimeZoneFixed) -> bool {
1572 self.to_offset() == rhs.to_offset()
1573 }
1574}
1575
1576#[cfg(feature = "alloc")]
1577#[derive(Clone, Eq, PartialEq)]
1578struct TimeZonePosix {
1579 posix: ReasonablePosixTimeZone,
1580}
1581
1582#[cfg(feature = "alloc")]
1583impl TimeZonePosix {
1584 #[inline]
1585 fn to_offset(&self, timestamp: Timestamp) -> Offset {
1586 self.posix.to_offset(timestamp)
1587 }
1588
1589 #[inline]
1590 fn to_offset_info(&self, timestamp: Timestamp) -> TimeZoneOffsetInfo<'_> {
1591 self.posix.to_offset_info(timestamp)
1592 }
1593
1594 #[inline]
1595 fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
1596 self.posix.to_ambiguous_kind(dt)
1597 }
1598
1599 #[inline]
1600 fn previous_transition(
1601 &self,
1602 timestamp: Timestamp,
1603 ) -> Option<TimeZoneTransition> {
1604 self.posix.previous_transition(timestamp)
1605 }
1606
1607 #[inline]
1608 fn next_transition(
1609 &self,
1610 timestamp: Timestamp,
1611 ) -> Option<TimeZoneTransition> {
1612 self.posix.next_transition(timestamp)
1613 }
1614}
1615
1616// This is implemented by hand because dumping out the full representation of
1617// a `ReasonablePosixTimeZone` is way too much noise for users of Jiff.
1618#[cfg(feature = "alloc")]
1619impl core::fmt::Debug for TimeZonePosix {
1620 #[inline]
1621 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1622 write!(f, "Posix({})", self.posix)
1623 }
1624}
1625
1626#[cfg(feature = "alloc")]
1627impl core::fmt::Display for TimeZonePosix {
1628 #[inline]
1629 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1630 core::fmt::Display::fmt(&self.posix, f)
1631 }
1632}
1633
1634#[cfg(feature = "alloc")]
1635#[derive(Eq, PartialEq)]
1636struct TimeZoneTzif {
1637 tzif: self::tzif::Tzif,
1638}
1639
1640#[cfg(feature = "alloc")]
1641impl TimeZoneTzif {
1642 #[inline]
1643 fn new(
1644 name: Option<alloc::string::String>,
1645 bytes: &[u8],
1646 ) -> Result<TimeZoneTzif, Error> {
1647 let tzif = self::tzif::Tzif::parse(name, bytes)?;
1648 Ok(TimeZoneTzif { tzif })
1649 }
1650
1651 #[inline]
1652 fn name(&self) -> Option<&str> {
1653 self.tzif.name()
1654 }
1655
1656 #[inline]
1657 fn to_offset(&self, timestamp: Timestamp) -> Offset {
1658 self.tzif.to_offset(timestamp)
1659 }
1660
1661 #[inline]
1662 fn to_offset_info(&self, timestamp: Timestamp) -> TimeZoneOffsetInfo<'_> {
1663 self.tzif.to_offset_info(timestamp)
1664 }
1665
1666 #[inline]
1667 fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
1668 self.tzif.to_ambiguous_kind(dt)
1669 }
1670
1671 #[inline]
1672 fn previous_transition(
1673 &self,
1674 timestamp: Timestamp,
1675 ) -> Option<TimeZoneTransition> {
1676 self.tzif.previous_transition(timestamp)
1677 }
1678
1679 #[inline]
1680 fn next_transition(
1681 &self,
1682 timestamp: Timestamp,
1683 ) -> Option<TimeZoneTransition> {
1684 self.tzif.next_transition(timestamp)
1685 }
1686}
1687
1688// This is implemented by hand because dumping out the full representation of
1689// all TZif data is too much noise for users of Jiff.
1690#[cfg(feature = "alloc")]
1691impl core::fmt::Debug for TimeZoneTzif {
1692 #[inline]
1693 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1694 f.debug_tuple("TZif").field(&self.name().unwrap_or("Local")).finish()
1695 }
1696}
1697
1698/// A representation a single time zone transition.
1699///
1700/// A time zone transition is an instant in time the marks the beginning of
1701/// a change in the offset from UTC that civil time is computed from in a
1702/// particular time zone. For example, when daylight saving time comes into
1703/// effect (or goes away). Another example is when a geographic region changes
1704/// its permanent offset from UTC.
1705///
1706/// This is a low level type that you generally shouldn't need. It's useful in
1707/// cases where you need to know something about the specific instants at which
1708/// time zone transitions occur. For example, an embedded device might need to
1709/// be explicitly programmed with daylight saving time transitions. APIs like
1710/// this enable callers to explore those transitions.
1711///
1712/// This type is yielded by the iterators
1713/// [`TimeZonePrecedingTransitions`] and
1714/// [`TimeZoneFollowingTransitions`]. The iterators are created by
1715/// [`TimeZone::preceding`] and [`TimeZone::following`], respectively.
1716///
1717/// # Example
1718///
1719/// This shows a somewhat silly example that finds all of the unique civil
1720/// (or "clock" or "local") times at which a time zone transition has occurred
1721/// in a particular time zone:
1722///
1723/// ```
1724/// use std::collections::BTreeSet;
1725/// use jiff::{civil, tz::TimeZone};
1726///
1727/// let tz = TimeZone::get("America/New_York")?;
1728/// let now = civil::date(2024, 12, 31).at(18, 25, 0, 0).to_zoned(tz.clone())?;
1729/// let mut set = BTreeSet::new();
1730/// for trans in tz.preceding(now.timestamp()) {
1731/// let time = tz.to_datetime(trans.timestamp()).time();
1732/// set.insert(time);
1733/// }
1734/// assert_eq!(Vec::from_iter(set), vec![
1735/// civil::time(1, 0, 0, 0), // typical transition out of DST
1736/// civil::time(3, 0, 0, 0), // typical transition into DST
1737/// civil::time(12, 0, 0, 0), // from when IANA starts keeping track
1738/// civil::time(19, 0, 0, 0), // from World War 2
1739/// ]);
1740///
1741/// # Ok::<(), Box<dyn std::error::Error>>(())
1742/// ```
1743#[derive(Clone, Debug)]
1744pub struct TimeZoneTransition<'t> {
1745 // We don't currently do anything smart to make iterating over
1746 // transitions faster. We could if we pushed the iterator impl down into
1747 // the respective modules (`posix` and `tzif`), but it's not clear such
1748 // optimization is really worth it. However, this API should permit that
1749 // kind of optimization in the future.
1750 timestamp: Timestamp,
1751 offset: Offset,
1752 abbrev: &'t str,
1753 dst: Dst,
1754}
1755
1756impl<'t> TimeZoneTransition<'t> {
1757 /// Returns the timestamp at which this transition began.
1758 ///
1759 /// # Example
1760 ///
1761 /// ```
1762 /// use jiff::{civil, tz::TimeZone};
1763 ///
1764 /// let tz = TimeZone::get("US/Eastern")?;
1765 /// // Look for the first time zone transition in `US/Eastern` following
1766 /// // 2023-03-09 00:00:00.
1767 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1768 /// let next = tz.following(start).next().unwrap();
1769 /// assert_eq!(
1770 /// next.timestamp().to_zoned(tz.clone()).to_string(),
1771 /// "2024-03-10T03:00:00-04:00[US/Eastern]",
1772 /// );
1773 ///
1774 /// # Ok::<(), Box<dyn std::error::Error>>(())
1775 /// ```
1776 #[inline]
1777 pub fn timestamp(&self) -> Timestamp {
1778 self.timestamp
1779 }
1780
1781 /// Returns the offset corresponding to this time zone transition. All
1782 /// instants at and following this transition's timestamp (and before the
1783 /// next transition's timestamp) need to apply this offset from UTC to get
1784 /// the civil or "local" time in the corresponding time zone.
1785 ///
1786 /// # Example
1787 ///
1788 /// ```
1789 /// use jiff::{civil, tz::{TimeZone, offset}};
1790 ///
1791 /// let tz = TimeZone::get("US/Eastern")?;
1792 /// // Get the offset of the next transition after
1793 /// // 2023-03-09 00:00:00.
1794 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1795 /// let next = tz.following(start).next().unwrap();
1796 /// assert_eq!(next.offset(), offset(-4));
1797 /// // Or go backwards to find the previous transition.
1798 /// let prev = tz.preceding(start).next().unwrap();
1799 /// assert_eq!(prev.offset(), offset(-5));
1800 ///
1801 /// # Ok::<(), Box<dyn std::error::Error>>(())
1802 /// ```
1803 #[inline]
1804 pub fn offset(&self) -> Offset {
1805 self.offset
1806 }
1807
1808 /// Returns the time zone abbreviation corresponding to this time
1809 /// zone transition. All instants at and following this transition's
1810 /// timestamp (and before the next transition's timestamp) may use this
1811 /// abbreviation when creating a human readable string. For example,
1812 /// this is the abbreviation used with the `%Z` specifier with Jiff's
1813 /// [`fmt::strtime`](crate::fmt::strtime) module.
1814 ///
1815 /// Note that abbreviations can to be ambiguous. For example, the
1816 /// abbreviation `CST` can be used for the time zones `Asia/Shanghai`,
1817 /// `America/Chicago` and `America/Havana`.
1818 ///
1819 /// The lifetime of the string returned is tied to this
1820 /// `TimeZoneTransition`, which may be shorter than `'t` (the lifetime of
1821 /// the time zone this transition was created from).
1822 ///
1823 /// # Example
1824 ///
1825 /// ```
1826 /// use jiff::{civil, tz::TimeZone};
1827 ///
1828 /// let tz = TimeZone::get("US/Eastern")?;
1829 /// // Get the abbreviation of the next transition after
1830 /// // 2023-03-09 00:00:00.
1831 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1832 /// let next = tz.following(start).next().unwrap();
1833 /// assert_eq!(next.abbreviation(), "EDT");
1834 /// // Or go backwards to find the previous transition.
1835 /// let prev = tz.preceding(start).next().unwrap();
1836 /// assert_eq!(prev.abbreviation(), "EST");
1837 ///
1838 /// # Ok::<(), Box<dyn std::error::Error>>(())
1839 /// ```
1840 #[inline]
1841 pub fn abbreviation<'a>(&'a self) -> &'a str {
1842 self.abbrev
1843 }
1844
1845 /// Returns whether daylight saving time is enabled for this time zone
1846 /// transition.
1847 ///
1848 /// Callers should generally treat this as informational only. In
1849 /// particular, not all time zone transitions are related to daylight
1850 /// saving time. For example, some transitions are a result of a region
1851 /// permanently changing their offset from UTC.
1852 ///
1853 /// # Example
1854 ///
1855 /// ```
1856 /// use jiff::{civil, tz::{Dst, TimeZone}};
1857 ///
1858 /// let tz = TimeZone::get("US/Eastern")?;
1859 /// // Get the DST status of the next transition after
1860 /// // 2023-03-09 00:00:00.
1861 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1862 /// let next = tz.following(start).next().unwrap();
1863 /// assert_eq!(next.dst(), Dst::Yes);
1864 /// // Or go backwards to find the previous transition.
1865 /// let prev = tz.preceding(start).next().unwrap();
1866 /// assert_eq!(prev.dst(), Dst::No);
1867 ///
1868 /// # Ok::<(), Box<dyn std::error::Error>>(())
1869 /// ```
1870 #[inline]
1871 pub fn dst(&self) -> Dst {
1872 self.dst
1873 }
1874}
1875
1876/// An iterator over time zone transitions going backward in time.
1877///
1878/// This iterator is created by [`TimeZone::preceding`].
1879///
1880/// # Example: show the 5 previous time zone transitions
1881///
1882/// This shows how to find the 5 preceding time zone transitions (from a
1883/// particular datetime) for a particular time zone:
1884///
1885/// ```
1886/// use jiff::{tz::offset, Zoned};
1887///
1888/// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1889/// let transitions = now
1890/// .time_zone()
1891/// .preceding(now.timestamp())
1892/// .take(5)
1893/// .map(|t| (
1894/// t.timestamp().to_zoned(now.time_zone().clone()),
1895/// t.offset(),
1896/// t.abbreviation().to_string(),
1897/// ))
1898/// .collect::<Vec<_>>();
1899/// assert_eq!(transitions, vec![
1900/// ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1901/// ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1902/// ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1903/// ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1904/// ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1905/// ]);
1906///
1907/// # Ok::<(), Box<dyn std::error::Error>>(())
1908/// ```
1909#[derive(Clone, Debug)]
1910pub struct TimeZonePrecedingTransitions<'t> {
1911 tz: &'t TimeZone,
1912 cur: Timestamp,
1913}
1914
1915impl<'t> Iterator for TimeZonePrecedingTransitions<'t> {
1916 type Item = TimeZoneTransition<'t>;
1917
1918 fn next(&mut self) -> Option<TimeZoneTransition<'t>> {
1919 let trans = self.tz.previous_transition(self.cur)?;
1920 self.cur = trans.timestamp();
1921 Some(trans)
1922 }
1923}
1924
1925impl<'t> core::iter::FusedIterator for TimeZonePrecedingTransitions<'t> {}
1926
1927/// An iterator over time zone transitions going forward in time.
1928///
1929/// This iterator is created by [`TimeZone::following`].
1930///
1931/// # Example: show the 5 next time zone transitions
1932///
1933/// This shows how to find the 5 following time zone transitions (from a
1934/// particular datetime) for a particular time zone:
1935///
1936/// ```
1937/// use jiff::{tz::offset, Zoned};
1938///
1939/// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1940/// let transitions = now
1941/// .time_zone()
1942/// .following(now.timestamp())
1943/// .take(5)
1944/// .map(|t| (
1945/// t.timestamp().to_zoned(now.time_zone().clone()),
1946/// t.offset(),
1947/// t.abbreviation().to_string(),
1948/// ))
1949/// .collect::<Vec<_>>();
1950/// assert_eq!(transitions, vec![
1951/// ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1952/// ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1953/// ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1954/// ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1955/// ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1956/// ]);
1957///
1958/// # Ok::<(), Box<dyn std::error::Error>>(())
1959/// ```
1960#[derive(Clone, Debug)]
1961pub struct TimeZoneFollowingTransitions<'t> {
1962 tz: &'t TimeZone,
1963 cur: Timestamp,
1964}
1965
1966impl<'t> Iterator for TimeZoneFollowingTransitions<'t> {
1967 type Item = TimeZoneTransition<'t>;
1968
1969 fn next(&mut self) -> Option<TimeZoneTransition<'t>> {
1970 let trans = self.tz.next_transition(self.cur)?;
1971 self.cur = trans.timestamp();
1972 Some(trans)
1973 }
1974}
1975
1976impl<'t> core::iter::FusedIterator for TimeZoneFollowingTransitions<'t> {}
1977
1978/// A helper type for converting a `TimeZone` to a succinct human readable
1979/// description.
1980///
1981/// This is principally used in error messages in various places.
1982///
1983/// A previous iteration of this was just an `as_str() -> &str` method on
1984/// `TimeZone`, but that's difficult to do without relying on dynamic memory
1985/// allocation (or chunky arrays).
1986pub(crate) struct DiagnosticName<'a>(&'a TimeZone);
1987
1988impl<'a> core::fmt::Display for DiagnosticName<'a> {
1989 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1990 let Some(ref kind) = self.0.kind else { return write!(f, "UTC") };
1991 match **kind {
1992 TimeZoneKind::Unknown => write!(f, "Etc/Unknown"),
1993 TimeZoneKind::Fixed(ref tz) => write!(f, "{tz}"),
1994 #[cfg(feature = "alloc")]
1995 TimeZoneKind::Posix(ref tz) => write!(f, "{tz}"),
1996 #[cfg(feature = "alloc")]
1997 TimeZoneKind::Tzif(ref tz) => {
1998 write!(f, "{}", tz.name().unwrap_or("Local"))
1999 }
2000 }
2001 }
2002}
2003
2004/// Configuration for resolving ambiguous datetimes in a particular time zone.
2005///
2006/// This is useful for specifying how to disambiguate ambiguous datetimes at
2007/// runtime. For example, as configuration for parsing [`Zoned`] values via
2008/// [`fmt::temporal::DateTimeParser::disambiguation`](crate::fmt::temporal::DateTimeParser::disambiguation).
2009///
2010/// Note that there is no difference in using
2011/// `Disambiguation::Compatible.disambiguate(ambiguous_timestamp)` and
2012/// `ambiguous_timestamp.compatible()`. They are equivalent. The purpose of
2013/// this enum is to expose the disambiguation strategy as a runtime value for
2014/// configuration purposes.
2015///
2016/// The default value is `Disambiguation::Compatible`, which matches the
2017/// behavior specified in [RFC 5545 (iCalendar)]. Namely, when an ambiguous
2018/// datetime is found in a fold (the clocks are rolled back), then the earlier
2019/// time is selected. And when an ambiguous datetime is found in a gap (the
2020/// clocks are skipped forward), then the later time is selected.
2021///
2022/// This enum is non-exhaustive so that other forms of disambiguation may be
2023/// added in semver compatible releases.
2024///
2025/// [RFC 5545 (iCalendar)]: https://datatracker.ietf.org/doc/html/rfc5545
2026///
2027/// # Example
2028///
2029/// This example shows the default disambiguation mode ("compatible") when
2030/// given a datetime that falls in a "gap" (i.e., a forwards DST transition).
2031///
2032/// ```
2033/// use jiff::{civil::date, tz};
2034///
2035/// let newyork = tz::db().get("America/New_York")?;
2036/// let ambiguous = newyork.to_ambiguous_zoned(date(2024, 3, 10).at(2, 30, 0, 0));
2037///
2038/// // NOTE: This is identical to `ambiguous.compatible()`.
2039/// let zdt = ambiguous.disambiguate(tz::Disambiguation::Compatible)?;
2040/// assert_eq!(zdt.datetime(), date(2024, 3, 10).at(3, 30, 0, 0));
2041/// // In compatible mode, forward transitions select the later
2042/// // time. In the EST->EDT transition, that's the -04 (EDT) offset.
2043/// assert_eq!(zdt.offset(), tz::offset(-4));
2044///
2045/// # Ok::<(), Box<dyn std::error::Error>>(())
2046/// ```
2047///
2048/// # Example: parsing
2049///
2050/// This example shows how to set the disambiguation configuration while
2051/// parsing a [`Zoned`] datetime. In this example, we always prefer the earlier
2052/// time.
2053///
2054/// ```
2055/// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
2056///
2057/// static PARSER: DateTimeParser = DateTimeParser::new()
2058/// .disambiguation(tz::Disambiguation::Earlier);
2059///
2060/// let zdt = PARSER.parse_zoned("2024-03-10T02:30[America/New_York]")?;
2061/// // In earlier mode, forward transitions select the earlier time, unlike
2062/// // in compatible mode. In this case, that's the pre-DST offset of -05.
2063/// assert_eq!(zdt.datetime(), date(2024, 3, 10).at(1, 30, 0, 0));
2064/// assert_eq!(zdt.offset(), tz::offset(-5));
2065///
2066/// # Ok::<(), Box<dyn std::error::Error>>(())
2067/// ```
2068#[derive(Clone, Copy, Debug, Default)]
2069#[non_exhaustive]
2070pub enum Disambiguation {
2071 /// In a backward transition, the earlier time is selected. In forward
2072 /// transition, the later time is selected.
2073 ///
2074 /// This is equivalent to [`AmbiguousTimestamp::compatible`] and
2075 /// [`AmbiguousZoned::compatible`].
2076 #[default]
2077 Compatible,
2078 /// The earlier time is always selected.
2079 ///
2080 /// This is equivalent to [`AmbiguousTimestamp::earlier`] and
2081 /// [`AmbiguousZoned::earlier`].
2082 Earlier,
2083 /// The later time is always selected.
2084 ///
2085 /// This is equivalent to [`AmbiguousTimestamp::later`] and
2086 /// [`AmbiguousZoned::later`].
2087 Later,
2088 /// When an ambiguous datetime is encountered, this strategy will always
2089 /// result in an error. This is useful if you need to require datetimes
2090 /// from users to unambiguously refer to a specific instant.
2091 ///
2092 /// This is equivalent to [`AmbiguousTimestamp::unambiguous`] and
2093 /// [`AmbiguousZoned::unambiguous`].
2094 Reject,
2095}
2096
2097/// A possibly ambiguous [`Offset`].
2098///
2099/// An `AmbiguousOffset` is part of both [`AmbiguousTimestamp`] and
2100/// [`AmbiguousZoned`], which are created by
2101/// [`TimeZone::to_ambiguous_timestamp`] and
2102/// [`TimeZone::to_ambiguous_zoned`], respectively.
2103///
2104/// When converting a civil datetime in a particular time zone to a precise
2105/// instant in time (that is, either `Timestamp` or `Zoned`), then the primary
2106/// thing needed to form a precise instant in time is an [`Offset`]. The
2107/// problem is that some civil datetimes are ambiguous. That is, some do not
2108/// exist (because they fall into a gap, where some civil time is skipped),
2109/// or some are repeated (because they fall into a fold, where some civil time
2110/// is repeated).
2111///
2112/// The purpose of this type is to represent that ambiguity when it occurs.
2113/// The ambiguity is manifest through the offset choice: it is either the
2114/// offset _before_ the transition or the offset _after_ the transition. This
2115/// is true regardless of whether the ambiguity occurs as a result of a gap
2116/// or a fold.
2117///
2118/// It is generally considered very rare to need to inspect values of this
2119/// type directly. Instead, higher level routines like
2120/// [`AmbiguousZoned::compatible`] or [`AmbiguousZoned::unambiguous`] will
2121/// implement a strategy for you.
2122///
2123/// # Example
2124///
2125/// This example shows how the "compatible" disambiguation strategy is
2126/// implemented. Recall that the "compatible" strategy chooses the offset
2127/// corresponding to the civil datetime after a gap, and the offset
2128/// corresponding to the civil datetime before a gap.
2129///
2130/// ```
2131/// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2132///
2133/// let tz = tz::db().get("America/New_York")?;
2134/// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2135/// let offset = match tz.to_ambiguous_timestamp(dt).offset() {
2136/// AmbiguousOffset::Unambiguous { offset } => offset,
2137/// // This is counter-intuitive, but in order to get the civil datetime
2138/// // *after* the gap, we need to select the offset from *before* the
2139/// // gap.
2140/// AmbiguousOffset::Gap { before, .. } => before,
2141/// AmbiguousOffset::Fold { before, .. } => before,
2142/// };
2143/// assert_eq!(offset.to_timestamp(dt)?.to_string(), "2024-03-10T07:30:00Z");
2144///
2145/// # Ok::<(), Box<dyn std::error::Error>>(())
2146/// ```
2147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2148pub enum AmbiguousOffset {
2149 /// The offset for a particular civil datetime and time zone is
2150 /// unambiguous.
2151 ///
2152 /// This is the overwhelmingly common case. In general, the only time this
2153 /// case does not occur is when there is a transition to a different time
2154 /// zone (rare) or to/from daylight saving time (occurs for 1 hour twice
2155 /// in year in many geographic locations).
2156 Unambiguous {
2157 /// The offset from UTC for the corresponding civil datetime given. The
2158 /// offset is determined via the relevant time zone data, and in this
2159 /// case, there is only one possible offset that could be applied to
2160 /// the given civil datetime.
2161 offset: Offset,
2162 },
2163 /// The offset for a particular civil datetime and time zone is ambiguous
2164 /// because there is a gap.
2165 ///
2166 /// This most commonly occurs when a civil datetime corresponds to an hour
2167 /// that was "skipped" in a jump to DST (daylight saving time).
2168 Gap {
2169 /// The offset corresponding to the time before a gap.
2170 ///
2171 /// For example, given a time zone of `America/Los_Angeles`, the offset
2172 /// for time immediately preceding `2020-03-08T02:00:00` is `-08`.
2173 before: Offset,
2174 /// The offset corresponding to the later time in a gap.
2175 ///
2176 /// For example, given a time zone of `America/Los_Angeles`, the offset
2177 /// for time immediately following `2020-03-08T02:59:59` is `-07`.
2178 after: Offset,
2179 },
2180 /// The offset for a particular civil datetime and time zone is ambiguous
2181 /// because there is a fold.
2182 ///
2183 /// This most commonly occurs when a civil datetime corresponds to an hour
2184 /// that was "repeated" in a jump to standard time from DST (daylight
2185 /// saving time).
2186 Fold {
2187 /// The offset corresponding to the earlier time in a fold.
2188 ///
2189 /// For example, given a time zone of `America/Los_Angeles`, the offset
2190 /// for time on the first `2020-11-01T01:00:00` is `-07`.
2191 before: Offset,
2192 /// The offset corresponding to the earlier time in a fold.
2193 ///
2194 /// For example, given a time zone of `America/Los_Angeles`, the offset
2195 /// for time on the second `2020-11-01T01:00:00` is `-08`.
2196 after: Offset,
2197 },
2198}
2199
2200/// A possibly ambiguous [`Timestamp`], created by
2201/// [`TimeZone::to_ambiguous_timestamp`].
2202///
2203/// While this is called an ambiguous _timestamp_, the thing that is
2204/// actually ambiguous is the offset. That is, an ambiguous timestamp is
2205/// actually a pair of a [`civil::DateTime`](crate::civil::DateTime) and an
2206/// [`AmbiguousOffset`].
2207///
2208/// When the offset is ambiguous, it either represents a gap (civil time is
2209/// skipped) or a fold (civil time is repeated). In both cases, there are, by
2210/// construction, two different offsets to choose from: the offset from before
2211/// the transition and the offset from after the transition.
2212///
2213/// The purpose of this type is to represent that ambiguity (when it occurs)
2214/// and enable callers to make a choice about how to resolve that ambiguity.
2215/// In some cases, you might want to reject ambiguity altogether, which is
2216/// supported by the [`AmbiguousTimestamp::unambiguous`] routine.
2217///
2218/// This type provides four different out-of-the-box disambiguation strategies:
2219///
2220/// * [`AmbiguousTimestamp::compatible`] implements the
2221/// [`Disambiguation::Compatible`] strategy. In the case of a gap, the offset
2222/// after the gap is selected. In the case of a fold, the offset before the
2223/// fold occurs is selected.
2224/// * [`AmbiguousTimestamp::earlier`] implements the
2225/// [`Disambiguation::Earlier`] strategy. This always selects the "earlier"
2226/// offset.
2227/// * [`AmbiguousTimestamp::later`] implements the
2228/// [`Disambiguation::Later`] strategy. This always selects the "later"
2229/// offset.
2230/// * [`AmbiguousTimestamp::unambiguous`] implements the
2231/// [`Disambiguation::Reject`] strategy. It acts as an assertion that the
2232/// offset is unambiguous. If it is ambiguous, then an appropriate error is
2233/// returned.
2234///
2235/// The [`AmbiguousTimestamp::disambiguate`] method can be used with the
2236/// [`Disambiguation`] enum when the disambiguation strategy isn't known until
2237/// runtime.
2238///
2239/// Note also that these aren't the only disambiguation strategies. The
2240/// [`AmbiguousOffset`] type, accessible via [`AmbiguousTimestamp::offset`],
2241/// exposes the full details of the ambiguity. So any strategy can be
2242/// implemented.
2243///
2244/// # Example
2245///
2246/// This example shows how the "compatible" disambiguation strategy is
2247/// implemented. Recall that the "compatible" strategy chooses the offset
2248/// corresponding to the civil datetime after a gap, and the offset
2249/// corresponding to the civil datetime before a gap.
2250///
2251/// ```
2252/// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2253///
2254/// let tz = tz::db().get("America/New_York")?;
2255/// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2256/// let offset = match tz.to_ambiguous_timestamp(dt).offset() {
2257/// AmbiguousOffset::Unambiguous { offset } => offset,
2258/// // This is counter-intuitive, but in order to get the civil datetime
2259/// // *after* the gap, we need to select the offset from *before* the
2260/// // gap.
2261/// AmbiguousOffset::Gap { before, .. } => before,
2262/// AmbiguousOffset::Fold { before, .. } => before,
2263/// };
2264/// assert_eq!(offset.to_timestamp(dt)?.to_string(), "2024-03-10T07:30:00Z");
2265///
2266/// # Ok::<(), Box<dyn std::error::Error>>(())
2267/// ```
2268#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2269pub struct AmbiguousTimestamp {
2270 dt: DateTime,
2271 offset: AmbiguousOffset,
2272}
2273
2274impl AmbiguousTimestamp {
2275 #[inline]
2276 fn new(dt: DateTime, kind: AmbiguousOffset) -> AmbiguousTimestamp {
2277 AmbiguousTimestamp { dt, offset: kind }
2278 }
2279
2280 /// Returns the civil datetime that was used to create this ambiguous
2281 /// timestamp.
2282 ///
2283 /// # Example
2284 ///
2285 /// ```
2286 /// use jiff::{civil::date, tz};
2287 ///
2288 /// let tz = tz::db().get("America/New_York")?;
2289 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
2290 /// let ts = tz.to_ambiguous_timestamp(dt);
2291 /// assert_eq!(ts.datetime(), dt);
2292 ///
2293 /// # Ok::<(), Box<dyn std::error::Error>>(())
2294 /// ```
2295 #[inline]
2296 pub fn datetime(&self) -> DateTime {
2297 self.dt
2298 }
2299
2300 /// Returns the possibly ambiguous offset that is the ultimate source of
2301 /// ambiguity.
2302 ///
2303 /// Most civil datetimes are not ambiguous, and thus, the offset will not
2304 /// be ambiguous either. In this case, the offset returned will be the
2305 /// [`AmbiguousOffset::Unambiguous`] variant.
2306 ///
2307 /// But, not all civil datetimes are unambiguous. There are exactly two
2308 /// cases where a civil datetime can be ambiguous: when a civil datetime
2309 /// does not exist (a gap) or when a civil datetime is repeated (a fold).
2310 /// In both such cases, the _offset_ is the thing that is ambiguous as
2311 /// there are two possible choices for the offset in both cases: the offset
2312 /// before the transition (whether it's a gap or a fold) or the offset
2313 /// after the transition.
2314 ///
2315 /// This type captures the fact that computing an offset from a civil
2316 /// datetime in a particular time zone is in one of three possible states:
2317 ///
2318 /// 1. It is unambiguous.
2319 /// 2. It is ambiguous because there is a gap in time.
2320 /// 3. It is ambiguous because there is a fold in time.
2321 ///
2322 /// # Example
2323 ///
2324 /// ```
2325 /// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2326 ///
2327 /// let tz = tz::db().get("America/New_York")?;
2328 ///
2329 /// // Not ambiguous.
2330 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2331 /// let ts = tz.to_ambiguous_timestamp(dt);
2332 /// assert_eq!(ts.offset(), AmbiguousOffset::Unambiguous {
2333 /// offset: tz::offset(-4),
2334 /// });
2335 ///
2336 /// // Ambiguous because of a gap.
2337 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2338 /// let ts = tz.to_ambiguous_timestamp(dt);
2339 /// assert_eq!(ts.offset(), AmbiguousOffset::Gap {
2340 /// before: tz::offset(-5),
2341 /// after: tz::offset(-4),
2342 /// });
2343 ///
2344 /// // Ambiguous because of a fold.
2345 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2346 /// let ts = tz.to_ambiguous_timestamp(dt);
2347 /// assert_eq!(ts.offset(), AmbiguousOffset::Fold {
2348 /// before: tz::offset(-4),
2349 /// after: tz::offset(-5),
2350 /// });
2351 ///
2352 /// # Ok::<(), Box<dyn std::error::Error>>(())
2353 /// ```
2354 #[inline]
2355 pub fn offset(&self) -> AmbiguousOffset {
2356 self.offset
2357 }
2358
2359 /// Returns true if and only if this possibly ambiguous timestamp is
2360 /// actually ambiguous.
2361 ///
2362 /// This occurs precisely in cases when the offset is _not_
2363 /// [`AmbiguousOffset::Unambiguous`].
2364 ///
2365 /// # Example
2366 ///
2367 /// ```
2368 /// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2369 ///
2370 /// let tz = tz::db().get("America/New_York")?;
2371 ///
2372 /// // Not ambiguous.
2373 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2374 /// let ts = tz.to_ambiguous_timestamp(dt);
2375 /// assert!(!ts.is_ambiguous());
2376 ///
2377 /// // Ambiguous because of a gap.
2378 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2379 /// let ts = tz.to_ambiguous_timestamp(dt);
2380 /// assert!(ts.is_ambiguous());
2381 ///
2382 /// // Ambiguous because of a fold.
2383 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2384 /// let ts = tz.to_ambiguous_timestamp(dt);
2385 /// assert!(ts.is_ambiguous());
2386 ///
2387 /// # Ok::<(), Box<dyn std::error::Error>>(())
2388 /// ```
2389 #[inline]
2390 pub fn is_ambiguous(&self) -> bool {
2391 !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
2392 }
2393
2394 /// Disambiguates this timestamp according to the
2395 /// [`Disambiguation::Compatible`] strategy.
2396 ///
2397 /// If this timestamp is unambiguous, then this is a no-op.
2398 ///
2399 /// The "compatible" strategy selects the offset corresponding to the civil
2400 /// time after a gap, and the offset corresponding to the civil time before
2401 /// a fold. This is what is specified in [RFC 5545].
2402 ///
2403 /// [RFC 5545]: https://datatracker.ietf.org/doc/html/rfc5545
2404 ///
2405 /// # Errors
2406 ///
2407 /// This returns an error when the combination of the civil datetime
2408 /// and offset would lead to a `Timestamp` outside of the
2409 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
2410 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
2411 /// and [`DateTime::MAX`] limits.
2412 ///
2413 /// # Example
2414 ///
2415 /// ```
2416 /// use jiff::{civil::date, tz};
2417 ///
2418 /// let tz = tz::db().get("America/New_York")?;
2419 ///
2420 /// // Not ambiguous.
2421 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2422 /// let ts = tz.to_ambiguous_timestamp(dt);
2423 /// assert_eq!(
2424 /// ts.compatible()?.to_string(),
2425 /// "2024-07-15T21:30:00Z",
2426 /// );
2427 ///
2428 /// // Ambiguous because of a gap.
2429 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2430 /// let ts = tz.to_ambiguous_timestamp(dt);
2431 /// assert_eq!(
2432 /// ts.compatible()?.to_string(),
2433 /// "2024-03-10T07:30:00Z",
2434 /// );
2435 ///
2436 /// // Ambiguous because of a fold.
2437 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2438 /// let ts = tz.to_ambiguous_timestamp(dt);
2439 /// assert_eq!(
2440 /// ts.compatible()?.to_string(),
2441 /// "2024-11-03T05:30:00Z",
2442 /// );
2443 ///
2444 /// # Ok::<(), Box<dyn std::error::Error>>(())
2445 /// ```
2446 #[inline]
2447 pub fn compatible(self) -> Result<Timestamp, Error> {
2448 let offset = match self.offset() {
2449 AmbiguousOffset::Unambiguous { offset } => offset,
2450 AmbiguousOffset::Gap { before, .. } => before,
2451 AmbiguousOffset::Fold { before, .. } => before,
2452 };
2453 offset.to_timestamp(self.dt)
2454 }
2455
2456 /// Disambiguates this timestamp according to the
2457 /// [`Disambiguation::Earlier`] strategy.
2458 ///
2459 /// If this timestamp is unambiguous, then this is a no-op.
2460 ///
2461 /// The "earlier" strategy selects the offset corresponding to the civil
2462 /// time before a gap, and the offset corresponding to the civil time
2463 /// before a fold.
2464 ///
2465 /// # Errors
2466 ///
2467 /// This returns an error when the combination of the civil datetime
2468 /// and offset would lead to a `Timestamp` outside of the
2469 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
2470 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
2471 /// and [`DateTime::MAX`] limits.
2472 ///
2473 /// # Example
2474 ///
2475 /// ```
2476 /// use jiff::{civil::date, tz};
2477 ///
2478 /// let tz = tz::db().get("America/New_York")?;
2479 ///
2480 /// // Not ambiguous.
2481 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2482 /// let ts = tz.to_ambiguous_timestamp(dt);
2483 /// assert_eq!(
2484 /// ts.earlier()?.to_string(),
2485 /// "2024-07-15T21:30:00Z",
2486 /// );
2487 ///
2488 /// // Ambiguous because of a gap.
2489 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2490 /// let ts = tz.to_ambiguous_timestamp(dt);
2491 /// assert_eq!(
2492 /// ts.earlier()?.to_string(),
2493 /// "2024-03-10T06:30:00Z",
2494 /// );
2495 ///
2496 /// // Ambiguous because of a fold.
2497 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2498 /// let ts = tz.to_ambiguous_timestamp(dt);
2499 /// assert_eq!(
2500 /// ts.earlier()?.to_string(),
2501 /// "2024-11-03T05:30:00Z",
2502 /// );
2503 ///
2504 /// # Ok::<(), Box<dyn std::error::Error>>(())
2505 /// ```
2506 #[inline]
2507 pub fn earlier(self) -> Result<Timestamp, Error> {
2508 let offset = match self.offset() {
2509 AmbiguousOffset::Unambiguous { offset } => offset,
2510 AmbiguousOffset::Gap { after, .. } => after,
2511 AmbiguousOffset::Fold { before, .. } => before,
2512 };
2513 offset.to_timestamp(self.dt)
2514 }
2515
2516 /// Disambiguates this timestamp according to the
2517 /// [`Disambiguation::Later`] strategy.
2518 ///
2519 /// If this timestamp is unambiguous, then this is a no-op.
2520 ///
2521 /// The "later" strategy selects the offset corresponding to the civil
2522 /// time after a gap, and the offset corresponding to the civil time
2523 /// after a fold.
2524 ///
2525 /// # Errors
2526 ///
2527 /// This returns an error when the combination of the civil datetime
2528 /// and offset would lead to a `Timestamp` outside of the
2529 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
2530 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
2531 /// and [`DateTime::MAX`] limits.
2532 ///
2533 /// # Example
2534 ///
2535 /// ```
2536 /// use jiff::{civil::date, tz};
2537 ///
2538 /// let tz = tz::db().get("America/New_York")?;
2539 ///
2540 /// // Not ambiguous.
2541 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2542 /// let ts = tz.to_ambiguous_timestamp(dt);
2543 /// assert_eq!(
2544 /// ts.later()?.to_string(),
2545 /// "2024-07-15T21:30:00Z",
2546 /// );
2547 ///
2548 /// // Ambiguous because of a gap.
2549 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2550 /// let ts = tz.to_ambiguous_timestamp(dt);
2551 /// assert_eq!(
2552 /// ts.later()?.to_string(),
2553 /// "2024-03-10T07:30:00Z",
2554 /// );
2555 ///
2556 /// // Ambiguous because of a fold.
2557 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2558 /// let ts = tz.to_ambiguous_timestamp(dt);
2559 /// assert_eq!(
2560 /// ts.later()?.to_string(),
2561 /// "2024-11-03T06:30:00Z",
2562 /// );
2563 ///
2564 /// # Ok::<(), Box<dyn std::error::Error>>(())
2565 /// ```
2566 #[inline]
2567 pub fn later(self) -> Result<Timestamp, Error> {
2568 let offset = match self.offset() {
2569 AmbiguousOffset::Unambiguous { offset } => offset,
2570 AmbiguousOffset::Gap { before, .. } => before,
2571 AmbiguousOffset::Fold { after, .. } => after,
2572 };
2573 offset.to_timestamp(self.dt)
2574 }
2575
2576 /// Disambiguates this timestamp according to the
2577 /// [`Disambiguation::Reject`] strategy.
2578 ///
2579 /// If this timestamp is unambiguous, then this is a no-op.
2580 ///
2581 /// The "reject" strategy always returns an error when the timestamp
2582 /// is ambiguous.
2583 ///
2584 /// # Errors
2585 ///
2586 /// This returns an error when the combination of the civil datetime
2587 /// and offset would lead to a `Timestamp` outside of the
2588 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
2589 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
2590 /// and [`DateTime::MAX`] limits.
2591 ///
2592 /// This also returns an error when the timestamp is ambiguous.
2593 ///
2594 /// # Example
2595 ///
2596 /// ```
2597 /// use jiff::{civil::date, tz};
2598 ///
2599 /// let tz = tz::db().get("America/New_York")?;
2600 ///
2601 /// // Not ambiguous.
2602 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2603 /// let ts = tz.to_ambiguous_timestamp(dt);
2604 /// assert_eq!(
2605 /// ts.later()?.to_string(),
2606 /// "2024-07-15T21:30:00Z",
2607 /// );
2608 ///
2609 /// // Ambiguous because of a gap.
2610 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2611 /// let ts = tz.to_ambiguous_timestamp(dt);
2612 /// assert!(ts.unambiguous().is_err());
2613 ///
2614 /// // Ambiguous because of a fold.
2615 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2616 /// let ts = tz.to_ambiguous_timestamp(dt);
2617 /// assert!(ts.unambiguous().is_err());
2618 ///
2619 /// # Ok::<(), Box<dyn std::error::Error>>(())
2620 /// ```
2621 #[inline]
2622 pub fn unambiguous(self) -> Result<Timestamp, Error> {
2623 let offset = match self.offset() {
2624 AmbiguousOffset::Unambiguous { offset } => offset,
2625 AmbiguousOffset::Gap { before, after } => {
2626 return Err(err!(
2627 "the datetime {dt} is ambiguous since it falls into \
2628 a gap between offsets {before} and {after}",
2629 dt = self.dt,
2630 ));
2631 }
2632 AmbiguousOffset::Fold { before, after } => {
2633 return Err(err!(
2634 "the datetime {dt} is ambiguous since it falls into \
2635 a fold between offsets {before} and {after}",
2636 dt = self.dt,
2637 ));
2638 }
2639 };
2640 offset.to_timestamp(self.dt)
2641 }
2642
2643 /// Disambiguates this (possibly ambiguous) timestamp into a specific
2644 /// timestamp.
2645 ///
2646 /// This is the same as calling one of the disambiguation methods, but
2647 /// the method chosen is indicated by the option given. This is useful
2648 /// when the disambiguation option needs to be chosen at runtime.
2649 ///
2650 /// # Errors
2651 ///
2652 /// This returns an error if this would have returned a timestamp
2653 /// outside of its minimum and maximum values.
2654 ///
2655 /// This can also return an error when using the [`Disambiguation::Reject`]
2656 /// strategy. Namely, when using the `Reject` strategy, any ambiguous
2657 /// timestamp always results in an error.
2658 ///
2659 /// # Example
2660 ///
2661 /// This example shows the various disambiguation modes when given a
2662 /// datetime that falls in a "fold" (i.e., a backwards DST transition).
2663 ///
2664 /// ```
2665 /// use jiff::{civil::date, tz::{self, Disambiguation}};
2666 ///
2667 /// let newyork = tz::db().get("America/New_York")?;
2668 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2669 /// let ambiguous = newyork.to_ambiguous_timestamp(dt);
2670 ///
2671 /// // In compatible mode, backward transitions select the earlier
2672 /// // time. In the EDT->EST transition, that's the -04 (EDT) offset.
2673 /// let ts = ambiguous.clone().disambiguate(Disambiguation::Compatible)?;
2674 /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z");
2675 ///
2676 /// // The earlier time in the EDT->EST transition is the -04 (EDT) offset.
2677 /// let ts = ambiguous.clone().disambiguate(Disambiguation::Earlier)?;
2678 /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z");
2679 ///
2680 /// // The later time in the EDT->EST transition is the -05 (EST) offset.
2681 /// let ts = ambiguous.clone().disambiguate(Disambiguation::Later)?;
2682 /// assert_eq!(ts.to_string(), "2024-11-03T06:30:00Z");
2683 ///
2684 /// // Since our datetime is ambiguous, the 'reject' strategy errors.
2685 /// assert!(ambiguous.disambiguate(Disambiguation::Reject).is_err());
2686 ///
2687 /// # Ok::<(), Box<dyn std::error::Error>>(())
2688 /// ```
2689 #[inline]
2690 pub fn disambiguate(
2691 self,
2692 option: Disambiguation,
2693 ) -> Result<Timestamp, Error> {
2694 match option {
2695 Disambiguation::Compatible => self.compatible(),
2696 Disambiguation::Earlier => self.earlier(),
2697 Disambiguation::Later => self.later(),
2698 Disambiguation::Reject => self.unambiguous(),
2699 }
2700 }
2701
2702 /// Convert this ambiguous timestamp into an ambiguous zoned date time by
2703 /// attaching a time zone.
2704 ///
2705 /// This is useful when you have a [`civil::DateTime`], [`TimeZone`] and
2706 /// want to convert it to an instant while applying a particular
2707 /// disambiguation strategy without an extra clone of the `TimeZone`.
2708 ///
2709 /// This isn't currently exposed because I believe use cases for crate
2710 /// users can be satisfied via [`TimeZone::into_ambiguous_zoned`] (which
2711 /// is implemented via this routine).
2712 #[inline]
2713 fn into_ambiguous_zoned(self, tz: TimeZone) -> AmbiguousZoned {
2714 AmbiguousZoned::new(self, tz)
2715 }
2716}
2717
2718/// A possibly ambiguous [`Zoned`], created by
2719/// [`TimeZone::to_ambiguous_zoned`].
2720///
2721/// While this is called an ambiguous zoned datetime, the thing that is
2722/// actually ambiguous is the offset. That is, an ambiguous zoned datetime
2723/// is actually a triple of a [`civil::DateTime`](crate::civil::DateTime), a
2724/// [`TimeZone`] and an [`AmbiguousOffset`].
2725///
2726/// When the offset is ambiguous, it either represents a gap (civil time is
2727/// skipped) or a fold (civil time is repeated). In both cases, there are, by
2728/// construction, two different offsets to choose from: the offset from before
2729/// the transition and the offset from after the transition.
2730///
2731/// The purpose of this type is to represent that ambiguity (when it occurs)
2732/// and enable callers to make a choice about how to resolve that ambiguity.
2733/// In some cases, you might want to reject ambiguity altogether, which is
2734/// supported by the [`AmbiguousZoned::unambiguous`] routine.
2735///
2736/// This type provides four different out-of-the-box disambiguation strategies:
2737///
2738/// * [`AmbiguousZoned::compatible`] implements the
2739/// [`Disambiguation::Compatible`] strategy. In the case of a gap, the offset
2740/// after the gap is selected. In the case of a fold, the offset before the
2741/// fold occurs is selected.
2742/// * [`AmbiguousZoned::earlier`] implements the
2743/// [`Disambiguation::Earlier`] strategy. This always selects the "earlier"
2744/// offset.
2745/// * [`AmbiguousZoned::later`] implements the
2746/// [`Disambiguation::Later`] strategy. This always selects the "later"
2747/// offset.
2748/// * [`AmbiguousZoned::unambiguous`] implements the
2749/// [`Disambiguation::Reject`] strategy. It acts as an assertion that the
2750/// offset is unambiguous. If it is ambiguous, then an appropriate error is
2751/// returned.
2752///
2753/// The [`AmbiguousZoned::disambiguate`] method can be used with the
2754/// [`Disambiguation`] enum when the disambiguation strategy isn't known until
2755/// runtime.
2756///
2757/// Note also that these aren't the only disambiguation strategies. The
2758/// [`AmbiguousOffset`] type, accessible via [`AmbiguousZoned::offset`],
2759/// exposes the full details of the ambiguity. So any strategy can be
2760/// implemented.
2761///
2762/// # Example
2763///
2764/// This example shows how the "compatible" disambiguation strategy is
2765/// implemented. Recall that the "compatible" strategy chooses the offset
2766/// corresponding to the civil datetime after a gap, and the offset
2767/// corresponding to the civil datetime before a gap.
2768///
2769/// ```
2770/// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2771///
2772/// let tz = tz::db().get("America/New_York")?;
2773/// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2774/// let ambiguous = tz.to_ambiguous_zoned(dt);
2775/// let offset = match ambiguous.offset() {
2776/// AmbiguousOffset::Unambiguous { offset } => offset,
2777/// // This is counter-intuitive, but in order to get the civil datetime
2778/// // *after* the gap, we need to select the offset from *before* the
2779/// // gap.
2780/// AmbiguousOffset::Gap { before, .. } => before,
2781/// AmbiguousOffset::Fold { before, .. } => before,
2782/// };
2783/// let zdt = offset.to_timestamp(dt)?.to_zoned(ambiguous.into_time_zone());
2784/// assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]");
2785///
2786/// # Ok::<(), Box<dyn std::error::Error>>(())
2787/// ```
2788#[derive(Clone, Debug, Eq, PartialEq)]
2789pub struct AmbiguousZoned {
2790 ts: AmbiguousTimestamp,
2791 tz: TimeZone,
2792}
2793
2794impl AmbiguousZoned {
2795 #[inline]
2796 fn new(ts: AmbiguousTimestamp, tz: TimeZone) -> AmbiguousZoned {
2797 AmbiguousZoned { ts, tz }
2798 }
2799
2800 /// Returns a reference to the time zone that was used to create this
2801 /// ambiguous zoned datetime.
2802 ///
2803 /// # Example
2804 ///
2805 /// ```
2806 /// use jiff::{civil::date, tz};
2807 ///
2808 /// let tz = tz::db().get("America/New_York")?;
2809 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
2810 /// let zdt = tz.to_ambiguous_zoned(dt);
2811 /// assert_eq!(&tz, zdt.time_zone());
2812 ///
2813 /// # Ok::<(), Box<dyn std::error::Error>>(())
2814 /// ```
2815 #[inline]
2816 pub fn time_zone(&self) -> &TimeZone {
2817 &self.tz
2818 }
2819
2820 /// Consumes this ambiguous zoned datetime and returns the underlying
2821 /// `TimeZone`. This is useful if you no longer need the ambiguous zoned
2822 /// datetime and want its `TimeZone` without cloning it. (Cloning a
2823 /// `TimeZone` is cheap but not free.)
2824 ///
2825 /// # Example
2826 ///
2827 /// ```
2828 /// use jiff::{civil::date, tz};
2829 ///
2830 /// let tz = tz::db().get("America/New_York")?;
2831 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
2832 /// let zdt = tz.to_ambiguous_zoned(dt);
2833 /// assert_eq!(tz, zdt.into_time_zone());
2834 ///
2835 /// # Ok::<(), Box<dyn std::error::Error>>(())
2836 /// ```
2837 #[inline]
2838 pub fn into_time_zone(self) -> TimeZone {
2839 self.tz
2840 }
2841
2842 /// Returns the civil datetime that was used to create this ambiguous
2843 /// zoned datetime.
2844 ///
2845 /// # Example
2846 ///
2847 /// ```
2848 /// use jiff::{civil::date, tz};
2849 ///
2850 /// let tz = tz::db().get("America/New_York")?;
2851 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
2852 /// let zdt = tz.to_ambiguous_zoned(dt);
2853 /// assert_eq!(zdt.datetime(), dt);
2854 ///
2855 /// # Ok::<(), Box<dyn std::error::Error>>(())
2856 /// ```
2857 #[inline]
2858 pub fn datetime(&self) -> DateTime {
2859 self.ts.datetime()
2860 }
2861
2862 /// Returns the possibly ambiguous offset that is the ultimate source of
2863 /// ambiguity.
2864 ///
2865 /// Most civil datetimes are not ambiguous, and thus, the offset will not
2866 /// be ambiguous either. In this case, the offset returned will be the
2867 /// [`AmbiguousOffset::Unambiguous`] variant.
2868 ///
2869 /// But, not all civil datetimes are unambiguous. There are exactly two
2870 /// cases where a civil datetime can be ambiguous: when a civil datetime
2871 /// does not exist (a gap) or when a civil datetime is repeated (a fold).
2872 /// In both such cases, the _offset_ is the thing that is ambiguous as
2873 /// there are two possible choices for the offset in both cases: the offset
2874 /// before the transition (whether it's a gap or a fold) or the offset
2875 /// after the transition.
2876 ///
2877 /// This type captures the fact that computing an offset from a civil
2878 /// datetime in a particular time zone is in one of three possible states:
2879 ///
2880 /// 1. It is unambiguous.
2881 /// 2. It is ambiguous because there is a gap in time.
2882 /// 3. It is ambiguous because there is a fold in time.
2883 ///
2884 /// # Example
2885 ///
2886 /// ```
2887 /// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2888 ///
2889 /// let tz = tz::db().get("America/New_York")?;
2890 ///
2891 /// // Not ambiguous.
2892 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2893 /// let zdt = tz.to_ambiguous_zoned(dt);
2894 /// assert_eq!(zdt.offset(), AmbiguousOffset::Unambiguous {
2895 /// offset: tz::offset(-4),
2896 /// });
2897 ///
2898 /// // Ambiguous because of a gap.
2899 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2900 /// let zdt = tz.to_ambiguous_zoned(dt);
2901 /// assert_eq!(zdt.offset(), AmbiguousOffset::Gap {
2902 /// before: tz::offset(-5),
2903 /// after: tz::offset(-4),
2904 /// });
2905 ///
2906 /// // Ambiguous because of a fold.
2907 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2908 /// let zdt = tz.to_ambiguous_zoned(dt);
2909 /// assert_eq!(zdt.offset(), AmbiguousOffset::Fold {
2910 /// before: tz::offset(-4),
2911 /// after: tz::offset(-5),
2912 /// });
2913 ///
2914 /// # Ok::<(), Box<dyn std::error::Error>>(())
2915 /// ```
2916 #[inline]
2917 pub fn offset(&self) -> AmbiguousOffset {
2918 self.ts.offset
2919 }
2920
2921 /// Returns true if and only if this possibly ambiguous zoned datetime is
2922 /// actually ambiguous.
2923 ///
2924 /// This occurs precisely in cases when the offset is _not_
2925 /// [`AmbiguousOffset::Unambiguous`].
2926 ///
2927 /// # Example
2928 ///
2929 /// ```
2930 /// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
2931 ///
2932 /// let tz = tz::db().get("America/New_York")?;
2933 ///
2934 /// // Not ambiguous.
2935 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2936 /// let zdt = tz.to_ambiguous_zoned(dt);
2937 /// assert!(!zdt.is_ambiguous());
2938 ///
2939 /// // Ambiguous because of a gap.
2940 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2941 /// let zdt = tz.to_ambiguous_zoned(dt);
2942 /// assert!(zdt.is_ambiguous());
2943 ///
2944 /// // Ambiguous because of a fold.
2945 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
2946 /// let zdt = tz.to_ambiguous_zoned(dt);
2947 /// assert!(zdt.is_ambiguous());
2948 ///
2949 /// # Ok::<(), Box<dyn std::error::Error>>(())
2950 /// ```
2951 #[inline]
2952 pub fn is_ambiguous(&self) -> bool {
2953 !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
2954 }
2955
2956 /// Disambiguates this zoned datetime according to the
2957 /// [`Disambiguation::Compatible`] strategy.
2958 ///
2959 /// If this zoned datetime is unambiguous, then this is a no-op.
2960 ///
2961 /// The "compatible" strategy selects the offset corresponding to the civil
2962 /// time after a gap, and the offset corresponding to the civil time before
2963 /// a fold. This is what is specified in [RFC 5545].
2964 ///
2965 /// [RFC 5545]: https://datatracker.ietf.org/doc/html/rfc5545
2966 ///
2967 /// # Errors
2968 ///
2969 /// This returns an error when the combination of the civil datetime
2970 /// and offset would lead to a `Zoned` with a timestamp outside of the
2971 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
2972 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
2973 /// and [`DateTime::MAX`] limits.
2974 ///
2975 /// # Example
2976 ///
2977 /// ```
2978 /// use jiff::{civil::date, tz};
2979 ///
2980 /// let tz = tz::db().get("America/New_York")?;
2981 ///
2982 /// // Not ambiguous.
2983 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
2984 /// let zdt = tz.to_ambiguous_zoned(dt);
2985 /// assert_eq!(
2986 /// zdt.compatible()?.to_string(),
2987 /// "2024-07-15T17:30:00-04:00[America/New_York]",
2988 /// );
2989 ///
2990 /// // Ambiguous because of a gap.
2991 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
2992 /// let zdt = tz.to_ambiguous_zoned(dt);
2993 /// assert_eq!(
2994 /// zdt.compatible()?.to_string(),
2995 /// "2024-03-10T03:30:00-04:00[America/New_York]",
2996 /// );
2997 ///
2998 /// // Ambiguous because of a fold.
2999 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
3000 /// let zdt = tz.to_ambiguous_zoned(dt);
3001 /// assert_eq!(
3002 /// zdt.compatible()?.to_string(),
3003 /// "2024-11-03T01:30:00-04:00[America/New_York]",
3004 /// );
3005 ///
3006 /// # Ok::<(), Box<dyn std::error::Error>>(())
3007 /// ```
3008 #[inline]
3009 pub fn compatible(self) -> Result<Zoned, Error> {
3010 let ts = self.ts.compatible().with_context(|| {
3011 err!(
3012 "error converting datetime {dt} to instant in time zone {tz}",
3013 dt = self.datetime(),
3014 tz = self.time_zone().diagnostic_name(),
3015 )
3016 })?;
3017 Ok(ts.to_zoned(self.tz))
3018 }
3019
3020 /// Disambiguates this zoned datetime according to the
3021 /// [`Disambiguation::Earlier`] strategy.
3022 ///
3023 /// If this zoned datetime is unambiguous, then this is a no-op.
3024 ///
3025 /// The "earlier" strategy selects the offset corresponding to the civil
3026 /// time before a gap, and the offset corresponding to the civil time
3027 /// before a fold.
3028 ///
3029 /// # Errors
3030 ///
3031 /// This returns an error when the combination of the civil datetime
3032 /// and offset would lead to a `Zoned` with a timestamp outside of the
3033 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
3034 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
3035 /// and [`DateTime::MAX`] limits.
3036 ///
3037 /// # Example
3038 ///
3039 /// ```
3040 /// use jiff::{civil::date, tz};
3041 ///
3042 /// let tz = tz::db().get("America/New_York")?;
3043 ///
3044 /// // Not ambiguous.
3045 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
3046 /// let zdt = tz.to_ambiguous_zoned(dt);
3047 /// assert_eq!(
3048 /// zdt.earlier()?.to_string(),
3049 /// "2024-07-15T17:30:00-04:00[America/New_York]",
3050 /// );
3051 ///
3052 /// // Ambiguous because of a gap.
3053 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
3054 /// let zdt = tz.to_ambiguous_zoned(dt);
3055 /// assert_eq!(
3056 /// zdt.earlier()?.to_string(),
3057 /// "2024-03-10T01:30:00-05:00[America/New_York]",
3058 /// );
3059 ///
3060 /// // Ambiguous because of a fold.
3061 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
3062 /// let zdt = tz.to_ambiguous_zoned(dt);
3063 /// assert_eq!(
3064 /// zdt.earlier()?.to_string(),
3065 /// "2024-11-03T01:30:00-04:00[America/New_York]",
3066 /// );
3067 ///
3068 /// # Ok::<(), Box<dyn std::error::Error>>(())
3069 /// ```
3070 #[inline]
3071 pub fn earlier(self) -> Result<Zoned, Error> {
3072 let ts = self.ts.earlier().with_context(|| {
3073 err!(
3074 "error converting datetime {dt} to instant in time zone {tz}",
3075 dt = self.datetime(),
3076 tz = self.time_zone().diagnostic_name(),
3077 )
3078 })?;
3079 Ok(ts.to_zoned(self.tz))
3080 }
3081
3082 /// Disambiguates this zoned datetime according to the
3083 /// [`Disambiguation::Later`] strategy.
3084 ///
3085 /// If this zoned datetime is unambiguous, then this is a no-op.
3086 ///
3087 /// The "later" strategy selects the offset corresponding to the civil
3088 /// time after a gap, and the offset corresponding to the civil time
3089 /// after a fold.
3090 ///
3091 /// # Errors
3092 ///
3093 /// This returns an error when the combination of the civil datetime
3094 /// and offset would lead to a `Zoned` with a timestamp outside of the
3095 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
3096 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
3097 /// and [`DateTime::MAX`] limits.
3098 ///
3099 /// # Example
3100 ///
3101 /// ```
3102 /// use jiff::{civil::date, tz};
3103 ///
3104 /// let tz = tz::db().get("America/New_York")?;
3105 ///
3106 /// // Not ambiguous.
3107 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
3108 /// let zdt = tz.to_ambiguous_zoned(dt);
3109 /// assert_eq!(
3110 /// zdt.later()?.to_string(),
3111 /// "2024-07-15T17:30:00-04:00[America/New_York]",
3112 /// );
3113 ///
3114 /// // Ambiguous because of a gap.
3115 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
3116 /// let zdt = tz.to_ambiguous_zoned(dt);
3117 /// assert_eq!(
3118 /// zdt.later()?.to_string(),
3119 /// "2024-03-10T03:30:00-04:00[America/New_York]",
3120 /// );
3121 ///
3122 /// // Ambiguous because of a fold.
3123 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
3124 /// let zdt = tz.to_ambiguous_zoned(dt);
3125 /// assert_eq!(
3126 /// zdt.later()?.to_string(),
3127 /// "2024-11-03T01:30:00-05:00[America/New_York]",
3128 /// );
3129 ///
3130 /// # Ok::<(), Box<dyn std::error::Error>>(())
3131 /// ```
3132 #[inline]
3133 pub fn later(self) -> Result<Zoned, Error> {
3134 let ts = self.ts.later().with_context(|| {
3135 err!(
3136 "error converting datetime {dt} to instant in time zone {tz}",
3137 dt = self.datetime(),
3138 tz = self.time_zone().diagnostic_name(),
3139 )
3140 })?;
3141 Ok(ts.to_zoned(self.tz))
3142 }
3143
3144 /// Disambiguates this zoned datetime according to the
3145 /// [`Disambiguation::Reject`] strategy.
3146 ///
3147 /// If this zoned datetime is unambiguous, then this is a no-op.
3148 ///
3149 /// The "reject" strategy always returns an error when the zoned datetime
3150 /// is ambiguous.
3151 ///
3152 /// # Errors
3153 ///
3154 /// This returns an error when the combination of the civil datetime
3155 /// and offset would lead to a `Zoned` with a timestamp outside of the
3156 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
3157 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
3158 /// and [`DateTime::MAX`] limits.
3159 ///
3160 /// This also returns an error when the timestamp is ambiguous.
3161 ///
3162 /// # Example
3163 ///
3164 /// ```
3165 /// use jiff::{civil::date, tz};
3166 ///
3167 /// let tz = tz::db().get("America/New_York")?;
3168 ///
3169 /// // Not ambiguous.
3170 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
3171 /// let zdt = tz.to_ambiguous_zoned(dt);
3172 /// assert_eq!(
3173 /// zdt.later()?.to_string(),
3174 /// "2024-07-15T17:30:00-04:00[America/New_York]",
3175 /// );
3176 ///
3177 /// // Ambiguous because of a gap.
3178 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
3179 /// let zdt = tz.to_ambiguous_zoned(dt);
3180 /// assert!(zdt.unambiguous().is_err());
3181 ///
3182 /// // Ambiguous because of a fold.
3183 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
3184 /// let zdt = tz.to_ambiguous_zoned(dt);
3185 /// assert!(zdt.unambiguous().is_err());
3186 ///
3187 /// # Ok::<(), Box<dyn std::error::Error>>(())
3188 /// ```
3189 #[inline]
3190 pub fn unambiguous(self) -> Result<Zoned, Error> {
3191 let ts = self.ts.unambiguous().with_context(|| {
3192 err!(
3193 "error converting datetime {dt} to instant in time zone {tz}",
3194 dt = self.datetime(),
3195 tz = self.time_zone().diagnostic_name(),
3196 )
3197 })?;
3198 Ok(ts.to_zoned(self.tz))
3199 }
3200
3201 /// Disambiguates this (possibly ambiguous) timestamp into a concrete
3202 /// time zone aware timestamp.
3203 ///
3204 /// This is the same as calling one of the disambiguation methods, but
3205 /// the method chosen is indicated by the option given. This is useful
3206 /// when the disambiguation option needs to be chosen at runtime.
3207 ///
3208 /// # Errors
3209 ///
3210 /// This returns an error if this would have returned a zoned datetime
3211 /// outside of its minimum and maximum values.
3212 ///
3213 /// This can also return an error when using the [`Disambiguation::Reject`]
3214 /// strategy. Namely, when using the `Reject` strategy, any ambiguous
3215 /// timestamp always results in an error.
3216 ///
3217 /// # Example
3218 ///
3219 /// This example shows the various disambiguation modes when given a
3220 /// datetime that falls in a "fold" (i.e., a backwards DST transition).
3221 ///
3222 /// ```
3223 /// use jiff::{civil::date, tz::{self, Disambiguation}};
3224 ///
3225 /// let newyork = tz::db().get("America/New_York")?;
3226 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
3227 /// let ambiguous = newyork.to_ambiguous_zoned(dt);
3228 ///
3229 /// // In compatible mode, backward transitions select the earlier
3230 /// // time. In the EDT->EST transition, that's the -04 (EDT) offset.
3231 /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Compatible)?;
3232 /// assert_eq!(
3233 /// zdt.to_string(),
3234 /// "2024-11-03T01:30:00-04:00[America/New_York]",
3235 /// );
3236 ///
3237 /// // The earlier time in the EDT->EST transition is the -04 (EDT) offset.
3238 /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Earlier)?;
3239 /// assert_eq!(
3240 /// zdt.to_string(),
3241 /// "2024-11-03T01:30:00-04:00[America/New_York]",
3242 /// );
3243 ///
3244 /// // The later time in the EDT->EST transition is the -05 (EST) offset.
3245 /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Later)?;
3246 /// assert_eq!(
3247 /// zdt.to_string(),
3248 /// "2024-11-03T01:30:00-05:00[America/New_York]",
3249 /// );
3250 ///
3251 /// // Since our datetime is ambiguous, the 'reject' strategy errors.
3252 /// assert!(ambiguous.disambiguate(Disambiguation::Reject).is_err());
3253 ///
3254 /// # Ok::<(), Box<dyn std::error::Error>>(())
3255 /// ```
3256 #[inline]
3257 pub fn disambiguate(self, option: Disambiguation) -> Result<Zoned, Error> {
3258 match option {
3259 Disambiguation::Compatible => self.compatible(),
3260 Disambiguation::Earlier => self.earlier(),
3261 Disambiguation::Later => self.later(),
3262 Disambiguation::Reject => self.unambiguous(),
3263 }
3264 }
3265}
3266
3267/// An offset along with DST status and a time zone abbreviation.
3268///
3269/// This information can be computed from a [`TimeZone`] given a [`Timestamp`]
3270/// via [`TimeZone::to_offset_info`].
3271///
3272/// Generally, the extra information associated with the offset is not commonly
3273/// needed. And indeed, inspecting the daylight saving time status of a
3274/// particular instant in a time zone _usually_ leads to bugs. For example, not
3275/// all time zone transitions are the result of daylight saving time. Some are
3276/// the result of permanent changes to the standard UTC offset of a region.
3277///
3278/// This information is available via an API distinct from
3279/// [`TimeZone::to_offset`] because it is not commonly needed and because it
3280/// can sometimes be more expensive to compute.
3281///
3282/// The main use case for daylight saving time status or time zone
3283/// abbreviations is for formatting datetimes in an end user's locale. If you
3284/// want this, consider using the [`icu`] crate via [`jiff-icu`].
3285///
3286/// The lifetime parameter `'t` corresponds to the lifetime of the `TimeZone`
3287/// that this info was extracted from.
3288///
3289/// # Example
3290///
3291/// ```
3292/// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
3293///
3294/// let tz = TimeZone::get("America/New_York")?;
3295///
3296/// // A timestamp in DST in New York.
3297/// let ts = Timestamp::from_second(1_720_493_204)?;
3298/// let info = tz.to_offset_info(ts);
3299/// assert_eq!(info.offset(), tz::offset(-4));
3300/// assert_eq!(info.dst(), Dst::Yes);
3301/// assert_eq!(info.abbreviation(), "EDT");
3302/// assert_eq!(
3303/// info.offset().to_datetime(ts).to_string(),
3304/// "2024-07-08T22:46:44",
3305/// );
3306///
3307/// // A timestamp *not* in DST in New York.
3308/// let ts = Timestamp::from_second(1_704_941_204)?;
3309/// let info = tz.to_offset_info(ts);
3310/// assert_eq!(info.offset(), tz::offset(-5));
3311/// assert_eq!(info.dst(), Dst::No);
3312/// assert_eq!(info.abbreviation(), "EST");
3313/// assert_eq!(
3314/// info.offset().to_datetime(ts).to_string(),
3315/// "2024-01-10T21:46:44",
3316/// );
3317///
3318/// # Ok::<(), Box<dyn std::error::Error>>(())
3319/// ```
3320///
3321/// [`icu`]: https://docs.rs/icu
3322/// [`jiff-icu`]: https://docs.rs/jiff-icu
3323#[derive(Clone, Debug, Eq, Hash, PartialEq)]
3324pub struct TimeZoneOffsetInfo<'t> {
3325 offset: Offset,
3326 dst: Dst,
3327 abbreviation: TimeZoneAbbreviation<'t>,
3328}
3329
3330impl<'t> TimeZoneOffsetInfo<'t> {
3331 /// Returns the offset.
3332 ///
3333 /// The offset is duration, from UTC, that should be used to offset the
3334 /// civil time in a particular location.
3335 ///
3336 /// # Example
3337 ///
3338 /// ```
3339 /// use jiff::{civil, tz::{TimeZone, offset}};
3340 ///
3341 /// let tz = TimeZone::get("US/Eastern")?;
3342 /// // Get the offset for 2023-03-10 00:00:00.
3343 /// let start = civil::date(2024, 3, 10).to_zoned(tz.clone())?.timestamp();
3344 /// let info = tz.to_offset_info(start);
3345 /// assert_eq!(info.offset(), offset(-5));
3346 /// // Go forward a day and notice the offset changes due to DST!
3347 /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
3348 /// let info = tz.to_offset_info(start);
3349 /// assert_eq!(info.offset(), offset(-4));
3350 ///
3351 /// # Ok::<(), Box<dyn std::error::Error>>(())
3352 /// ```
3353 #[inline]
3354 pub fn offset(&self) -> Offset {
3355 self.offset
3356 }
3357
3358 /// Returns the time zone abbreviation corresponding to this offset info.
3359 ///
3360 /// Note that abbreviations can to be ambiguous. For example, the
3361 /// abbreviation `CST` can be used for the time zones `Asia/Shanghai`,
3362 /// `America/Chicago` and `America/Havana`.
3363 ///
3364 /// The lifetime of the string returned is tied to this
3365 /// `TimeZoneOffsetInfo`, which may be shorter than `'t` (the lifetime of
3366 /// the time zone this transition was created from).
3367 ///
3368 /// # Example
3369 ///
3370 /// ```
3371 /// use jiff::{civil, tz::TimeZone};
3372 ///
3373 /// let tz = TimeZone::get("US/Eastern")?;
3374 /// // Get the time zone abbreviation for 2023-03-10 00:00:00.
3375 /// let start = civil::date(2024, 3, 10).to_zoned(tz.clone())?.timestamp();
3376 /// let info = tz.to_offset_info(start);
3377 /// assert_eq!(info.abbreviation(), "EST");
3378 /// // Go forward a day and notice the abbreviation changes due to DST!
3379 /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
3380 /// let info = tz.to_offset_info(start);
3381 /// assert_eq!(info.abbreviation(), "EDT");
3382 ///
3383 /// # Ok::<(), Box<dyn std::error::Error>>(())
3384 /// ```
3385 #[inline]
3386 pub fn abbreviation(&self) -> &str {
3387 self.abbreviation.as_str()
3388 }
3389
3390 /// Returns whether daylight saving time is enabled for this offset
3391 /// info.
3392 ///
3393 /// Callers should generally treat this as informational only. In
3394 /// particular, not all time zone transitions are related to daylight
3395 /// saving time. For example, some transitions are a result of a region
3396 /// permanently changing their offset from UTC.
3397 ///
3398 /// # Example
3399 ///
3400 /// ```
3401 /// use jiff::{civil, tz::{Dst, TimeZone}};
3402 ///
3403 /// let tz = TimeZone::get("US/Eastern")?;
3404 /// // Get the DST status of 2023-03-11 00:00:00.
3405 /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
3406 /// let info = tz.to_offset_info(start);
3407 /// assert_eq!(info.dst(), Dst::Yes);
3408 ///
3409 /// # Ok::<(), Box<dyn std::error::Error>>(())
3410 /// ```
3411 #[inline]
3412 pub fn dst(&self) -> Dst {
3413 self.dst
3414 }
3415}
3416
3417/// A light abstraction over different representations of a time zone
3418/// abbreviation.
3419///
3420/// The lifetime parameter `'t` corresponds to the lifetime of the time zone
3421/// that produced this abbreviation.
3422#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3423enum TimeZoneAbbreviation<'t> {
3424 /// For when the abbreviation is borrowed directly from other data. For
3425 /// example, from TZif or from POSIX TZ strings.
3426 Borrowed(&'t str),
3427 /// For when the abbreviation has to be derived from other data. For
3428 /// example, from a fixed offset.
3429 ///
3430 /// The idea here is that a `TimeZone` shouldn't need to store the
3431 /// string representation of a fixed offset. Particularly in core-only
3432 /// environments, this is quite wasteful. So we make the string on-demand
3433 /// only when it's requested.
3434 ///
3435 /// An alternative design is to just implement `Display` and reuse
3436 /// `Offset`'s `Display` impl, but then we couldn't offer a `-> &str` API.
3437 /// I feel like that's just a bit overkill, and really just comes from the
3438 /// core-only straight-jacket.
3439 Owned(ArrayStr<9>),
3440}
3441
3442impl<'t> TimeZoneAbbreviation<'t> {
3443 /// Returns this abbreviation as a string borrowed from `self`.
3444 ///
3445 /// Notice that, like `Cow`, the lifetime of the string returned is
3446 /// tied to `self` and thus may be shorter than `'t`.
3447 fn as_str<'a>(&'a self) -> &'a str {
3448 match *self {
3449 TimeZoneAbbreviation::Borrowed(s) => s,
3450 TimeZoneAbbreviation::Owned(ref s) => s.as_str(),
3451 }
3452 }
3453}
3454
3455/// Creates a new time zone offset in a `const` context from a given number
3456/// of hours.
3457///
3458/// Negative offsets correspond to time zones west of the prime meridian,
3459/// while positive offsets correspond to time zones east of the prime
3460/// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
3461///
3462/// The fallible non-const version of this constructor is
3463/// [`Offset::from_hours`].
3464///
3465/// This is a convenience free function for [`Offset::constant`]. It is
3466/// intended to provide a terse syntax for constructing `Offset` values from
3467/// a value that is known to be valid.
3468///
3469/// # Panics
3470///
3471/// This routine panics when the given number of hours is out of range.
3472/// Namely, `hours` must be in the range `-25..=25`.
3473///
3474/// Similarly, when used in a const context, an out of bounds hour will prevent
3475/// your Rust program from compiling.
3476///
3477/// # Example
3478///
3479/// ```
3480/// use jiff::tz::offset;
3481///
3482/// let o = offset(-5);
3483/// assert_eq!(o.seconds(), -18_000);
3484/// let o = offset(5);
3485/// assert_eq!(o.seconds(), 18_000);
3486/// ```
3487#[inline]
3488pub const fn offset(hours: i8) -> Offset {
3489 Offset::constant(hours)
3490}
3491
3492#[cfg(test)]
3493mod tests {
3494 use crate::civil::date;
3495 #[cfg(feature = "alloc")]
3496 use crate::tz::testdata::TzifTestFile;
3497
3498 use super::*;
3499
3500 fn unambiguous(offset_hours: i8) -> AmbiguousOffset {
3501 let offset = offset(offset_hours);
3502 o_unambiguous(offset)
3503 }
3504
3505 fn gap(
3506 earlier_offset_hours: i8,
3507 later_offset_hours: i8,
3508 ) -> AmbiguousOffset {
3509 let earlier = offset(earlier_offset_hours);
3510 let later = offset(later_offset_hours);
3511 o_gap(earlier, later)
3512 }
3513
3514 fn fold(
3515 earlier_offset_hours: i8,
3516 later_offset_hours: i8,
3517 ) -> AmbiguousOffset {
3518 let earlier = offset(earlier_offset_hours);
3519 let later = offset(later_offset_hours);
3520 o_fold(earlier, later)
3521 }
3522
3523 fn o_unambiguous(offset: Offset) -> AmbiguousOffset {
3524 AmbiguousOffset::Unambiguous { offset }
3525 }
3526
3527 fn o_gap(earlier: Offset, later: Offset) -> AmbiguousOffset {
3528 AmbiguousOffset::Gap { before: earlier, after: later }
3529 }
3530
3531 fn o_fold(earlier: Offset, later: Offset) -> AmbiguousOffset {
3532 AmbiguousOffset::Fold { before: earlier, after: later }
3533 }
3534
3535 #[cfg(feature = "alloc")]
3536 #[test]
3537 fn time_zone_tzif_to_ambiguous_timestamp() {
3538 let tests: &[(&str, &[_])] = &[
3539 (
3540 "America/New_York",
3541 &[
3542 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
3543 ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
3544 ((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
3545 ((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
3546 ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
3547 ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
3548 ((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
3549 ((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
3550 ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
3551 ],
3552 ),
3553 (
3554 "Europe/Dublin",
3555 &[
3556 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(1)),
3557 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
3558 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
3559 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
3560 ((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
3561 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
3562 ((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
3563 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
3564 ((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
3565 ],
3566 ),
3567 (
3568 "Australia/Tasmania",
3569 &[
3570 ((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
3571 ((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
3572 ((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
3573 ((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
3574 ((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
3575 ((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
3576 ((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
3577 ((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
3578 ((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
3579 ],
3580 ),
3581 (
3582 "Antarctica/Troll",
3583 &[
3584 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
3585 // test the gap
3586 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
3587 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
3588 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
3589 // still in the gap!
3590 ((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
3591 ((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
3592 // finally out
3593 ((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
3594 // test the fold
3595 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
3596 ((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
3597 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
3598 // still in the fold!
3599 ((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
3600 ((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
3601 // finally out
3602 ((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
3603 ],
3604 ),
3605 (
3606 "America/St_Johns",
3607 &[
3608 (
3609 (1969, 12, 31, 20, 30, 0, 0),
3610 o_unambiguous(-Offset::hms(3, 30, 0)),
3611 ),
3612 (
3613 (2024, 3, 10, 1, 59, 59, 999_999_999),
3614 o_unambiguous(-Offset::hms(3, 30, 0)),
3615 ),
3616 (
3617 (2024, 3, 10, 2, 0, 0, 0),
3618 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
3619 ),
3620 (
3621 (2024, 3, 10, 2, 59, 59, 999_999_999),
3622 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
3623 ),
3624 (
3625 (2024, 3, 10, 3, 0, 0, 0),
3626 o_unambiguous(-Offset::hms(2, 30, 0)),
3627 ),
3628 (
3629 (2024, 11, 3, 0, 59, 59, 999_999_999),
3630 o_unambiguous(-Offset::hms(2, 30, 0)),
3631 ),
3632 (
3633 (2024, 11, 3, 1, 0, 0, 0),
3634 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
3635 ),
3636 (
3637 (2024, 11, 3, 1, 59, 59, 999_999_999),
3638 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
3639 ),
3640 (
3641 (2024, 11, 3, 2, 0, 0, 0),
3642 o_unambiguous(-Offset::hms(3, 30, 0)),
3643 ),
3644 ],
3645 ),
3646 // This time zone has an interesting transition where it jumps
3647 // backwards a full day at 1867-10-19T15:30:00.
3648 (
3649 "America/Sitka",
3650 &[
3651 ((1969, 12, 31, 16, 0, 0, 0), unambiguous(-8)),
3652 (
3653 (-9999, 1, 2, 16, 58, 46, 0),
3654 o_unambiguous(Offset::hms(14, 58, 47)),
3655 ),
3656 (
3657 (1867, 10, 18, 15, 29, 59, 0),
3658 o_unambiguous(Offset::hms(14, 58, 47)),
3659 ),
3660 (
3661 (1867, 10, 18, 15, 30, 0, 0),
3662 // A fold of 24 hours!!!
3663 o_fold(
3664 Offset::hms(14, 58, 47),
3665 -Offset::hms(9, 1, 13),
3666 ),
3667 ),
3668 (
3669 (1867, 10, 19, 15, 29, 59, 999_999_999),
3670 // Still in the fold...
3671 o_fold(
3672 Offset::hms(14, 58, 47),
3673 -Offset::hms(9, 1, 13),
3674 ),
3675 ),
3676 (
3677 (1867, 10, 19, 15, 30, 0, 0),
3678 // Finally out.
3679 o_unambiguous(-Offset::hms(9, 1, 13)),
3680 ),
3681 ],
3682 ),
3683 // As with to_datetime, we test every possible transition
3684 // point here since this time zone has a small number of them.
3685 (
3686 "Pacific/Honolulu",
3687 &[
3688 (
3689 (1896, 1, 13, 11, 59, 59, 0),
3690 o_unambiguous(-Offset::hms(10, 31, 26)),
3691 ),
3692 (
3693 (1896, 1, 13, 12, 0, 0, 0),
3694 o_gap(
3695 -Offset::hms(10, 31, 26),
3696 -Offset::hms(10, 30, 0),
3697 ),
3698 ),
3699 (
3700 (1896, 1, 13, 12, 1, 25, 0),
3701 o_gap(
3702 -Offset::hms(10, 31, 26),
3703 -Offset::hms(10, 30, 0),
3704 ),
3705 ),
3706 (
3707 (1896, 1, 13, 12, 1, 26, 0),
3708 o_unambiguous(-Offset::hms(10, 30, 0)),
3709 ),
3710 (
3711 (1933, 4, 30, 1, 59, 59, 0),
3712 o_unambiguous(-Offset::hms(10, 30, 0)),
3713 ),
3714 (
3715 (1933, 4, 30, 2, 0, 0, 0),
3716 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
3717 ),
3718 (
3719 (1933, 4, 30, 2, 59, 59, 0),
3720 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
3721 ),
3722 (
3723 (1933, 4, 30, 3, 0, 0, 0),
3724 o_unambiguous(-Offset::hms(9, 30, 0)),
3725 ),
3726 (
3727 (1933, 5, 21, 10, 59, 59, 0),
3728 o_unambiguous(-Offset::hms(9, 30, 0)),
3729 ),
3730 (
3731 (1933, 5, 21, 11, 0, 0, 0),
3732 o_fold(
3733 -Offset::hms(9, 30, 0),
3734 -Offset::hms(10, 30, 0),
3735 ),
3736 ),
3737 (
3738 (1933, 5, 21, 11, 59, 59, 0),
3739 o_fold(
3740 -Offset::hms(9, 30, 0),
3741 -Offset::hms(10, 30, 0),
3742 ),
3743 ),
3744 (
3745 (1933, 5, 21, 12, 0, 0, 0),
3746 o_unambiguous(-Offset::hms(10, 30, 0)),
3747 ),
3748 (
3749 (1942, 2, 9, 1, 59, 59, 0),
3750 o_unambiguous(-Offset::hms(10, 30, 0)),
3751 ),
3752 (
3753 (1942, 2, 9, 2, 0, 0, 0),
3754 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
3755 ),
3756 (
3757 (1942, 2, 9, 2, 59, 59, 0),
3758 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
3759 ),
3760 (
3761 (1942, 2, 9, 3, 0, 0, 0),
3762 o_unambiguous(-Offset::hms(9, 30, 0)),
3763 ),
3764 (
3765 (1945, 8, 14, 13, 29, 59, 0),
3766 o_unambiguous(-Offset::hms(9, 30, 0)),
3767 ),
3768 (
3769 (1945, 8, 14, 13, 30, 0, 0),
3770 o_unambiguous(-Offset::hms(9, 30, 0)),
3771 ),
3772 (
3773 (1945, 8, 14, 13, 30, 1, 0),
3774 o_unambiguous(-Offset::hms(9, 30, 0)),
3775 ),
3776 (
3777 (1945, 9, 30, 0, 59, 59, 0),
3778 o_unambiguous(-Offset::hms(9, 30, 0)),
3779 ),
3780 (
3781 (1945, 9, 30, 1, 0, 0, 0),
3782 o_fold(
3783 -Offset::hms(9, 30, 0),
3784 -Offset::hms(10, 30, 0),
3785 ),
3786 ),
3787 (
3788 (1945, 9, 30, 1, 59, 59, 0),
3789 o_fold(
3790 -Offset::hms(9, 30, 0),
3791 -Offset::hms(10, 30, 0),
3792 ),
3793 ),
3794 (
3795 (1945, 9, 30, 2, 0, 0, 0),
3796 o_unambiguous(-Offset::hms(10, 30, 0)),
3797 ),
3798 (
3799 (1947, 6, 8, 1, 59, 59, 0),
3800 o_unambiguous(-Offset::hms(10, 30, 0)),
3801 ),
3802 (
3803 (1947, 6, 8, 2, 0, 0, 0),
3804 o_gap(-Offset::hms(10, 30, 0), -offset(10)),
3805 ),
3806 (
3807 (1947, 6, 8, 2, 29, 59, 0),
3808 o_gap(-Offset::hms(10, 30, 0), -offset(10)),
3809 ),
3810 ((1947, 6, 8, 2, 30, 0, 0), unambiguous(-10)),
3811 ],
3812 ),
3813 ];
3814 for &(tzname, datetimes_to_ambiguous) in tests {
3815 let test_file = TzifTestFile::get(tzname);
3816 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
3817 for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
3818 let (year, month, day, hour, min, sec, nano) = datetime;
3819 let dt = date(year, month, day).at(hour, min, sec, nano);
3820 let got = tz.to_ambiguous_zoned(dt);
3821 assert_eq!(
3822 got.offset(),
3823 ambiguous_kind,
3824 "\nTZ: {tzname}\ndatetime: \
3825 {year:04}-{month:02}-{day:02}T\
3826 {hour:02}:{min:02}:{sec:02}.{nano:09}",
3827 );
3828 }
3829 }
3830 }
3831
3832 #[cfg(feature = "alloc")]
3833 #[test]
3834 fn time_zone_tzif_to_datetime() {
3835 let o = |hours| offset(hours);
3836 let tests: &[(&str, &[_])] = &[
3837 (
3838 "America/New_York",
3839 &[
3840 ((0, 0), o(-5), "EST", (1969, 12, 31, 19, 0, 0, 0)),
3841 (
3842 (1710052200, 0),
3843 o(-5),
3844 "EST",
3845 (2024, 3, 10, 1, 30, 0, 0),
3846 ),
3847 (
3848 (1710053999, 999_999_999),
3849 o(-5),
3850 "EST",
3851 (2024, 3, 10, 1, 59, 59, 999_999_999),
3852 ),
3853 ((1710054000, 0), o(-4), "EDT", (2024, 3, 10, 3, 0, 0, 0)),
3854 (
3855 (1710055800, 0),
3856 o(-4),
3857 "EDT",
3858 (2024, 3, 10, 3, 30, 0, 0),
3859 ),
3860 ((1730610000, 0), o(-4), "EDT", (2024, 11, 3, 1, 0, 0, 0)),
3861 (
3862 (1730611800, 0),
3863 o(-4),
3864 "EDT",
3865 (2024, 11, 3, 1, 30, 0, 0),
3866 ),
3867 (
3868 (1730613599, 999_999_999),
3869 o(-4),
3870 "EDT",
3871 (2024, 11, 3, 1, 59, 59, 999_999_999),
3872 ),
3873 ((1730613600, 0), o(-5), "EST", (2024, 11, 3, 1, 0, 0, 0)),
3874 (
3875 (1730615400, 0),
3876 o(-5),
3877 "EST",
3878 (2024, 11, 3, 1, 30, 0, 0),
3879 ),
3880 ],
3881 ),
3882 (
3883 "Australia/Tasmania",
3884 &[
3885 ((0, 0), o(11), "AEDT", (1970, 1, 1, 11, 0, 0, 0)),
3886 (
3887 (1728142200, 0),
3888 o(10),
3889 "AEST",
3890 (2024, 10, 6, 1, 30, 0, 0),
3891 ),
3892 (
3893 (1728143999, 999_999_999),
3894 o(10),
3895 "AEST",
3896 (2024, 10, 6, 1, 59, 59, 999_999_999),
3897 ),
3898 (
3899 (1728144000, 0),
3900 o(11),
3901 "AEDT",
3902 (2024, 10, 6, 3, 0, 0, 0),
3903 ),
3904 (
3905 (1728145800, 0),
3906 o(11),
3907 "AEDT",
3908 (2024, 10, 6, 3, 30, 0, 0),
3909 ),
3910 ((1712415600, 0), o(11), "AEDT", (2024, 4, 7, 2, 0, 0, 0)),
3911 (
3912 (1712417400, 0),
3913 o(11),
3914 "AEDT",
3915 (2024, 4, 7, 2, 30, 0, 0),
3916 ),
3917 (
3918 (1712419199, 999_999_999),
3919 o(11),
3920 "AEDT",
3921 (2024, 4, 7, 2, 59, 59, 999_999_999),
3922 ),
3923 ((1712419200, 0), o(10), "AEST", (2024, 4, 7, 2, 0, 0, 0)),
3924 (
3925 (1712421000, 0),
3926 o(10),
3927 "AEST",
3928 (2024, 4, 7, 2, 30, 0, 0),
3929 ),
3930 ],
3931 ),
3932 // Pacific/Honolulu is small eough that we just test every
3933 // possible instant before, at and after each transition.
3934 (
3935 "Pacific/Honolulu",
3936 &[
3937 (
3938 (-2334101315, 0),
3939 -Offset::hms(10, 31, 26),
3940 "LMT",
3941 (1896, 1, 13, 11, 59, 59, 0),
3942 ),
3943 (
3944 (-2334101314, 0),
3945 -Offset::hms(10, 30, 0),
3946 "HST",
3947 (1896, 1, 13, 12, 1, 26, 0),
3948 ),
3949 (
3950 (-2334101313, 0),
3951 -Offset::hms(10, 30, 0),
3952 "HST",
3953 (1896, 1, 13, 12, 1, 27, 0),
3954 ),
3955 (
3956 (-1157283001, 0),
3957 -Offset::hms(10, 30, 0),
3958 "HST",
3959 (1933, 4, 30, 1, 59, 59, 0),
3960 ),
3961 (
3962 (-1157283000, 0),
3963 -Offset::hms(9, 30, 0),
3964 "HDT",
3965 (1933, 4, 30, 3, 0, 0, 0),
3966 ),
3967 (
3968 (-1157282999, 0),
3969 -Offset::hms(9, 30, 0),
3970 "HDT",
3971 (1933, 4, 30, 3, 0, 1, 0),
3972 ),
3973 (
3974 (-1155436201, 0),
3975 -Offset::hms(9, 30, 0),
3976 "HDT",
3977 (1933, 5, 21, 11, 59, 59, 0),
3978 ),
3979 (
3980 (-1155436200, 0),
3981 -Offset::hms(10, 30, 0),
3982 "HST",
3983 (1933, 5, 21, 11, 0, 0, 0),
3984 ),
3985 (
3986 (-1155436199, 0),
3987 -Offset::hms(10, 30, 0),
3988 "HST",
3989 (1933, 5, 21, 11, 0, 1, 0),
3990 ),
3991 (
3992 (-880198201, 0),
3993 -Offset::hms(10, 30, 0),
3994 "HST",
3995 (1942, 2, 9, 1, 59, 59, 0),
3996 ),
3997 (
3998 (-880198200, 0),
3999 -Offset::hms(9, 30, 0),
4000 "HWT",
4001 (1942, 2, 9, 3, 0, 0, 0),
4002 ),
4003 (
4004 (-880198199, 0),
4005 -Offset::hms(9, 30, 0),
4006 "HWT",
4007 (1942, 2, 9, 3, 0, 1, 0),
4008 ),
4009 (
4010 (-769395601, 0),
4011 -Offset::hms(9, 30, 0),
4012 "HWT",
4013 (1945, 8, 14, 13, 29, 59, 0),
4014 ),
4015 (
4016 (-769395600, 0),
4017 -Offset::hms(9, 30, 0),
4018 "HPT",
4019 (1945, 8, 14, 13, 30, 0, 0),
4020 ),
4021 (
4022 (-769395599, 0),
4023 -Offset::hms(9, 30, 0),
4024 "HPT",
4025 (1945, 8, 14, 13, 30, 1, 0),
4026 ),
4027 (
4028 (-765376201, 0),
4029 -Offset::hms(9, 30, 0),
4030 "HPT",
4031 (1945, 9, 30, 1, 59, 59, 0),
4032 ),
4033 (
4034 (-765376200, 0),
4035 -Offset::hms(10, 30, 0),
4036 "HST",
4037 (1945, 9, 30, 1, 0, 0, 0),
4038 ),
4039 (
4040 (-765376199, 0),
4041 -Offset::hms(10, 30, 0),
4042 "HST",
4043 (1945, 9, 30, 1, 0, 1, 0),
4044 ),
4045 (
4046 (-712150201, 0),
4047 -Offset::hms(10, 30, 0),
4048 "HST",
4049 (1947, 6, 8, 1, 59, 59, 0),
4050 ),
4051 // At this point, we hit the last transition and the POSIX
4052 // TZ string takes over.
4053 (
4054 (-712150200, 0),
4055 -Offset::hms(10, 0, 0),
4056 "HST",
4057 (1947, 6, 8, 2, 30, 0, 0),
4058 ),
4059 (
4060 (-712150199, 0),
4061 -Offset::hms(10, 0, 0),
4062 "HST",
4063 (1947, 6, 8, 2, 30, 1, 0),
4064 ),
4065 ],
4066 ),
4067 // This time zone has an interesting transition where it jumps
4068 // backwards a full day at 1867-10-19T15:30:00.
4069 (
4070 "America/Sitka",
4071 &[
4072 ((0, 0), o(-8), "PST", (1969, 12, 31, 16, 0, 0, 0)),
4073 (
4074 (-377705023201, 0),
4075 Offset::hms(14, 58, 47),
4076 "LMT",
4077 (-9999, 1, 2, 16, 58, 46, 0),
4078 ),
4079 (
4080 (-3225223728, 0),
4081 Offset::hms(14, 58, 47),
4082 "LMT",
4083 (1867, 10, 19, 15, 29, 59, 0),
4084 ),
4085 // Notice the 24 hour time jump backwards a whole day!
4086 (
4087 (-3225223727, 0),
4088 -Offset::hms(9, 1, 13),
4089 "LMT",
4090 (1867, 10, 18, 15, 30, 0, 0),
4091 ),
4092 (
4093 (-3225223726, 0),
4094 -Offset::hms(9, 1, 13),
4095 "LMT",
4096 (1867, 10, 18, 15, 30, 1, 0),
4097 ),
4098 ],
4099 ),
4100 ];
4101 for &(tzname, timestamps_to_datetimes) in tests {
4102 let test_file = TzifTestFile::get(tzname);
4103 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
4104 for &((unix_sec, unix_nano), offset, abbrev, datetime) in
4105 timestamps_to_datetimes
4106 {
4107 let (year, month, day, hour, min, sec, nano) = datetime;
4108 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
4109 let info = tz.to_offset_info(timestamp);
4110 assert_eq!(
4111 info.offset(),
4112 offset,
4113 "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
4114 );
4115 assert_eq!(
4116 info.abbreviation(),
4117 abbrev,
4118 "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
4119 );
4120 assert_eq!(
4121 info.offset().to_datetime(timestamp),
4122 date(year, month, day).at(hour, min, sec, nano),
4123 "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
4124 );
4125 }
4126 }
4127 }
4128
4129 #[cfg(feature = "alloc")]
4130 #[test]
4131 fn time_zone_posix_to_ambiguous_timestamp() {
4132 let tests: &[(&str, &[_])] = &[
4133 // America/New_York, but a utopia in which DST is abolished.
4134 (
4135 "EST5",
4136 &[
4137 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
4138 ((2024, 3, 10, 2, 0, 0, 0), unambiguous(-5)),
4139 ],
4140 ),
4141 // The standard DST rule for America/New_York.
4142 (
4143 "EST5EDT,M3.2.0,M11.1.0",
4144 &[
4145 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
4146 ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
4147 ((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
4148 ((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
4149 ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
4150 ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
4151 ((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
4152 ((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
4153 ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
4154 ],
4155 ),
4156 // A bit of a nonsensical America/New_York that has DST, but whose
4157 // offset is equivalent to standard time. Having the same offset
4158 // means there's never any ambiguity.
4159 (
4160 "EST5EDT5,M3.2.0,M11.1.0",
4161 &[
4162 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
4163 ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
4164 ((2024, 3, 10, 2, 0, 0, 0), unambiguous(-5)),
4165 ((2024, 3, 10, 2, 59, 59, 999_999_999), unambiguous(-5)),
4166 ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-5)),
4167 ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-5)),
4168 ((2024, 11, 3, 1, 0, 0, 0), unambiguous(-5)),
4169 ((2024, 11, 3, 1, 59, 59, 999_999_999), unambiguous(-5)),
4170 ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
4171 ],
4172 ),
4173 // This is Europe/Dublin's rule. It's interesting because its
4174 // DST is an offset behind standard time. (DST is usually one hour
4175 // ahead of standard time.)
4176 (
4177 "IST-1GMT0,M10.5.0,M3.5.0/1",
4178 &[
4179 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
4180 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
4181 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
4182 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
4183 ((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
4184 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
4185 ((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
4186 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
4187 ((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
4188 ],
4189 ),
4190 // This is Australia/Tasmania's rule. We chose this because it's
4191 // in the southern hemisphere where DST still skips ahead one hour,
4192 // but it usually starts in the fall and ends in the spring.
4193 (
4194 "AEST-10AEDT,M10.1.0,M4.1.0/3",
4195 &[
4196 ((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
4197 ((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
4198 ((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
4199 ((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
4200 ((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
4201 ((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
4202 ((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
4203 ((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
4204 ((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
4205 ],
4206 ),
4207 // This is Antarctica/Troll's rule. We chose this one because its
4208 // DST transition is 2 hours instead of the standard 1 hour. This
4209 // means gaps and folds are twice as long as they usually are. And
4210 // it means there are 22 hour and 26 hour days, respectively. Wow!
4211 (
4212 "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
4213 &[
4214 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
4215 // test the gap
4216 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
4217 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
4218 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
4219 // still in the gap!
4220 ((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
4221 ((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
4222 // finally out
4223 ((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
4224 // test the fold
4225 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
4226 ((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
4227 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
4228 // still in the fold!
4229 ((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
4230 ((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
4231 // finally out
4232 ((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
4233 ],
4234 ),
4235 // This is America/St_Johns' rule, which has an offset with
4236 // non-zero minutes *and* a DST transition rule. (Indian Standard
4237 // Time is the one I'm more familiar with, but it turns out IST
4238 // does not have DST!)
4239 (
4240 "NST3:30NDT,M3.2.0,M11.1.0",
4241 &[
4242 (
4243 (1969, 12, 31, 20, 30, 0, 0),
4244 o_unambiguous(-Offset::hms(3, 30, 0)),
4245 ),
4246 (
4247 (2024, 3, 10, 1, 59, 59, 999_999_999),
4248 o_unambiguous(-Offset::hms(3, 30, 0)),
4249 ),
4250 (
4251 (2024, 3, 10, 2, 0, 0, 0),
4252 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
4253 ),
4254 (
4255 (2024, 3, 10, 2, 59, 59, 999_999_999),
4256 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
4257 ),
4258 (
4259 (2024, 3, 10, 3, 0, 0, 0),
4260 o_unambiguous(-Offset::hms(2, 30, 0)),
4261 ),
4262 (
4263 (2024, 11, 3, 0, 59, 59, 999_999_999),
4264 o_unambiguous(-Offset::hms(2, 30, 0)),
4265 ),
4266 (
4267 (2024, 11, 3, 1, 0, 0, 0),
4268 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
4269 ),
4270 (
4271 (2024, 11, 3, 1, 59, 59, 999_999_999),
4272 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
4273 ),
4274 (
4275 (2024, 11, 3, 2, 0, 0, 0),
4276 o_unambiguous(-Offset::hms(3, 30, 0)),
4277 ),
4278 ],
4279 ),
4280 ];
4281 for &(posix_tz, datetimes_to_ambiguous) in tests {
4282 let tz = TimeZone::posix(posix_tz).unwrap();
4283 for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
4284 let (year, month, day, hour, min, sec, nano) = datetime;
4285 let dt = date(year, month, day).at(hour, min, sec, nano);
4286 let got = tz.to_ambiguous_zoned(dt);
4287 assert_eq!(
4288 got.offset(),
4289 ambiguous_kind,
4290 "\nTZ: {posix_tz}\ndatetime: \
4291 {year:04}-{month:02}-{day:02}T\
4292 {hour:02}:{min:02}:{sec:02}.{nano:09}",
4293 );
4294 }
4295 }
4296 }
4297
4298 #[cfg(feature = "alloc")]
4299 #[test]
4300 fn time_zone_posix_to_datetime() {
4301 let o = |hours| offset(hours);
4302 let tests: &[(&str, &[_])] = &[
4303 ("EST5", &[((0, 0), o(-5), (1969, 12, 31, 19, 0, 0, 0))]),
4304 (
4305 // From America/New_York
4306 "EST5EDT,M3.2.0,M11.1.0",
4307 &[
4308 ((0, 0), o(-5), (1969, 12, 31, 19, 0, 0, 0)),
4309 ((1710052200, 0), o(-5), (2024, 3, 10, 1, 30, 0, 0)),
4310 (
4311 (1710053999, 999_999_999),
4312 o(-5),
4313 (2024, 3, 10, 1, 59, 59, 999_999_999),
4314 ),
4315 ((1710054000, 0), o(-4), (2024, 3, 10, 3, 0, 0, 0)),
4316 ((1710055800, 0), o(-4), (2024, 3, 10, 3, 30, 0, 0)),
4317 ((1730610000, 0), o(-4), (2024, 11, 3, 1, 0, 0, 0)),
4318 ((1730611800, 0), o(-4), (2024, 11, 3, 1, 30, 0, 0)),
4319 (
4320 (1730613599, 999_999_999),
4321 o(-4),
4322 (2024, 11, 3, 1, 59, 59, 999_999_999),
4323 ),
4324 ((1730613600, 0), o(-5), (2024, 11, 3, 1, 0, 0, 0)),
4325 ((1730615400, 0), o(-5), (2024, 11, 3, 1, 30, 0, 0)),
4326 ],
4327 ),
4328 (
4329 // From Australia/Tasmania
4330 //
4331 // We chose this because it's a time zone in the southern
4332 // hemisphere with DST. Unlike the northern hemisphere, its DST
4333 // starts in the fall and ends in the spring. In the northern
4334 // hemisphere, we typically start DST in the spring and end it
4335 // in the fall.
4336 "AEST-10AEDT,M10.1.0,M4.1.0/3",
4337 &[
4338 ((0, 0), o(11), (1970, 1, 1, 11, 0, 0, 0)),
4339 ((1728142200, 0), o(10), (2024, 10, 6, 1, 30, 0, 0)),
4340 (
4341 (1728143999, 999_999_999),
4342 o(10),
4343 (2024, 10, 6, 1, 59, 59, 999_999_999),
4344 ),
4345 ((1728144000, 0), o(11), (2024, 10, 6, 3, 0, 0, 0)),
4346 ((1728145800, 0), o(11), (2024, 10, 6, 3, 30, 0, 0)),
4347 ((1712415600, 0), o(11), (2024, 4, 7, 2, 0, 0, 0)),
4348 ((1712417400, 0), o(11), (2024, 4, 7, 2, 30, 0, 0)),
4349 (
4350 (1712419199, 999_999_999),
4351 o(11),
4352 (2024, 4, 7, 2, 59, 59, 999_999_999),
4353 ),
4354 ((1712419200, 0), o(10), (2024, 4, 7, 2, 0, 0, 0)),
4355 ((1712421000, 0), o(10), (2024, 4, 7, 2, 30, 0, 0)),
4356 ],
4357 ),
4358 (
4359 // Uses the maximum possible offset. A sloppy read of POSIX
4360 // seems to indicate the maximum offset is 24:59:59, but since
4361 // DST defaults to 1 hour ahead of standard time, it's possible
4362 // to use 24:59:59 for standard time, omit the DST offset, and
4363 // thus get a DST offset of 25:59:59.
4364 "XXX-24:59:59YYY,M3.2.0,M11.1.0",
4365 &[
4366 // 2024-01-05T00:00:00+00
4367 (
4368 (1704412800, 0),
4369 Offset::hms(24, 59, 59),
4370 (2024, 1, 6, 0, 59, 59, 0),
4371 ),
4372 // 2024-06-05T00:00:00+00 (DST)
4373 (
4374 (1717545600, 0),
4375 Offset::hms(25, 59, 59),
4376 (2024, 6, 6, 1, 59, 59, 0),
4377 ),
4378 ],
4379 ),
4380 ];
4381 for &(posix_tz, timestamps_to_datetimes) in tests {
4382 let tz = TimeZone::posix(posix_tz).unwrap();
4383 for &((unix_sec, unix_nano), offset, datetime) in
4384 timestamps_to_datetimes
4385 {
4386 let (year, month, day, hour, min, sec, nano) = datetime;
4387 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
4388 assert_eq!(
4389 tz.to_offset(timestamp),
4390 offset,
4391 "\ntimestamp({unix_sec}, {unix_nano})",
4392 );
4393 assert_eq!(
4394 tz.to_datetime(timestamp),
4395 date(year, month, day).at(hour, min, sec, nano),
4396 "\ntimestamp({unix_sec}, {unix_nano})",
4397 );
4398 }
4399 }
4400 }
4401
4402 #[test]
4403 fn time_zone_fixed_to_datetime() {
4404 let tz = offset(-5).to_time_zone();
4405 let unix_epoch = Timestamp::new(0, 0).unwrap();
4406 assert_eq!(
4407 tz.to_datetime(unix_epoch),
4408 date(1969, 12, 31).at(19, 0, 0, 0),
4409 );
4410
4411 let tz = Offset::from_seconds(93_599).unwrap().to_time_zone();
4412 let timestamp = Timestamp::new(253402207200, 999_999_999).unwrap();
4413 assert_eq!(
4414 tz.to_datetime(timestamp),
4415 date(9999, 12, 31).at(23, 59, 59, 999_999_999),
4416 );
4417
4418 let tz = Offset::from_seconds(-93_599).unwrap().to_time_zone();
4419 let timestamp = Timestamp::new(-377705023201, 0).unwrap();
4420 assert_eq!(
4421 tz.to_datetime(timestamp),
4422 date(-9999, 1, 1).at(0, 0, 0, 0),
4423 );
4424 }
4425
4426 #[test]
4427 fn time_zone_fixed_to_timestamp() {
4428 let tz = offset(-5).to_time_zone();
4429 let dt = date(1969, 12, 31).at(19, 0, 0, 0);
4430 assert_eq!(
4431 tz.to_zoned(dt).unwrap().timestamp(),
4432 Timestamp::new(0, 0).unwrap()
4433 );
4434
4435 let tz = Offset::from_seconds(93_599).unwrap().to_time_zone();
4436 let dt = date(9999, 12, 31).at(23, 59, 59, 999_999_999);
4437 assert_eq!(
4438 tz.to_zoned(dt).unwrap().timestamp(),
4439 Timestamp::new(253402207200, 999_999_999).unwrap(),
4440 );
4441 let tz = Offset::from_seconds(93_598).unwrap().to_time_zone();
4442 assert!(tz.to_zoned(dt).is_err());
4443
4444 let tz = Offset::from_seconds(-93_599).unwrap().to_time_zone();
4445 let dt = date(-9999, 1, 1).at(0, 0, 0, 0);
4446 assert_eq!(
4447 tz.to_zoned(dt).unwrap().timestamp(),
4448 Timestamp::new(-377705023201, 0).unwrap(),
4449 );
4450 let tz = Offset::from_seconds(-93_598).unwrap().to_time_zone();
4451 assert!(tz.to_zoned(dt).is_err());
4452 }
4453
4454 #[cfg(feature = "alloc")]
4455 #[test]
4456 fn time_zone_tzif_previous_transition() {
4457 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
4458 (
4459 "UTC",
4460 &[
4461 ("1969-12-31T19Z", None),
4462 ("2024-03-10T02Z", None),
4463 ("-009999-12-01 00Z", None),
4464 ("9999-12-01 00Z", None),
4465 ],
4466 ),
4467 (
4468 "America/New_York",
4469 &[
4470 ("2024-03-10 08Z", Some("2024-03-10 07Z")),
4471 ("2024-03-10 07:00:00.000000001Z", Some("2024-03-10 07Z")),
4472 ("2024-03-10 07Z", Some("2023-11-05 06Z")),
4473 ("2023-11-05 06Z", Some("2023-03-12 07Z")),
4474 ("-009999-01-31 00Z", None),
4475 ("9999-12-01 00Z", Some("9999-11-07 06Z")),
4476 // While at present we have "fat" TZif files for our
4477 // testdata, it's conceivable they could be swapped to
4478 // "slim." In which case, the tests above will mostly just
4479 // be testing POSIX TZ strings and not the TZif logic. So
4480 // below, we include times that will be in slim (i.e.,
4481 // historical times the precede the current DST rule).
4482 ("1969-12-31 19Z", Some("1969-10-26 06Z")),
4483 ("2000-04-02 08Z", Some("2000-04-02 07Z")),
4484 ("2000-04-02 07:00:00.000000001Z", Some("2000-04-02 07Z")),
4485 ("2000-04-02 07Z", Some("1999-10-31 06Z")),
4486 ("1999-10-31 06Z", Some("1999-04-04 07Z")),
4487 ],
4488 ),
4489 (
4490 "Australia/Tasmania",
4491 &[
4492 ("2010-04-03 17Z", Some("2010-04-03 16Z")),
4493 ("2010-04-03 16:00:00.000000001Z", Some("2010-04-03 16Z")),
4494 ("2010-04-03 16Z", Some("2009-10-03 16Z")),
4495 ("2009-10-03 16Z", Some("2009-04-04 16Z")),
4496 ("-009999-01-31 00Z", None),
4497 ("9999-12-01 00Z", Some("9999-10-02 16Z")),
4498 // Tests for historical data from tzdb. No POSIX TZ.
4499 ("2000-03-25 17Z", Some("2000-03-25 16Z")),
4500 ("2000-03-25 16:00:00.000000001Z", Some("2000-03-25 16Z")),
4501 ("2000-03-25 16Z", Some("1999-10-02 16Z")),
4502 ("1999-10-02 16Z", Some("1999-03-27 16Z")),
4503 ],
4504 ),
4505 // This is Europe/Dublin's rule. It's interesting because its
4506 // DST is an offset behind standard time. (DST is usually one hour
4507 // ahead of standard time.)
4508 (
4509 "Europe/Dublin",
4510 &[
4511 ("2010-03-28 02Z", Some("2010-03-28 01Z")),
4512 ("2010-03-28 01:00:00.000000001Z", Some("2010-03-28 01Z")),
4513 ("2010-03-28 01Z", Some("2009-10-25 01Z")),
4514 ("2009-10-25 01Z", Some("2009-03-29 01Z")),
4515 ("-009999-01-31 00Z", None),
4516 ("9999-12-01 00Z", Some("9999-10-31 01Z")),
4517 // Tests for historical data from tzdb. No POSIX TZ.
4518 ("1990-03-25 02Z", Some("1990-03-25 01Z")),
4519 ("1990-03-25 01:00:00.000000001Z", Some("1990-03-25 01Z")),
4520 ("1990-03-25 01Z", Some("1989-10-29 01Z")),
4521 ("1989-10-25 01Z", Some("1989-03-26 01Z")),
4522 ],
4523 ),
4524 ];
4525 for &(tzname, prev_trans) in tests {
4526 let test_file = TzifTestFile::get(tzname);
4527 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
4528 for (given, expected) in prev_trans {
4529 let given: Timestamp = given.parse().unwrap();
4530 let expected =
4531 expected.map(|s| s.parse::<Timestamp>().unwrap());
4532 let got = tz.previous_transition(given).map(|t| t.timestamp());
4533 assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
4534 }
4535 }
4536 }
4537
4538 #[cfg(feature = "alloc")]
4539 #[test]
4540 fn time_zone_tzif_next_transition() {
4541 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
4542 (
4543 "UTC",
4544 &[
4545 ("1969-12-31T19Z", None),
4546 ("2024-03-10T02Z", None),
4547 ("-009999-12-01 00Z", None),
4548 ("9999-12-01 00Z", None),
4549 ],
4550 ),
4551 (
4552 "America/New_York",
4553 &[
4554 ("2024-03-10 06Z", Some("2024-03-10 07Z")),
4555 ("2024-03-10 06:59:59.999999999Z", Some("2024-03-10 07Z")),
4556 ("2024-03-10 07Z", Some("2024-11-03 06Z")),
4557 ("2024-11-03 06Z", Some("2025-03-09 07Z")),
4558 ("-009999-12-01 00Z", Some("1883-11-18 17Z")),
4559 ("9999-12-01 00Z", None),
4560 // While at present we have "fat" TZif files for our
4561 // testdata, it's conceivable they could be swapped to
4562 // "slim." In which case, the tests above will mostly just
4563 // be testing POSIX TZ strings and not the TZif logic. So
4564 // below, we include times that will be in slim (i.e.,
4565 // historical times the precede the current DST rule).
4566 ("1969-12-31 19Z", Some("1970-04-26 07Z")),
4567 ("2000-04-02 06Z", Some("2000-04-02 07Z")),
4568 ("2000-04-02 06:59:59.999999999Z", Some("2000-04-02 07Z")),
4569 ("2000-04-02 07Z", Some("2000-10-29 06Z")),
4570 ("2000-10-29 06Z", Some("2001-04-01 07Z")),
4571 ],
4572 ),
4573 (
4574 "Australia/Tasmania",
4575 &[
4576 ("2010-04-03 15Z", Some("2010-04-03 16Z")),
4577 ("2010-04-03 15:59:59.999999999Z", Some("2010-04-03 16Z")),
4578 ("2010-04-03 16Z", Some("2010-10-02 16Z")),
4579 ("2010-10-02 16Z", Some("2011-04-02 16Z")),
4580 ("-009999-12-01 00Z", Some("1895-08-31 14:10:44Z")),
4581 ("9999-12-01 00Z", None),
4582 // Tests for historical data from tzdb. No POSIX TZ.
4583 ("2000-03-25 15Z", Some("2000-03-25 16Z")),
4584 ("2000-03-25 15:59:59.999999999Z", Some("2000-03-25 16Z")),
4585 ("2000-03-25 16Z", Some("2000-08-26 16Z")),
4586 ("2000-08-26 16Z", Some("2001-03-24 16Z")),
4587 ],
4588 ),
4589 (
4590 "Europe/Dublin",
4591 &[
4592 ("2010-03-28 00Z", Some("2010-03-28 01Z")),
4593 ("2010-03-28 00:59:59.999999999Z", Some("2010-03-28 01Z")),
4594 ("2010-03-28 01Z", Some("2010-10-31 01Z")),
4595 ("2010-10-31 01Z", Some("2011-03-27 01Z")),
4596 ("-009999-12-01 00Z", Some("1880-08-02 00:25:21Z")),
4597 ("9999-12-01 00Z", None),
4598 // Tests for historical data from tzdb. No POSIX TZ.
4599 ("1990-03-25 00Z", Some("1990-03-25 01Z")),
4600 ("1990-03-25 00:59:59.999999999Z", Some("1990-03-25 01Z")),
4601 ("1990-03-25 01Z", Some("1990-10-28 01Z")),
4602 ("1990-10-28 01Z", Some("1991-03-31 01Z")),
4603 ],
4604 ),
4605 ];
4606 for &(tzname, next_trans) in tests {
4607 let test_file = TzifTestFile::get(tzname);
4608 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
4609 for (given, expected) in next_trans {
4610 let given: Timestamp = given.parse().unwrap();
4611 let expected =
4612 expected.map(|s| s.parse::<Timestamp>().unwrap());
4613 let got = tz.next_transition(given).map(|t| t.timestamp());
4614 assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
4615 }
4616 }
4617 }
4618
4619 #[cfg(feature = "alloc")]
4620 #[test]
4621 fn time_zone_posix_previous_transition() {
4622 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
4623 // America/New_York, but a utopia in which DST is abolished. There
4624 // are no time zone transitions, so next_transition always returns
4625 // None.
4626 (
4627 "EST5",
4628 &[
4629 ("1969-12-31T19Z", None),
4630 ("2024-03-10T02Z", None),
4631 ("-009999-12-01 00Z", None),
4632 ("9999-12-01 00Z", None),
4633 ],
4634 ),
4635 // The standard DST rule for America/New_York.
4636 (
4637 "EST5EDT,M3.2.0,M11.1.0",
4638 &[
4639 ("1969-12-31 19Z", Some("1969-11-02 06Z")),
4640 ("2024-03-10 08Z", Some("2024-03-10 07Z")),
4641 ("2024-03-10 07:00:00.000000001Z", Some("2024-03-10 07Z")),
4642 ("2024-03-10 07Z", Some("2023-11-05 06Z")),
4643 ("2023-11-05 06Z", Some("2023-03-12 07Z")),
4644 ("-009999-01-31 00Z", None),
4645 ("9999-12-01 00Z", Some("9999-11-07 06Z")),
4646 ],
4647 ),
4648 (
4649 // From Australia/Tasmania
4650 "AEST-10AEDT,M10.1.0,M4.1.0/3",
4651 &[
4652 ("2010-04-03 17Z", Some("2010-04-03 16Z")),
4653 ("2010-04-03 16:00:00.000000001Z", Some("2010-04-03 16Z")),
4654 ("2010-04-03 16Z", Some("2009-10-03 16Z")),
4655 ("2009-10-03 16Z", Some("2009-04-04 16Z")),
4656 ("-009999-01-31 00Z", None),
4657 ("9999-12-01 00Z", Some("9999-10-02 16Z")),
4658 ],
4659 ),
4660 // This is Europe/Dublin's rule. It's interesting because its
4661 // DST is an offset behind standard time. (DST is usually one hour
4662 // ahead of standard time.)
4663 (
4664 "IST-1GMT0,M10.5.0,M3.5.0/1",
4665 &[
4666 ("2010-03-28 02Z", Some("2010-03-28 01Z")),
4667 ("2010-03-28 01:00:00.000000001Z", Some("2010-03-28 01Z")),
4668 ("2010-03-28 01Z", Some("2009-10-25 01Z")),
4669 ("2009-10-25 01Z", Some("2009-03-29 01Z")),
4670 ("-009999-01-31 00Z", None),
4671 ("9999-12-01 00Z", Some("9999-10-31 01Z")),
4672 ],
4673 ),
4674 ];
4675 for &(posix_tz, prev_trans) in tests {
4676 let tz = TimeZone::posix(posix_tz).unwrap();
4677 for (given, expected) in prev_trans {
4678 let given: Timestamp = given.parse().unwrap();
4679 let expected =
4680 expected.map(|s| s.parse::<Timestamp>().unwrap());
4681 let got = tz.previous_transition(given).map(|t| t.timestamp());
4682 assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
4683 }
4684 }
4685 }
4686
4687 #[cfg(feature = "alloc")]
4688 #[test]
4689 fn time_zone_posix_next_transition() {
4690 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
4691 // America/New_York, but a utopia in which DST is abolished. There
4692 // are no time zone transitions, so next_transition always returns
4693 // None.
4694 (
4695 "EST5",
4696 &[
4697 ("1969-12-31T19Z", None),
4698 ("2024-03-10T02Z", None),
4699 ("-009999-12-01 00Z", None),
4700 ("9999-12-01 00Z", None),
4701 ],
4702 ),
4703 // The standard DST rule for America/New_York.
4704 (
4705 "EST5EDT,M3.2.0,M11.1.0",
4706 &[
4707 ("1969-12-31 19Z", Some("1970-03-08 07Z")),
4708 ("2024-03-10 06Z", Some("2024-03-10 07Z")),
4709 ("2024-03-10 06:59:59.999999999Z", Some("2024-03-10 07Z")),
4710 ("2024-03-10 07Z", Some("2024-11-03 06Z")),
4711 ("2024-11-03 06Z", Some("2025-03-09 07Z")),
4712 ("-009999-12-01 00Z", Some("-009998-03-10 07Z")),
4713 ("9999-12-01 00Z", None),
4714 ],
4715 ),
4716 (
4717 // From Australia/Tasmania
4718 "AEST-10AEDT,M10.1.0,M4.1.0/3",
4719 &[
4720 ("2010-04-03 15Z", Some("2010-04-03 16Z")),
4721 ("2010-04-03 15:59:59.999999999Z", Some("2010-04-03 16Z")),
4722 ("2010-04-03 16Z", Some("2010-10-02 16Z")),
4723 ("2010-10-02 16Z", Some("2011-04-02 16Z")),
4724 ("-009999-12-01 00Z", Some("-009998-04-06 16Z")),
4725 ("9999-12-01 00Z", None),
4726 ],
4727 ),
4728 // This is Europe/Dublin's rule. It's interesting because its
4729 // DST is an offset behind standard time. (DST is usually one hour
4730 // ahead of standard time.)
4731 (
4732 "IST-1GMT0,M10.5.0,M3.5.0/1",
4733 &[
4734 ("2010-03-28 00Z", Some("2010-03-28 01Z")),
4735 ("2010-03-28 00:59:59.999999999Z", Some("2010-03-28 01Z")),
4736 ("2010-03-28 01Z", Some("2010-10-31 01Z")),
4737 ("2010-10-31 01Z", Some("2011-03-27 01Z")),
4738 ("-009999-12-01 00Z", Some("-009998-03-31 01Z")),
4739 ("9999-12-01 00Z", None),
4740 ],
4741 ),
4742 ];
4743 for &(posix_tz, next_trans) in tests {
4744 let tz = TimeZone::posix(posix_tz).unwrap();
4745 for (given, expected) in next_trans {
4746 let given: Timestamp = given.parse().unwrap();
4747 let expected =
4748 expected.map(|s| s.parse::<Timestamp>().unwrap());
4749 let got = tz.next_transition(given).map(|t| t.timestamp());
4750 assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
4751 }
4752 }
4753 }
4754
4755 /// This tests that the size of a time zone is kept at a single word.
4756 ///
4757 /// This is important because every jiff::Zoned has a TimeZone inside of
4758 /// it, and we want to keep its size as small as we can.
4759 #[test]
4760 fn time_zone_size() {
4761 #[cfg(feature = "alloc")]
4762 {
4763 let word = core::mem::size_of::<usize>();
4764 assert_eq!(word, core::mem::size_of::<TimeZone>());
4765 }
4766 #[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
4767 {
4768 #[cfg(debug_assertions)]
4769 {
4770 assert_eq!(16, core::mem::size_of::<TimeZone>());
4771 }
4772 #[cfg(not(debug_assertions))]
4773 {
4774 // This asserts the same value as the alloc value above, but
4775 // it wasn't always this way, which is why it's written out
4776 // separately. Moreover, in theory, I'd be open to regressing
4777 // this value if it led to an improvement in alloc-mode. But
4778 // more likely, it would be nice to decrease this size in
4779 // non-alloc modes.
4780 assert_eq!(8, core::mem::size_of::<TimeZone>());
4781 }
4782 }
4783 }
4784
4785 /// This tests a few other cases for `TimeZone::to_offset` that
4786 /// probably aren't worth showing in doctest examples.
4787 #[test]
4788 fn time_zone_to_offset() {
4789 let ts = Timestamp::from_second(123456789).unwrap();
4790
4791 let tz = TimeZone::fixed(offset(-5));
4792 let info = tz.to_offset_info(ts);
4793 assert_eq!(info.offset(), offset(-5));
4794 assert_eq!(info.dst(), Dst::No);
4795 assert_eq!(info.abbreviation(), "-05");
4796
4797 let tz = TimeZone::fixed(offset(5));
4798 let info = tz.to_offset_info(ts);
4799 assert_eq!(info.offset(), offset(5));
4800 assert_eq!(info.dst(), Dst::No);
4801 assert_eq!(info.abbreviation(), "+05");
4802
4803 let tz = TimeZone::fixed(offset(-12));
4804 let info = tz.to_offset_info(ts);
4805 assert_eq!(info.offset(), offset(-12));
4806 assert_eq!(info.dst(), Dst::No);
4807 assert_eq!(info.abbreviation(), "-12");
4808
4809 let tz = TimeZone::fixed(offset(12));
4810 let info = tz.to_offset_info(ts);
4811 assert_eq!(info.offset(), offset(12));
4812 assert_eq!(info.dst(), Dst::No);
4813 assert_eq!(info.abbreviation(), "+12");
4814
4815 let tz = TimeZone::fixed(offset(0));
4816 let info = tz.to_offset_info(ts);
4817 assert_eq!(info.offset(), offset(0));
4818 assert_eq!(info.dst(), Dst::No);
4819 assert_eq!(info.abbreviation(), "UTC");
4820 }
4821
4822 /// This tests a few other cases for `TimeZone::to_fixed_offset` that
4823 /// probably aren't worth showing in doctest examples.
4824 #[test]
4825 fn time_zone_to_fixed_offset() {
4826 let tz = TimeZone::UTC;
4827 assert_eq!(tz.to_fixed_offset().unwrap(), Offset::UTC);
4828
4829 let offset = Offset::from_hours(1).unwrap();
4830 let tz = TimeZone::fixed(offset);
4831 assert_eq!(tz.to_fixed_offset().unwrap(), offset);
4832
4833 #[cfg(feature = "alloc")]
4834 {
4835 let tz = TimeZone::posix("EST5").unwrap();
4836 assert!(tz.to_fixed_offset().is_err());
4837
4838 let test_file = TzifTestFile::get("America/New_York");
4839 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
4840 assert!(tz.to_fixed_offset().is_err());
4841 }
4842 }
4843}