jiff/tz/timezone.rs
1use crate::{
2 civil::DateTime,
3 error::{err, Error},
4 tz::{
5 ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned},
6 offset::{Dst, Offset},
7 },
8 util::{array_str::ArrayStr, sync::Arc},
9 Timestamp, Zoned,
10};
11
12#[cfg(feature = "alloc")]
13use crate::tz::posix::PosixTimeZoneOwned;
14
15use self::repr::Repr;
16
17/// A representation of a [time zone].
18///
19/// A time zone is a set of rules for determining the civil time, via an offset
20/// from UTC, in a particular geographic region. In many cases, the offset
21/// in a particular time zone can vary over the course of a year through
22/// transitions into and out of [daylight saving time].
23///
24/// A `TimeZone` can be one of three possible representations:
25///
26/// * An identifier from the [IANA Time Zone Database] and the rules associated
27/// with that identifier.
28/// * A fixed offset where there are never any time zone transitions.
29/// * A [POSIX TZ] string that specifies a standard offset and an optional
30/// daylight saving time offset along with a rule for when DST is in effect.
31/// The rule applies for every year. Since POSIX TZ strings cannot capture the
32/// full complexity of time zone rules, they generally should not be used.
33///
34/// The most practical and useful representation is an IANA time zone. Namely,
35/// it enjoys broad support and its database is regularly updated to reflect
36/// real changes in time zone rules throughout the world. On Unix systems,
37/// the time zone database is typically found at `/usr/share/zoneinfo`. For
38/// more information on how Jiff interacts with The Time Zone Database, see
39/// [`TimeZoneDatabase`](crate::tz::TimeZoneDatabase).
40///
41/// In typical usage, users of Jiff shouldn't need to reference a `TimeZone`
42/// directly. Instead, there are convenience APIs on datetime types that accept
43/// IANA time zone identifiers and do automatic database lookups for you. For
44/// example, to convert a timestamp to a zone aware datetime:
45///
46/// ```
47/// use jiff::Timestamp;
48///
49/// let ts = Timestamp::from_second(1_456_789_123)?;
50/// let zdt = ts.in_tz("America/New_York")?;
51/// assert_eq!(zdt.to_string(), "2016-02-29T18:38:43-05:00[America/New_York]");
52///
53/// # Ok::<(), Box<dyn std::error::Error>>(())
54/// ```
55///
56/// Or to convert a civil datetime to a zoned datetime corresponding to a
57/// precise instant in time:
58///
59/// ```
60/// use jiff::civil::date;
61///
62/// let dt = date(2024, 7, 15).at(21, 27, 0, 0);
63/// let zdt = dt.in_tz("America/New_York")?;
64/// assert_eq!(zdt.to_string(), "2024-07-15T21:27:00-04:00[America/New_York]");
65///
66/// # Ok::<(), Box<dyn std::error::Error>>(())
67/// ```
68///
69/// Or even converted a zoned datetime from one time zone to another:
70///
71/// ```
72/// use jiff::civil::date;
73///
74/// let dt = date(2024, 7, 15).at(21, 27, 0, 0);
75/// let zdt1 = dt.in_tz("America/New_York")?;
76/// let zdt2 = zdt1.in_tz("Israel")?;
77/// assert_eq!(zdt2.to_string(), "2024-07-16T04:27:00+03:00[Israel]");
78///
79/// # Ok::<(), Box<dyn std::error::Error>>(())
80/// ```
81///
82/// # The system time zone
83///
84/// The system time zone can be retrieved via [`TimeZone::system`]. If it
85/// couldn't be detected or if the `tz-system` crate feature is not enabled,
86/// then [`TimeZone::UTC`] is returned. `TimeZone::system` is what's used
87/// internally for retrieving the current zoned datetime via [`Zoned::now`].
88///
89/// While there is no platform independent way to detect your system's
90/// "default" time zone, Jiff employs best-effort heuristics to determine it.
91/// (For example, by examining `/etc/localtime` on Unix systems.) When the
92/// heuristics fail, Jiff will emit a `WARN` level log. It can be viewed by
93/// installing a `log` compatible logger, such as [`env_logger`].
94///
95/// # Custom time zones
96///
97/// At present, Jiff doesn't provide any APIs for manually constructing a
98/// custom time zone. However, [`TimeZone::tzif`] is provided for reading
99/// any valid TZif formatted data, as specified by [RFC 8536]. This provides
100/// an interoperable way of utilizing custom time zone rules.
101///
102/// # A `TimeZone` is immutable
103///
104/// Once a `TimeZone` is created, it is immutable. That is, its underlying
105/// time zone transition rules will never change. This is true for system time
106/// zones or even if the IANA Time Zone Database it was loaded from changes on
107/// disk. The only way such changes can be observed is by re-requesting the
108/// `TimeZone` from a `TimeZoneDatabase`. (Or, in the case of the system time
109/// zone, by calling `TimeZone::system`.)
110///
111/// # A `TimeZone` is cheap to clone
112///
113/// A `TimeZone` can be cheaply cloned. It uses automic reference counting
114/// internally. When `alloc` is disabled, cloning a `TimeZone` is still cheap
115/// because POSIX time zones and TZif time zones are unsupported. Therefore,
116/// cloning a time zone does a deep copy (since automic reference counting is
117/// not available), but the data being copied is small.
118///
119/// # Time zone equality
120///
121/// `TimeZone` provides an imperfect notion of equality. That is, when two time
122/// zones are equal, then it is guaranteed for them to have the same rules.
123/// However, two time zones may compare unequal and yet still have the same
124/// rules.
125///
126/// The equality semantics are as follows:
127///
128/// * Two fixed offset time zones are equal when their offsets are equal.
129/// * Two POSIX time zones are equal when their original rule strings are
130/// byte-for-byte identical.
131/// * Two IANA time zones are equal when their identifiers are equal _and_
132/// checksums of their rules are equal.
133/// * In all other cases, time zones are unequal.
134///
135/// Time zone equality is, for example, used in APIs like [`Zoned::since`]
136/// when asking for spans with calendar units. Namely, since days can be of
137/// different lengths in different time zones, `Zoned::since` will return an
138/// error when the two zoned datetimes are in different time zones and when
139/// the caller requests units greater than hours.
140///
141/// # Dealing with ambiguity
142///
143/// The principal job of a `TimeZone` is to provide two different
144/// transformations:
145///
146/// * A conversion from a [`Timestamp`] to a civil time (also known as local,
147/// naive or plain time). This conversion is always unambiguous. That is,
148/// there is always precisely one representation of civil time for any
149/// particular instant in time for a particular time zone.
150/// * A conversion from a [`civil::DateTime`](crate::civil::DateTime) to an
151/// instant in time. This conversion is sometimes ambiguous in that a civil
152/// time might have either never appear on the clocks in a particular
153/// time zone (a gap), or in that the civil time may have been repeated on the
154/// clocks in a particular time zone (a fold). Typically, a transition to
155/// daylight saving time is a gap, while a transition out of daylight saving
156/// time is a fold.
157///
158/// The timestamp-to-civil time conversion is done via
159/// [`TimeZone::to_datetime`], or its lower level counterpart,
160/// [`TimeZone::to_offset`]. The civil time-to-timestamp conversion is done
161/// via one of the following routines:
162///
163/// * [`TimeZone::to_zoned`] conveniently returns a [`Zoned`] and automatically
164/// uses the
165/// [`Disambiguation::Compatible`](crate::tz::Disambiguation::Compatible)
166/// strategy if the given civil datetime is ambiguous in the time zone.
167/// * [`TimeZone::to_ambiguous_zoned`] returns a potentially ambiguous
168/// zoned datetime, [`AmbiguousZoned`], and provides fine-grained control over
169/// how to resolve ambiguity, if it occurs.
170/// * [`TimeZone::to_timestamp`] is like `TimeZone::to_zoned`, but returns
171/// a [`Timestamp`] instead.
172/// * [`TimeZone::to_ambiguous_timestamp`] is like
173/// `TimeZone::to_ambiguous_zoned`, but returns an [`AmbiguousTimestamp`]
174/// instead.
175///
176/// Here is an example where we explore the different disambiguation strategies
177/// for a fold in time, where in this case, the 1 o'clock hour is repeated:
178///
179/// ```
180/// use jiff::{civil::date, tz::TimeZone};
181///
182/// let tz = TimeZone::get("America/New_York")?;
183/// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
184/// // It's ambiguous, so asking for an unambiguous instant presents an error!
185/// assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
186/// // Gives you the earlier time in a fold, i.e., before DST ends:
187/// assert_eq!(
188/// tz.to_ambiguous_zoned(dt).earlier()?.to_string(),
189/// "2024-11-03T01:30:00-04:00[America/New_York]",
190/// );
191/// // Gives you the later time in a fold, i.e., after DST ends.
192/// // Notice the offset change from the previous example!
193/// assert_eq!(
194/// tz.to_ambiguous_zoned(dt).later()?.to_string(),
195/// "2024-11-03T01:30:00-05:00[America/New_York]",
196/// );
197/// // "Just give me something reasonable"
198/// assert_eq!(
199/// tz.to_ambiguous_zoned(dt).compatible()?.to_string(),
200/// "2024-11-03T01:30:00-04:00[America/New_York]",
201/// );
202///
203/// # Ok::<(), Box<dyn std::error::Error>>(())
204/// ```
205///
206/// # Serde integration
207///
208/// At present, a `TimeZone` does not implement Serde's `Serialize` or
209/// `Deserialize` traits directly. Nor does it implement `std::fmt::Display`
210/// or `std::str::FromStr`. The reason for this is that it's not totally
211/// clear if there is one single obvious behavior. Moreover, some `TimeZone`
212/// values do not have an obvious succinct serialized representation. (For
213/// example, when `/etc/localtime` on a Unix system is your system's time zone,
214/// and it isn't a symlink to a TZif file in `/usr/share/zoneinfo`. In which
215/// case, an IANA time zone identifier cannot easily be deduced by Jiff.)
216///
217/// Instead, Jiff offers helpers for use with Serde's [`with` attribute] via
218/// the [`fmt::serde`](crate::fmt::serde) module:
219///
220/// ```
221/// use jiff::tz::TimeZone;
222///
223/// #[derive(Debug, serde::Deserialize, serde::Serialize)]
224/// struct Record {
225/// #[serde(with = "jiff::fmt::serde::tz::optional")]
226/// tz: Option<TimeZone>,
227/// }
228///
229/// let json = r#"{"tz":"America/Nuuk"}"#;
230/// let got: Record = serde_json::from_str(&json)?;
231/// assert_eq!(got.tz, Some(TimeZone::get("America/Nuuk")?));
232/// assert_eq!(serde_json::to_string(&got)?, json);
233///
234/// # Ok::<(), Box<dyn std::error::Error>>(())
235/// ```
236///
237/// Alternatively, you may use the
238/// [`fmt::temporal::DateTimeParser::parse_time_zone`](crate::fmt::temporal::DateTimeParser::parse_time_zone)
239/// or
240/// [`fmt::temporal::DateTimePrinter::print_time_zone`](crate::fmt::temporal::DateTimePrinter::print_time_zone)
241/// routines to parse or print `TimeZone` values without using Serde.
242///
243/// [time zone]: https://en.wikipedia.org/wiki/Time_zone
244/// [daylight saving time]: https://en.wikipedia.org/wiki/Daylight_saving_time
245/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
246/// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
247/// [`env_logger`]: https://docs.rs/env_logger
248/// [RFC 8536]: https://datatracker.ietf.org/doc/html/rfc8536
249/// [`with` attribute]: https://serde.rs/field-attrs.html#with
250#[derive(Clone, Eq, PartialEq)]
251pub struct TimeZone {
252 repr: Repr,
253}
254
255impl TimeZone {
256 /// The UTC time zone.
257 ///
258 /// The offset of this time is `0` and never has any transitions.
259 pub const UTC: TimeZone = TimeZone { repr: Repr::utc() };
260
261 /// Returns the system configured time zone, if available.
262 ///
263 /// Detection of a system's default time zone is generally heuristic
264 /// based and platform specific.
265 ///
266 /// If callers need to know whether discovery of the system time zone
267 /// failed, then use [`TimeZone::try_system`].
268 ///
269 /// # Fallback behavior
270 ///
271 /// If the system's default time zone could not be determined, or if
272 /// the `tz-system` crate feature is not enabled, then this returns
273 /// [`TimeZone::unknown`]. A `WARN` level log will also be emitted with
274 /// a message explaining why time zone detection failed. The fallback to
275 /// an unknown time zone is a practical trade-off, is what most other
276 /// systems tend to do and is also recommended by [relevant standards such
277 /// as freedesktop.org][freedesktop-org-localtime].
278 ///
279 /// An unknown time zone _behaves_ like [`TimeZone::UTC`], but will
280 /// print as `Etc/Unknown` when converting a `Zoned` to a string.
281 ///
282 /// If you would instead like to fall back to UTC instead
283 /// of the special "unknown" time zone, then you can do
284 /// `TimeZone::try_system().unwrap_or(TimeZone::UTC)`.
285 ///
286 /// # Platform behavior
287 ///
288 /// This section is a "best effort" explanation of how the time zone is
289 /// detected on supported platforms. The behavior is subject to change.
290 ///
291 /// On all platforms, the `TZ` environment variable overrides any other
292 /// heuristic, and provides a way for end users to set the time zone for
293 /// specific use cases. In general, Jiff respects the [POSIX TZ] rules.
294 /// Here are some examples:
295 ///
296 /// * `TZ=America/New_York` for setting a time zone via an IANA Time Zone
297 /// Database Identifier.
298 /// * `TZ=/usr/share/zoneinfo/America/New_York` for setting a time zone
299 /// by providing a file path to a TZif file directly.
300 /// * `TZ=EST5EDT,M3.2.0,M11.1.0` for setting a time zone via a daylight
301 /// saving time transition rule.
302 ///
303 /// Otherwise, when `TZ` isn't set, then:
304 ///
305 /// On Unix non-Android systems, this inspects `/etc/localtime`. If it's
306 /// a symbolic link to an entry in `/usr/share/zoneinfo`, then the suffix
307 /// is considered an IANA Time Zone Database identifier. Otherwise,
308 /// `/etc/localtime` is read as a TZif file directly.
309 ///
310 /// On Android systems, this inspects the `persist.sys.timezone` property.
311 ///
312 /// On Windows, the system time zone is determined via
313 /// [`GetDynamicTimeZoneInformation`]. The result is then mapped to an
314 /// IANA Time Zone Database identifier via Unicode's
315 /// [CLDR XML data].
316 ///
317 /// [freedesktop-org-localtime]: https://www.freedesktop.org/software/systemd/man/latest/localtime.html
318 /// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
319 /// [`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
320 /// [CLDR XML data]: https://github.com/unicode-org/cldr/raw/main/common/supplemental/windowsZones.xml
321 #[inline]
322 pub fn system() -> TimeZone {
323 match TimeZone::try_system() {
324 Ok(tz) => tz,
325 Err(_err) => {
326 warn!(
327 "failed to get system time zone, \
328 falling back to `Etc/Unknown` \
329 (which behaves like UTC): {_err}",
330 );
331 TimeZone::unknown()
332 }
333 }
334 }
335
336 /// Returns the system configured time zone, if available.
337 ///
338 /// If the system's default time zone could not be determined, or if the
339 /// `tz-system` crate feature is not enabled, then this returns an error.
340 ///
341 /// Detection of a system's default time zone is generally heuristic
342 /// based and platform specific.
343 ///
344 /// Note that callers should generally prefer using [`TimeZone::system`].
345 /// If a system time zone could not be found, then it falls
346 /// back to [`TimeZone::UTC`] automatically. This is often
347 /// what is recommended by [relevant standards such as
348 /// freedesktop.org][freedesktop-org-localtime]. Conversely, this routine
349 /// is useful if detection of a system's default time zone is critical.
350 ///
351 /// # Platform behavior
352 ///
353 /// This section is a "best effort" explanation of how the time zone is
354 /// detected on supported platforms. The behavior is subject to change.
355 ///
356 /// On all platforms, the `TZ` environment variable overrides any other
357 /// heuristic, and provides a way for end users to set the time zone for
358 /// specific use cases. In general, Jiff respects the [POSIX TZ] rules.
359 /// Here are some examples:
360 ///
361 /// * `TZ=America/New_York` for setting a time zone via an IANA Time Zone
362 /// Database Identifier.
363 /// * `TZ=/usr/share/zoneinfo/America/New_York` for setting a time zone
364 /// by providing a file path to a TZif file directly.
365 /// * `TZ=EST5EDT,M3.2.0,M11.1.0` for setting a time zone via a daylight
366 /// saving time transition rule.
367 ///
368 /// Otherwise, when `TZ` isn't set, then:
369 ///
370 /// On Unix systems, this inspects `/etc/localtime`. If it's a symbolic
371 /// link to an entry in `/usr/share/zoneinfo`, then the suffix is
372 /// considered an IANA Time Zone Database identifier. Otherwise,
373 /// `/etc/localtime` is read as a TZif file directly.
374 ///
375 /// On Windows, the system time zone is determined via
376 /// [`GetDynamicTimeZoneInformation`]. The result is then mapped to an
377 /// IANA Time Zone Database identifier via Unicode's
378 /// [CLDR XML data].
379 ///
380 /// [freedesktop-org-localtime]: https://www.freedesktop.org/software/systemd/man/latest/localtime.html
381 /// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
382 /// [`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
383 /// [CLDR XML data]: https://github.com/unicode-org/cldr/raw/main/common/supplemental/windowsZones.xml
384 #[inline]
385 pub fn try_system() -> Result<TimeZone, Error> {
386 #[cfg(not(feature = "tz-system"))]
387 {
388 Err(err!(
389 "failed to get system time zone since 'tz-system' \
390 crate feature is not enabled",
391 ))
392 }
393 #[cfg(feature = "tz-system")]
394 {
395 crate::tz::system::get(crate::tz::db())
396 }
397 }
398
399 /// A convenience function for performing a time zone database lookup for
400 /// the given time zone identifier. It uses the default global time zone
401 /// database via [`tz::db()`](crate::tz::db()).
402 ///
403 /// # Errors
404 ///
405 /// This returns an error if the given time zone identifier could not be
406 /// found in the default [`TimeZoneDatabase`](crate::tz::TimeZoneDatabase).
407 ///
408 /// # Example
409 ///
410 /// ```
411 /// use jiff::{tz::TimeZone, Timestamp};
412 ///
413 /// let tz = TimeZone::get("Japan")?;
414 /// assert_eq!(
415 /// tz.to_datetime(Timestamp::UNIX_EPOCH).to_string(),
416 /// "1970-01-01T09:00:00",
417 /// );
418 ///
419 /// # Ok::<(), Box<dyn std::error::Error>>(())
420 /// ```
421 #[inline]
422 pub fn get(time_zone_name: &str) -> Result<TimeZone, Error> {
423 crate::tz::db().get(time_zone_name)
424 }
425
426 /// Returns a time zone with a fixed offset.
427 ///
428 /// A fixed offset will never have any transitions and won't follow any
429 /// particular time zone rules. In general, one should avoid using fixed
430 /// offset time zones unless you have a specific need for them. Otherwise,
431 /// IANA time zones via [`TimeZone::get`] should be preferred, as they
432 /// more accurately model the actual time zone transitions rules used in
433 /// practice.
434 ///
435 /// # Example
436 ///
437 /// ```
438 /// use jiff::{tz::{self, TimeZone}, Timestamp};
439 ///
440 /// let tz = TimeZone::fixed(tz::offset(10));
441 /// assert_eq!(
442 /// tz.to_datetime(Timestamp::UNIX_EPOCH).to_string(),
443 /// "1970-01-01T10:00:00",
444 /// );
445 ///
446 /// # Ok::<(), Box<dyn std::error::Error>>(())
447 /// ```
448 #[inline]
449 pub const fn fixed(offset: Offset) -> TimeZone {
450 // Not doing `offset == Offset::UTC` because of `const`.
451 if offset.seconds_ranged().get_unchecked() == 0 {
452 return TimeZone::UTC;
453 }
454 let repr = Repr::fixed(offset);
455 TimeZone { repr }
456 }
457
458 /// Creates a time zone from a [POSIX TZ] rule string.
459 ///
460 /// A POSIX time zone provides a way to tersely define a single daylight
461 /// saving time transition rule (or none at all) that applies for all
462 /// years.
463 ///
464 /// Users should avoid using this kind of time zone unless there is a
465 /// specific need for it. Namely, POSIX time zones cannot capture the full
466 /// complexity of time zone transition rules in the real world. (See the
467 /// example below.)
468 ///
469 /// [POSIX TZ]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
470 ///
471 /// # Errors
472 ///
473 /// This returns an error if the given POSIX time zone string is invalid.
474 ///
475 /// # Example
476 ///
477 /// This example demonstrates how a POSIX time zone may be historically
478 /// inaccurate:
479 ///
480 /// ```
481 /// use jiff::{civil::date, tz::TimeZone};
482 ///
483 /// // The tzdb entry for America/New_York.
484 /// let iana = TimeZone::get("America/New_York")?;
485 /// // The POSIX TZ string for New York DST that went into effect in 2007.
486 /// let posix = TimeZone::posix("EST5EDT,M3.2.0,M11.1.0")?;
487 ///
488 /// // New York entered DST on April 2, 2006 at 2am:
489 /// let dt = date(2006, 4, 2).at(2, 0, 0, 0);
490 /// // The IANA tzdb entry correctly reports it as ambiguous:
491 /// assert!(iana.to_ambiguous_timestamp(dt).is_ambiguous());
492 /// // But the POSIX time zone does not:
493 /// assert!(!posix.to_ambiguous_timestamp(dt).is_ambiguous());
494 ///
495 /// # Ok::<(), Box<dyn std::error::Error>>(())
496 /// ```
497 #[cfg(feature = "alloc")]
498 pub fn posix(posix_tz_string: &str) -> Result<TimeZone, Error> {
499 let posix_tz = PosixTimeZoneOwned::parse(posix_tz_string)?;
500 Ok(TimeZone::from_posix_tz(posix_tz))
501 }
502
503 /// Creates a time zone from a POSIX tz. Expose so that other parts of Jiff
504 /// can create a `TimeZone` from a POSIX tz. (Kinda sloppy to be honest.)
505 #[cfg(feature = "alloc")]
506 pub(crate) fn from_posix_tz(posix: PosixTimeZoneOwned) -> TimeZone {
507 let repr = Repr::arc_posix(Arc::new(posix));
508 TimeZone { repr }
509 }
510
511 /// Creates a time zone from TZif binary data, whose format is specified
512 /// in [RFC 8536]. All versions of TZif (up through version 4) are
513 /// supported.
514 ///
515 /// This constructor is typically not used, and instead, one should rely
516 /// on time zone lookups via time zone identifiers with routines like
517 /// [`TimeZone::get`]. However, this constructor does provide one way
518 /// of using custom time zones with Jiff.
519 ///
520 /// The name given should be a IANA time zone database identifier.
521 ///
522 /// [RFC 8536]: https://datatracker.ietf.org/doc/html/rfc8536
523 ///
524 /// # Errors
525 ///
526 /// This returns an error if the given data was not recognized as valid
527 /// TZif.
528 #[cfg(feature = "alloc")]
529 pub fn tzif(name: &str, data: &[u8]) -> Result<TimeZone, Error> {
530 use alloc::string::ToString;
531
532 let name = name.to_string();
533 let tzif = crate::tz::tzif::Tzif::parse(Some(name), data)?;
534 let repr = Repr::arc_tzif(Arc::new(tzif));
535 Ok(TimeZone { repr })
536 }
537
538 /// Returns a `TimeZone` that is specifially marked as "unknown."
539 ///
540 /// This corresponds to the Unicode CLDR identifier `Etc/Unknown`, which
541 /// is guaranteed to never be a valid IANA time zone identifier (as of
542 /// the `2025a` release of tzdb).
543 ///
544 /// This type of `TimeZone` is used in circumstances where one wants to
545 /// signal that discovering a time zone failed for some reason, but that
546 /// execution can reasonably continue. For example, [`TimeZone::system`]
547 /// returns this type of time zone when the system time zone could not be
548 /// discovered.
549 ///
550 /// # Example
551 ///
552 /// Jiff permits an "unknown" time zone to losslessly be transmitted
553 /// through serialization:
554 ///
555 /// ```
556 /// use jiff::{civil::date, tz::TimeZone, Zoned};
557 ///
558 /// let tz = TimeZone::unknown();
559 /// let zdt = date(2025, 2, 1).at(17, 0, 0, 0).to_zoned(tz)?;
560 /// assert_eq!(zdt.to_string(), "2025-02-01T17:00:00Z[Etc/Unknown]");
561 /// let got: Zoned = "2025-02-01T17:00:00Z[Etc/Unknown]".parse()?;
562 /// assert_eq!(got, zdt);
563 ///
564 /// # Ok::<(), Box<dyn std::error::Error>>(())
565 /// ```
566 ///
567 /// Note that not all systems support this. Some systems will reject
568 /// `Etc/Unknown` because it is not a valid IANA time zone identifier and
569 /// does not have an entry in the IANA time zone database. However, Jiff
570 /// takes this approach because it surfaces an error condition in detecting
571 /// the end user's time zone. Callers not wanting an "unknown" time zone
572 /// can use `TimeZone::try_system().unwrap_or(TimeZone::UTC)` instead of
573 /// `TimeZone::system`. (Where the latter falls back to the "unknown" time
574 /// zone when a system configured time zone could not be found.)
575 pub const fn unknown() -> TimeZone {
576 let repr = Repr::unknown();
577 TimeZone { repr }
578 }
579
580 /// This creates an unnamed TZif-backed `TimeZone`.
581 ///
582 /// At present, the only way for an unnamed TZif-backed `TimeZone` to be
583 /// created is when the system time zone has no identifiable name. For
584 /// example, when `/etc/localtime` is hard-linked to a TZif file instead
585 /// of being symlinked. In this case, there is no cheap and unambiguous
586 /// way to determine the time zone name. So we just let it be unnamed.
587 /// Since this is the only such case, and hopefully will only ever be the
588 /// only such case, we consider such unnamed TZif-back `TimeZone` values
589 /// as being the "system" time zone.
590 ///
591 /// When this is used to construct a `TimeZone`, the `TimeZone::name`
592 /// method will be "Local". This is... pretty unfortunate. I'm not sure
593 /// what else to do other than to make `TimeZone::name` return an
594 /// `Option<&str>`. But... we use it in a bunch of places and it just
595 /// seems bad for a time zone to not have a name.
596 ///
597 /// OK, because of the above, I renamed `TimeZone::name` to
598 /// `TimeZone::diagnostic_name`. This should make it clearer that you can't
599 /// really use the name to do anything interesting. This also makes more
600 /// sense for POSIX TZ strings too.
601 ///
602 /// In any case, this routine stays unexported because I don't want TZif
603 /// backed `TimeZone` values to proliferate. If you have a legitimate use
604 /// case otherwise, please file an issue. It will require API design.
605 ///
606 /// # Errors
607 ///
608 /// This returns an error if the given TZif data is invalid.
609 #[cfg(feature = "tz-system")]
610 pub(crate) fn tzif_system(data: &[u8]) -> Result<TimeZone, Error> {
611 let tzif = crate::tz::tzif::Tzif::parse(None, data)?;
612 let repr = Repr::arc_tzif(Arc::new(tzif));
613 Ok(TimeZone { repr })
614 }
615
616 #[inline]
617 pub(crate) fn diagnostic_name(&self) -> DiagnosticName<'_> {
618 DiagnosticName(self)
619 }
620
621 /// Returns true if and only if this `TimeZone` can be succinctly
622 /// serialized.
623 ///
624 /// Basically, this is only `false` when this `TimeZone` was created from
625 /// a `/etc/localtime` for which a valid IANA time zone identifier could
626 /// not be extracted.
627 #[cfg(feature = "serde")]
628 #[inline]
629 pub(crate) fn has_succinct_serialization(&self) -> bool {
630 repr::each! {
631 &self.repr,
632 UTC => true,
633 UNKNOWN => true,
634 FIXED(_offset) => true,
635 STATIC_TZIF(tzif) => tzif.name().is_some(),
636 ARC_TZIF(tzif) => tzif.name().is_some(),
637 ARC_POSIX(_posix) => true,
638 }
639 }
640
641 /// When this time zone was loaded from an IANA time zone database entry,
642 /// then this returns the canonicalized name for that time zone.
643 ///
644 /// # Example
645 ///
646 /// ```
647 /// use jiff::tz::TimeZone;
648 ///
649 /// let tz = TimeZone::get("america/NEW_YORK")?;
650 /// assert_eq!(tz.iana_name(), Some("America/New_York"));
651 ///
652 /// # Ok::<(), Box<dyn std::error::Error>>(())
653 /// ```
654 #[inline]
655 pub fn iana_name(&self) -> Option<&str> {
656 repr::each! {
657 &self.repr,
658 UTC => Some("UTC"),
659 // Note that while `Etc/Unknown` looks like an IANA time zone
660 // identifier, it is specifically and explicitly NOT an IANA time
661 // zone identifier. So we do not return it here if we have an
662 // unknown time zone identifier.
663 UNKNOWN => None,
664 FIXED(_offset) => None,
665 STATIC_TZIF(tzif) => tzif.name(),
666 ARC_TZIF(tzif) => tzif.name(),
667 ARC_POSIX(_posix) => None,
668 }
669 }
670
671 /// Returns true if and only if this time zone is unknown.
672 ///
673 /// This has the special internal identifier of `Etc/Unknown`, and this
674 /// is what will be used when converting a `Zoned` to a string.
675 ///
676 /// Note that while `Etc/Unknown` looks like an IANA time zone identifier,
677 /// it is specifically and explicitly not one. It is reserved and is
678 /// guaranteed to never be an IANA time zone identifier.
679 ///
680 /// An unknown time zone can be created via [`TimeZone::unknown`]. It is
681 /// also returned by [`TimeZone::system`] when a system configured time
682 /// zone could not be found.
683 ///
684 /// # Example
685 ///
686 /// ```
687 /// use jiff::tz::TimeZone;
688 ///
689 /// let tz = TimeZone::unknown();
690 /// assert_eq!(tz.iana_name(), None);
691 /// assert!(tz.is_unknown());
692 /// ```
693 #[inline]
694 pub fn is_unknown(&self) -> bool {
695 self.repr.is_unknown()
696 }
697
698 /// When this time zone is a POSIX time zone, return it.
699 ///
700 /// This doesn't attempt to convert other time zones that are representable
701 /// as POSIX time zones to POSIX time zones (e.g., fixed offset time
702 /// zones). Instead, this only returns something when the actual
703 /// representation of the time zone is a POSIX time zone.
704 #[cfg(feature = "alloc")]
705 #[inline]
706 pub(crate) fn posix_tz(&self) -> Option<&PosixTimeZoneOwned> {
707 repr::each! {
708 &self.repr,
709 UTC => None,
710 UNKNOWN => None,
711 FIXED(_offset) => None,
712 STATIC_TZIF(_tzif) => None,
713 ARC_TZIF(_tzif) => None,
714 ARC_POSIX(posix) => Some(posix),
715 }
716 }
717
718 /// Returns the civil datetime corresponding to the given timestamp in this
719 /// time zone.
720 ///
721 /// This operation is always unambiguous. That is, for any instant in time
722 /// supported by Jiff (that is, a `Timestamp`), there is always precisely
723 /// one civil datetime corresponding to that instant.
724 ///
725 /// Note that this is considered a lower level routine. Consider working
726 /// with zoned datetimes instead, and use [`Zoned::datetime`] to get its
727 /// civil time if necessary.
728 ///
729 /// # Example
730 ///
731 /// ```
732 /// use jiff::{tz::TimeZone, Timestamp};
733 ///
734 /// let tz = TimeZone::get("Europe/Rome")?;
735 /// assert_eq!(
736 /// tz.to_datetime(Timestamp::UNIX_EPOCH).to_string(),
737 /// "1970-01-01T01:00:00",
738 /// );
739 ///
740 /// # Ok::<(), Box<dyn std::error::Error>>(())
741 /// ```
742 ///
743 /// As mentioned above, consider using `Zoned` instead:
744 ///
745 /// ```
746 /// use jiff::{tz::TimeZone, Timestamp};
747 ///
748 /// let zdt = Timestamp::UNIX_EPOCH.in_tz("Europe/Rome")?;
749 /// assert_eq!(zdt.datetime().to_string(), "1970-01-01T01:00:00");
750 ///
751 /// # Ok::<(), Box<dyn std::error::Error>>(())
752 /// ```
753 #[inline]
754 pub fn to_datetime(&self, timestamp: Timestamp) -> DateTime {
755 self.to_offset(timestamp).to_datetime(timestamp)
756 }
757
758 /// Returns the offset corresponding to the given timestamp in this time
759 /// zone.
760 ///
761 /// This operation is always unambiguous. That is, for any instant in time
762 /// supported by Jiff (that is, a `Timestamp`), there is always precisely
763 /// one offset corresponding to that instant.
764 ///
765 /// Given an offset, one can use APIs like [`Offset::to_datetime`] to
766 /// create a civil datetime from a timestamp.
767 ///
768 /// This also returns whether this timestamp is considered to be in
769 /// "daylight saving time," as well as the abbreviation for the time zone
770 /// at this time.
771 ///
772 /// # Example
773 ///
774 /// ```
775 /// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
776 ///
777 /// let tz = TimeZone::get("America/New_York")?;
778 ///
779 /// // A timestamp in DST in New York.
780 /// let ts = Timestamp::from_second(1_720_493_204)?;
781 /// let offset = tz.to_offset(ts);
782 /// assert_eq!(offset, tz::offset(-4));
783 /// assert_eq!(offset.to_datetime(ts).to_string(), "2024-07-08T22:46:44");
784 ///
785 /// // A timestamp *not* in DST in New York.
786 /// let ts = Timestamp::from_second(1_704_941_204)?;
787 /// let offset = tz.to_offset(ts);
788 /// assert_eq!(offset, tz::offset(-5));
789 /// assert_eq!(offset.to_datetime(ts).to_string(), "2024-01-10T21:46:44");
790 ///
791 /// # Ok::<(), Box<dyn std::error::Error>>(())
792 /// ```
793 #[inline]
794 pub fn to_offset(&self, timestamp: Timestamp) -> Offset {
795 repr::each! {
796 &self.repr,
797 UTC => Offset::UTC,
798 UNKNOWN => Offset::UTC,
799 FIXED(offset) => offset,
800 STATIC_TZIF(tzif) => tzif.to_offset(timestamp),
801 ARC_TZIF(tzif) => tzif.to_offset(timestamp),
802 ARC_POSIX(posix) => posix.to_offset(timestamp),
803 }
804 }
805
806 /// Returns the offset information corresponding to the given timestamp in
807 /// this time zone. This includes the offset along with daylight saving
808 /// time status and a time zone abbreviation.
809 ///
810 /// This is like [`TimeZone::to_offset`], but returns the aforementioned
811 /// extra data in addition to the offset. This data may, in some cases, be
812 /// more expensive to compute.
813 ///
814 /// # Example
815 ///
816 /// ```
817 /// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
818 ///
819 /// let tz = TimeZone::get("America/New_York")?;
820 ///
821 /// // A timestamp in DST in New York.
822 /// let ts = Timestamp::from_second(1_720_493_204)?;
823 /// let info = tz.to_offset_info(ts);
824 /// assert_eq!(info.offset(), tz::offset(-4));
825 /// assert_eq!(info.dst(), Dst::Yes);
826 /// assert_eq!(info.abbreviation(), "EDT");
827 /// assert_eq!(
828 /// info.offset().to_datetime(ts).to_string(),
829 /// "2024-07-08T22:46:44",
830 /// );
831 ///
832 /// // A timestamp *not* in DST in New York.
833 /// let ts = Timestamp::from_second(1_704_941_204)?;
834 /// let info = tz.to_offset_info(ts);
835 /// assert_eq!(info.offset(), tz::offset(-5));
836 /// assert_eq!(info.dst(), Dst::No);
837 /// assert_eq!(info.abbreviation(), "EST");
838 /// assert_eq!(
839 /// info.offset().to_datetime(ts).to_string(),
840 /// "2024-01-10T21:46:44",
841 /// );
842 ///
843 /// # Ok::<(), Box<dyn std::error::Error>>(())
844 /// ```
845 #[inline]
846 pub fn to_offset_info<'t>(
847 &'t self,
848 timestamp: Timestamp,
849 ) -> TimeZoneOffsetInfo<'t> {
850 repr::each! {
851 &self.repr,
852 UTC => TimeZoneOffsetInfo {
853 offset: Offset::UTC,
854 dst: Dst::No,
855 abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
856 },
857 UNKNOWN => TimeZoneOffsetInfo {
858 offset: Offset::UTC,
859 dst: Dst::No,
860 // It'd be kinda nice if this were just `ERR` to
861 // indicate an error, but I can't find any precedent
862 // for that. And CLDR says `Etc/Unknown` should behave
863 // like UTC, so... I guess we use UTC here.
864 abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
865 },
866 FIXED(offset) => {
867 let abbreviation =
868 TimeZoneAbbreviation::Owned(offset.to_array_str());
869 TimeZoneOffsetInfo {
870 offset,
871 dst: Dst::No,
872 abbreviation,
873 }
874 },
875 STATIC_TZIF(tzif) => tzif.to_offset_info(timestamp),
876 ARC_TZIF(tzif) => tzif.to_offset_info(timestamp),
877 ARC_POSIX(posix) => posix.to_offset_info(timestamp),
878 }
879 }
880
881 /// If this time zone is a fixed offset, then this returns the offset.
882 /// If this time zone is not a fixed offset, then an error is returned.
883 ///
884 /// If you just need an offset for a given timestamp, then you can use
885 /// [`TimeZone::to_offset`]. Or, if you need an offset for a civil
886 /// datetime, then you can use [`TimeZone::to_ambiguous_timestamp`] or
887 /// [`TimeZone::to_ambiguous_zoned`], although the result may be ambiguous.
888 ///
889 /// Generally, this routine is useful when you need to know whether the
890 /// time zone is fixed, and you want to get the offset without having to
891 /// specify a timestamp. This is sometimes required for interoperating with
892 /// other datetime systems that need to distinguish between time zones that
893 /// are fixed and time zones that are based on rules such as those found in
894 /// the IANA time zone database.
895 ///
896 /// # Example
897 ///
898 /// ```
899 /// use jiff::tz::{Offset, TimeZone};
900 ///
901 /// let tz = TimeZone::get("America/New_York")?;
902 /// // A named time zone is not a fixed offset
903 /// // and so cannot be converted to an offset
904 /// // without a timestamp or civil datetime.
905 /// assert_eq!(
906 /// tz.to_fixed_offset().unwrap_err().to_string(),
907 /// "cannot convert non-fixed IANA time zone \
908 /// to offset without timestamp or civil datetime",
909 /// );
910 ///
911 /// let tz = TimeZone::UTC;
912 /// // UTC is a fixed offset and so can be converted
913 /// // without a timestamp.
914 /// assert_eq!(tz.to_fixed_offset()?, Offset::UTC);
915 ///
916 /// // And of course, creating a time zone from a
917 /// // fixed offset results in a fixed offset time
918 /// // zone too:
919 /// let tz = TimeZone::fixed(jiff::tz::offset(-10));
920 /// assert_eq!(tz.to_fixed_offset()?, jiff::tz::offset(-10));
921 ///
922 /// # Ok::<(), Box<dyn std::error::Error>>(())
923 /// ```
924 #[inline]
925 pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
926 let mkerr = || {
927 err!(
928 "cannot convert non-fixed {kind} time zone to offset \
929 without timestamp or civil datetime",
930 kind = self.kind_description(),
931 )
932 };
933 repr::each! {
934 &self.repr,
935 UTC => Ok(Offset::UTC),
936 UNKNOWN => Ok(Offset::UTC),
937 FIXED(offset) => Ok(offset),
938 STATIC_TZIF(_tzif) => Err(mkerr()),
939 ARC_TZIF(_tzif) => Err(mkerr()),
940 ARC_POSIX(_posix) => Err(mkerr()),
941 }
942 }
943
944 /// Converts a civil datetime to a [`Zoned`] in this time zone.
945 ///
946 /// The given civil datetime may be ambiguous in this time zone. A civil
947 /// datetime is ambiguous when either of the following occurs:
948 ///
949 /// * When the civil datetime falls into a "gap." That is, when there is a
950 /// jump forward in time where a span of time does not appear on the clocks
951 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
952 /// into daylight saving time.
953 /// * When the civil datetime falls into a "fold." That is, when there is
954 /// a jump backward in time where a span of time is _repeated_ on the
955 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
956 /// backward out of daylight saving time.
957 ///
958 /// This routine automatically resolves both of the above ambiguities via
959 /// the
960 /// [`Disambiguation::Compatible`](crate::tz::Disambiguation::Compatible)
961 /// strategy. That in, the case of a gap, the time after the gap is used.
962 /// In the case of a fold, the first repetition of the clock time is used.
963 ///
964 /// # Example
965 ///
966 /// This example shows how disambiguation works:
967 ///
968 /// ```
969 /// use jiff::{civil::date, tz::TimeZone};
970 ///
971 /// let tz = TimeZone::get("America/New_York")?;
972 ///
973 /// // This demonstrates disambiguation behavior for a gap.
974 /// let zdt = tz.to_zoned(date(2024, 3, 10).at(2, 30, 0, 0))?;
975 /// assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]");
976 /// // This demonstrates disambiguation behavior for a fold.
977 /// // Notice the offset: the -04 corresponds to the time while
978 /// // still in DST. The second repetition of the 1 o'clock hour
979 /// // occurs outside of DST, in "standard" time, with the offset -5.
980 /// let zdt = tz.to_zoned(date(2024, 11, 3).at(1, 30, 0, 0))?;
981 /// assert_eq!(zdt.to_string(), "2024-11-03T01:30:00-04:00[America/New_York]");
982 ///
983 /// # Ok::<(), Box<dyn std::error::Error>>(())
984 /// ```
985 #[inline]
986 pub fn to_zoned(&self, dt: DateTime) -> Result<Zoned, Error> {
987 self.to_ambiguous_zoned(dt).compatible()
988 }
989
990 /// Converts a civil datetime to a possibly ambiguous zoned datetime in
991 /// this time zone.
992 ///
993 /// The given civil datetime may be ambiguous in this time zone. A civil
994 /// datetime is ambiguous when either of the following occurs:
995 ///
996 /// * When the civil datetime falls into a "gap." That is, when there is a
997 /// jump forward in time where a span of time does not appear on the clocks
998 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
999 /// into daylight saving time.
1000 /// * When the civil datetime falls into a "fold." That is, when there is
1001 /// a jump backward in time where a span of time is _repeated_ on the
1002 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1003 /// backward out of daylight saving time.
1004 ///
1005 /// Unlike [`TimeZone::to_zoned`], this method does not do any automatic
1006 /// disambiguation. Instead, callers are expected to use the methods on
1007 /// [`AmbiguousZoned`] to resolve any ambiguity, if it occurs.
1008 ///
1009 /// # Example
1010 ///
1011 /// This example shows how to return an error when the civil datetime given
1012 /// is ambiguous:
1013 ///
1014 /// ```
1015 /// use jiff::{civil::date, tz::TimeZone};
1016 ///
1017 /// let tz = TimeZone::get("America/New_York")?;
1018 ///
1019 /// // This is not ambiguous:
1020 /// let dt = date(2024, 3, 10).at(1, 0, 0, 0);
1021 /// assert_eq!(
1022 /// tz.to_ambiguous_zoned(dt).unambiguous()?.to_string(),
1023 /// "2024-03-10T01:00:00-05:00[America/New_York]",
1024 /// );
1025 /// // But this is a gap, and thus ambiguous! So an error is returned.
1026 /// let dt = date(2024, 3, 10).at(2, 0, 0, 0);
1027 /// assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
1028 /// // And so is this, because it's a fold.
1029 /// let dt = date(2024, 11, 3).at(1, 0, 0, 0);
1030 /// assert!(tz.to_ambiguous_zoned(dt).unambiguous().is_err());
1031 ///
1032 /// # Ok::<(), Box<dyn std::error::Error>>(())
1033 /// ```
1034 #[inline]
1035 pub fn to_ambiguous_zoned(&self, dt: DateTime) -> AmbiguousZoned {
1036 self.clone().into_ambiguous_zoned(dt)
1037 }
1038
1039 /// Converts a civil datetime to a possibly ambiguous zoned datetime in
1040 /// this time zone, and does so by assuming ownership of this `TimeZone`.
1041 ///
1042 /// This is identical to [`TimeZone::to_ambiguous_zoned`], but it avoids
1043 /// a `TimeZone::clone()` call. (Which are cheap, but not completely free.)
1044 ///
1045 /// # Example
1046 ///
1047 /// This example shows how to create a `Zoned` value from a `TimeZone`
1048 /// and a `DateTime` without cloning the `TimeZone`:
1049 ///
1050 /// ```
1051 /// use jiff::{civil::date, tz::TimeZone};
1052 ///
1053 /// let tz = TimeZone::get("America/New_York")?;
1054 /// let dt = date(2024, 3, 10).at(1, 0, 0, 0);
1055 /// assert_eq!(
1056 /// tz.into_ambiguous_zoned(dt).unambiguous()?.to_string(),
1057 /// "2024-03-10T01:00:00-05:00[America/New_York]",
1058 /// );
1059 ///
1060 /// # Ok::<(), Box<dyn std::error::Error>>(())
1061 /// ```
1062 #[inline]
1063 pub fn into_ambiguous_zoned(self, dt: DateTime) -> AmbiguousZoned {
1064 self.to_ambiguous_timestamp(dt).into_ambiguous_zoned(self)
1065 }
1066
1067 /// Converts a civil datetime to a [`Timestamp`] in this time zone.
1068 ///
1069 /// The given civil datetime may be ambiguous in this time zone. A civil
1070 /// datetime is ambiguous when either of the following occurs:
1071 ///
1072 /// * When the civil datetime falls into a "gap." That is, when there is a
1073 /// jump forward in time where a span of time does not appear on the clocks
1074 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
1075 /// into daylight saving time.
1076 /// * When the civil datetime falls into a "fold." That is, when there is
1077 /// a jump backward in time where a span of time is _repeated_ on the
1078 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1079 /// backward out of daylight saving time.
1080 ///
1081 /// This routine automatically resolves both of the above ambiguities via
1082 /// the
1083 /// [`Disambiguation::Compatible`](crate::tz::Disambiguation::Compatible)
1084 /// strategy. That in, the case of a gap, the time after the gap is used.
1085 /// In the case of a fold, the first repetition of the clock time is used.
1086 ///
1087 /// This routine is identical to [`TimeZone::to_zoned`], except it returns
1088 /// a `Timestamp` instead of a zoned datetime. The benefit of this
1089 /// method is that it never requires cloning or consuming ownership of a
1090 /// `TimeZone`, and it doesn't require construction of `Zoned` which has
1091 /// a small but non-zero cost. (This is partially because a `Zoned` value
1092 /// contains a `TimeZone`, but of course, a `Timestamp` does not.)
1093 ///
1094 /// # Example
1095 ///
1096 /// This example shows how disambiguation works:
1097 ///
1098 /// ```
1099 /// use jiff::{civil::date, tz::TimeZone};
1100 ///
1101 /// let tz = TimeZone::get("America/New_York")?;
1102 ///
1103 /// // This demonstrates disambiguation behavior for a gap.
1104 /// let ts = tz.to_timestamp(date(2024, 3, 10).at(2, 30, 0, 0))?;
1105 /// assert_eq!(ts.to_string(), "2024-03-10T07:30:00Z");
1106 /// // This demonstrates disambiguation behavior for a fold.
1107 /// // Notice the offset: the -04 corresponds to the time while
1108 /// // still in DST. The second repetition of the 1 o'clock hour
1109 /// // occurs outside of DST, in "standard" time, with the offset -5.
1110 /// let ts = tz.to_timestamp(date(2024, 11, 3).at(1, 30, 0, 0))?;
1111 /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z");
1112 ///
1113 /// # Ok::<(), Box<dyn std::error::Error>>(())
1114 /// ```
1115 #[inline]
1116 pub fn to_timestamp(&self, dt: DateTime) -> Result<Timestamp, Error> {
1117 self.to_ambiguous_timestamp(dt).compatible()
1118 }
1119
1120 /// Converts a civil datetime to a possibly ambiguous timestamp in
1121 /// this time zone.
1122 ///
1123 /// The given civil datetime may be ambiguous in this time zone. A civil
1124 /// datetime is ambiguous when either of the following occurs:
1125 ///
1126 /// * When the civil datetime falls into a "gap." That is, when there is a
1127 /// jump forward in time where a span of time does not appear on the clocks
1128 /// in this time zone. This _typically_ manifests as a 1 hour jump forward
1129 /// into daylight saving time.
1130 /// * When the civil datetime falls into a "fold." That is, when there is
1131 /// a jump backward in time where a span of time is _repeated_ on the
1132 /// clocks in this time zone. This _typically_ manifests as a 1 hour jump
1133 /// backward out of daylight saving time.
1134 ///
1135 /// Unlike [`TimeZone::to_timestamp`], this method does not do any
1136 /// automatic disambiguation. Instead, callers are expected to use the
1137 /// methods on [`AmbiguousTimestamp`] to resolve any ambiguity, if it
1138 /// occurs.
1139 ///
1140 /// This routine is identical to [`TimeZone::to_ambiguous_zoned`], except
1141 /// it returns an `AmbiguousTimestamp` instead of a `AmbiguousZoned`. The
1142 /// benefit of this method is that it never requires cloning or consuming
1143 /// ownership of a `TimeZone`, and it doesn't require construction of
1144 /// `Zoned` which has a small but non-zero cost. (This is partially because
1145 /// a `Zoned` value contains a `TimeZone`, but of course, a `Timestamp`
1146 /// does not.)
1147 ///
1148 /// # Example
1149 ///
1150 /// This example shows how to return an error when the civil datetime given
1151 /// is ambiguous:
1152 ///
1153 /// ```
1154 /// use jiff::{civil::date, tz::TimeZone};
1155 ///
1156 /// let tz = TimeZone::get("America/New_York")?;
1157 ///
1158 /// // This is not ambiguous:
1159 /// let dt = date(2024, 3, 10).at(1, 0, 0, 0);
1160 /// assert_eq!(
1161 /// tz.to_ambiguous_timestamp(dt).unambiguous()?.to_string(),
1162 /// "2024-03-10T06:00:00Z",
1163 /// );
1164 /// // But this is a gap, and thus ambiguous! So an error is returned.
1165 /// let dt = date(2024, 3, 10).at(2, 0, 0, 0);
1166 /// assert!(tz.to_ambiguous_timestamp(dt).unambiguous().is_err());
1167 /// // And so is this, because it's a fold.
1168 /// let dt = date(2024, 11, 3).at(1, 0, 0, 0);
1169 /// assert!(tz.to_ambiguous_timestamp(dt).unambiguous().is_err());
1170 ///
1171 /// # Ok::<(), Box<dyn std::error::Error>>(())
1172 /// ```
1173 #[inline]
1174 pub fn to_ambiguous_timestamp(&self, dt: DateTime) -> AmbiguousTimestamp {
1175 let ambiguous_kind = repr::each! {
1176 &self.repr,
1177 UTC => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
1178 UNKNOWN => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
1179 FIXED(offset) => AmbiguousOffset::Unambiguous { offset },
1180 STATIC_TZIF(tzif) => tzif.to_ambiguous_kind(dt),
1181 ARC_TZIF(tzif) => tzif.to_ambiguous_kind(dt),
1182 ARC_POSIX(posix) => posix.to_ambiguous_kind(dt),
1183 };
1184 AmbiguousTimestamp::new(dt, ambiguous_kind)
1185 }
1186
1187 /// Returns an iterator of time zone transitions preceding the given
1188 /// timestamp. The iterator returned yields [`TimeZoneTransition`]
1189 /// elements.
1190 ///
1191 /// The order of the iterator returned moves backward through time. If
1192 /// there is a previous transition, then the timestamp of that transition
1193 /// is guaranteed to be strictly less than the timestamp given.
1194 ///
1195 /// This is a low level API that you generally shouldn't need. It's
1196 /// useful in cases where you need to know something about the specific
1197 /// instants at which time zone transitions occur. For example, an embedded
1198 /// device might need to be explicitly programmed with daylight saving
1199 /// time transitions. APIs like this enable callers to explore those
1200 /// transitions.
1201 ///
1202 /// A time zone transition refers to a specific point in time when the
1203 /// offset from UTC for a particular geographical region changes. This
1204 /// is usually a result of daylight saving time, but it can also occur
1205 /// when a geographic region changes its permanent offset from UTC.
1206 ///
1207 /// The iterator returned is not guaranteed to yield any elements. For
1208 /// example, this occurs with a fixed offset time zone. Logically, it
1209 /// would also be possible for the iterator to be infinite, except that
1210 /// eventually the timestamp would overflow Jiff's minimum timestamp
1211 /// value, at which point, iteration stops.
1212 ///
1213 /// # Example: time since the previous transition
1214 ///
1215 /// This example shows how much time has passed since the previous time
1216 /// zone transition:
1217 ///
1218 /// ```
1219 /// use jiff::{Unit, Zoned};
1220 ///
1221 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1222 /// let trans = now.time_zone().preceding(now.timestamp()).next().unwrap();
1223 /// let prev_at = trans.timestamp().to_zoned(now.time_zone().clone());
1224 /// let span = now.since((Unit::Year, &prev_at))?;
1225 /// assert_eq!(format!("{span:#}"), "1mo 27d 17h 25m");
1226 ///
1227 /// # Ok::<(), Box<dyn std::error::Error>>(())
1228 /// ```
1229 ///
1230 /// # Example: show the 5 previous time zone transitions
1231 ///
1232 /// This shows how to find the 5 preceding time zone transitions (from a
1233 /// particular datetime) for a particular time zone:
1234 ///
1235 /// ```
1236 /// use jiff::{tz::offset, Zoned};
1237 ///
1238 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1239 /// let transitions = now
1240 /// .time_zone()
1241 /// .preceding(now.timestamp())
1242 /// .take(5)
1243 /// .map(|t| (
1244 /// t.timestamp().to_zoned(now.time_zone().clone()),
1245 /// t.offset(),
1246 /// t.abbreviation().to_string(),
1247 /// ))
1248 /// .collect::<Vec<_>>();
1249 /// assert_eq!(transitions, vec![
1250 /// ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1251 /// ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1252 /// ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1253 /// ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1254 /// ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1255 /// ]);
1256 ///
1257 /// # Ok::<(), Box<dyn std::error::Error>>(())
1258 /// ```
1259 #[inline]
1260 pub fn preceding<'t>(
1261 &'t self,
1262 timestamp: Timestamp,
1263 ) -> TimeZonePrecedingTransitions<'t> {
1264 TimeZonePrecedingTransitions { tz: self, cur: timestamp }
1265 }
1266
1267 /// Returns an iterator of time zone transitions following the given
1268 /// timestamp. The iterator returned yields [`TimeZoneTransition`]
1269 /// elements.
1270 ///
1271 /// The order of the iterator returned moves forward through time. If
1272 /// there is a following transition, then the timestamp of that transition
1273 /// is guaranteed to be strictly greater than the timestamp given.
1274 ///
1275 /// This is a low level API that you generally shouldn't need. It's
1276 /// useful in cases where you need to know something about the specific
1277 /// instants at which time zone transitions occur. For example, an embedded
1278 /// device might need to be explicitly programmed with daylight saving
1279 /// time transitions. APIs like this enable callers to explore those
1280 /// transitions.
1281 ///
1282 /// A time zone transition refers to a specific point in time when the
1283 /// offset from UTC for a particular geographical region changes. This
1284 /// is usually a result of daylight saving time, but it can also occur
1285 /// when a geographic region changes its permanent offset from UTC.
1286 ///
1287 /// The iterator returned is not guaranteed to yield any elements. For
1288 /// example, this occurs with a fixed offset time zone. Logically, it
1289 /// would also be possible for the iterator to be infinite, except that
1290 /// eventually the timestamp would overflow Jiff's maximum timestamp
1291 /// value, at which point, iteration stops.
1292 ///
1293 /// # Example: time until the next transition
1294 ///
1295 /// This example shows how much time is left until the next time zone
1296 /// transition:
1297 ///
1298 /// ```
1299 /// use jiff::{Unit, Zoned};
1300 ///
1301 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1302 /// let trans = now.time_zone().following(now.timestamp()).next().unwrap();
1303 /// let next_at = trans.timestamp().to_zoned(now.time_zone().clone());
1304 /// let span = now.until((Unit::Year, &next_at))?;
1305 /// assert_eq!(format!("{span:#}"), "2mo 8d 7h 35m");
1306 ///
1307 /// # Ok::<(), Box<dyn std::error::Error>>(())
1308 /// ```
1309 ///
1310 /// # Example: show the 5 next time zone transitions
1311 ///
1312 /// This shows how to find the 5 following time zone transitions (from a
1313 /// particular datetime) for a particular time zone:
1314 ///
1315 /// ```
1316 /// use jiff::{tz::offset, Zoned};
1317 ///
1318 /// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1319 /// let transitions = now
1320 /// .time_zone()
1321 /// .following(now.timestamp())
1322 /// .take(5)
1323 /// .map(|t| (
1324 /// t.timestamp().to_zoned(now.time_zone().clone()),
1325 /// t.offset(),
1326 /// t.abbreviation().to_string(),
1327 /// ))
1328 /// .collect::<Vec<_>>();
1329 /// assert_eq!(transitions, vec![
1330 /// ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1331 /// ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1332 /// ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1333 /// ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1334 /// ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1335 /// ]);
1336 ///
1337 /// # Ok::<(), Box<dyn std::error::Error>>(())
1338 /// ```
1339 #[inline]
1340 pub fn following<'t>(
1341 &'t self,
1342 timestamp: Timestamp,
1343 ) -> TimeZoneFollowingTransitions<'t> {
1344 TimeZoneFollowingTransitions { tz: self, cur: timestamp }
1345 }
1346
1347 /// Used by the "preceding transitions" iterator.
1348 #[inline]
1349 fn previous_transition(
1350 &self,
1351 timestamp: Timestamp,
1352 ) -> Option<TimeZoneTransition> {
1353 repr::each! {
1354 &self.repr,
1355 UTC => None,
1356 UNKNOWN => None,
1357 FIXED(_offset) => None,
1358 STATIC_TZIF(tzif) => tzif.previous_transition(timestamp),
1359 ARC_TZIF(tzif) => tzif.previous_transition(timestamp),
1360 ARC_POSIX(posix) => posix.previous_transition(timestamp),
1361 }
1362 }
1363
1364 /// Used by the "following transitions" iterator.
1365 #[inline]
1366 fn next_transition(
1367 &self,
1368 timestamp: Timestamp,
1369 ) -> Option<TimeZoneTransition> {
1370 repr::each! {
1371 &self.repr,
1372 UTC => None,
1373 UNKNOWN => None,
1374 FIXED(_offset) => None,
1375 STATIC_TZIF(tzif) => tzif.next_transition(timestamp),
1376 ARC_TZIF(tzif) => tzif.next_transition(timestamp),
1377 ARC_POSIX(posix) => posix.next_transition(timestamp),
1378 }
1379 }
1380
1381 /// Returns a short description about the kind of this time zone.
1382 ///
1383 /// This is useful in error messages.
1384 fn kind_description(&self) -> &str {
1385 repr::each! {
1386 &self.repr,
1387 UTC => "UTC",
1388 UNKNOWN => "Etc/Unknown",
1389 FIXED(_offset) => "fixed",
1390 STATIC_TZIF(_tzif) => "IANA",
1391 ARC_TZIF(_tzif) => "IANA",
1392 ARC_POSIX(_posix) => "POSIX",
1393 }
1394 }
1395}
1396
1397// Exposed APIs for Jiff's time zone proc macro.
1398//
1399// These are NOT part of Jiff's public API. There are *zero* semver guarantees
1400// for them.
1401#[doc(hidden)]
1402impl TimeZone {
1403 pub const fn __internal_from_tzif(
1404 tzif: &'static crate::tz::tzif::TzifStatic,
1405 ) -> TimeZone {
1406 let repr = Repr::static_tzif(tzif);
1407 TimeZone { repr }
1408 }
1409
1410 /// Returns a dumb copy of this `TimeZone`.
1411 ///
1412 /// # Safety
1413 ///
1414 /// Callers must ensure that this time zone is UTC, unknown, a fixed
1415 /// offset or created with `TimeZone::__internal_from_tzif`.
1416 ///
1417 /// Namely, this specifically does not increment the ref count for
1418 /// the `Arc` pointers when the tag is `ARC_TZIF` or `ARC_POSIX`.
1419 /// This means that incorrect usage of this routine can lead to
1420 /// use-after-free.
1421 #[inline]
1422 pub const unsafe fn copy(&self) -> TimeZone {
1423 // SAFETY: Requirements are forwarded to the caller.
1424 unsafe { TimeZone { repr: self.repr.copy() } }
1425 }
1426}
1427
1428impl core::fmt::Debug for TimeZone {
1429 #[inline]
1430 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1431 f.debug_tuple("TimeZone").field(&self.repr).finish()
1432 }
1433}
1434
1435/// A representation a single time zone transition.
1436///
1437/// A time zone transition is an instant in time the marks the beginning of
1438/// a change in the offset from UTC that civil time is computed from in a
1439/// particular time zone. For example, when daylight saving time comes into
1440/// effect (or goes away). Another example is when a geographic region changes
1441/// its permanent offset from UTC.
1442///
1443/// This is a low level type that you generally shouldn't need. It's useful in
1444/// cases where you need to know something about the specific instants at which
1445/// time zone transitions occur. For example, an embedded device might need to
1446/// be explicitly programmed with daylight saving time transitions. APIs like
1447/// this enable callers to explore those transitions.
1448///
1449/// This type is yielded by the iterators
1450/// [`TimeZonePrecedingTransitions`] and
1451/// [`TimeZoneFollowingTransitions`]. The iterators are created by
1452/// [`TimeZone::preceding`] and [`TimeZone::following`], respectively.
1453///
1454/// # Example
1455///
1456/// This shows a somewhat silly example that finds all of the unique civil
1457/// (or "clock" or "local") times at which a time zone transition has occurred
1458/// in a particular time zone:
1459///
1460/// ```
1461/// use std::collections::BTreeSet;
1462/// use jiff::{civil, tz::TimeZone};
1463///
1464/// let tz = TimeZone::get("America/New_York")?;
1465/// let now = civil::date(2024, 12, 31).at(18, 25, 0, 0).to_zoned(tz.clone())?;
1466/// let mut set = BTreeSet::new();
1467/// for trans in tz.preceding(now.timestamp()) {
1468/// let time = tz.to_datetime(trans.timestamp()).time();
1469/// set.insert(time);
1470/// }
1471/// assert_eq!(Vec::from_iter(set), vec![
1472/// civil::time(1, 0, 0, 0), // typical transition out of DST
1473/// civil::time(3, 0, 0, 0), // typical transition into DST
1474/// civil::time(12, 0, 0, 0), // from when IANA starts keeping track
1475/// civil::time(19, 0, 0, 0), // from World War 2
1476/// ]);
1477///
1478/// # Ok::<(), Box<dyn std::error::Error>>(())
1479/// ```
1480#[derive(Clone, Debug)]
1481pub struct TimeZoneTransition<'t> {
1482 // We don't currently do anything smart to make iterating over
1483 // transitions faster. We could if we pushed the iterator impl down into
1484 // the respective modules (`posix` and `tzif`), but it's not clear such
1485 // optimization is really worth it. However, this API should permit that
1486 // kind of optimization in the future.
1487 pub(crate) timestamp: Timestamp,
1488 pub(crate) offset: Offset,
1489 pub(crate) abbrev: &'t str,
1490 pub(crate) dst: Dst,
1491}
1492
1493impl<'t> TimeZoneTransition<'t> {
1494 /// Returns the timestamp at which this transition began.
1495 ///
1496 /// # Example
1497 ///
1498 /// ```
1499 /// use jiff::{civil, tz::TimeZone};
1500 ///
1501 /// let tz = TimeZone::get("US/Eastern")?;
1502 /// // Look for the first time zone transition in `US/Eastern` following
1503 /// // 2023-03-09 00:00:00.
1504 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1505 /// let next = tz.following(start).next().unwrap();
1506 /// assert_eq!(
1507 /// next.timestamp().to_zoned(tz.clone()).to_string(),
1508 /// "2024-03-10T03:00:00-04:00[US/Eastern]",
1509 /// );
1510 ///
1511 /// # Ok::<(), Box<dyn std::error::Error>>(())
1512 /// ```
1513 #[inline]
1514 pub fn timestamp(&self) -> Timestamp {
1515 self.timestamp
1516 }
1517
1518 /// Returns the offset corresponding to this time zone transition. All
1519 /// instants at and following this transition's timestamp (and before the
1520 /// next transition's timestamp) need to apply this offset from UTC to get
1521 /// the civil or "local" time in the corresponding time zone.
1522 ///
1523 /// # Example
1524 ///
1525 /// ```
1526 /// use jiff::{civil, tz::{TimeZone, offset}};
1527 ///
1528 /// let tz = TimeZone::get("US/Eastern")?;
1529 /// // Get the offset of the next transition after
1530 /// // 2023-03-09 00:00:00.
1531 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1532 /// let next = tz.following(start).next().unwrap();
1533 /// assert_eq!(next.offset(), offset(-4));
1534 /// // Or go backwards to find the previous transition.
1535 /// let prev = tz.preceding(start).next().unwrap();
1536 /// assert_eq!(prev.offset(), offset(-5));
1537 ///
1538 /// # Ok::<(), Box<dyn std::error::Error>>(())
1539 /// ```
1540 #[inline]
1541 pub fn offset(&self) -> Offset {
1542 self.offset
1543 }
1544
1545 /// Returns the time zone abbreviation corresponding to this time
1546 /// zone transition. All instants at and following this transition's
1547 /// timestamp (and before the next transition's timestamp) may use this
1548 /// abbreviation when creating a human readable string. For example,
1549 /// this is the abbreviation used with the `%Z` specifier with Jiff's
1550 /// [`fmt::strtime`](crate::fmt::strtime) module.
1551 ///
1552 /// Note that abbreviations can to be ambiguous. For example, the
1553 /// abbreviation `CST` can be used for the time zones `Asia/Shanghai`,
1554 /// `America/Chicago` and `America/Havana`.
1555 ///
1556 /// The lifetime of the string returned is tied to this
1557 /// `TimeZoneTransition`, which may be shorter than `'t` (the lifetime of
1558 /// the time zone this transition was created from).
1559 ///
1560 /// # Example
1561 ///
1562 /// ```
1563 /// use jiff::{civil, tz::TimeZone};
1564 ///
1565 /// let tz = TimeZone::get("US/Eastern")?;
1566 /// // Get the abbreviation of the next transition after
1567 /// // 2023-03-09 00:00:00.
1568 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1569 /// let next = tz.following(start).next().unwrap();
1570 /// assert_eq!(next.abbreviation(), "EDT");
1571 /// // Or go backwards to find the previous transition.
1572 /// let prev = tz.preceding(start).next().unwrap();
1573 /// assert_eq!(prev.abbreviation(), "EST");
1574 ///
1575 /// # Ok::<(), Box<dyn std::error::Error>>(())
1576 /// ```
1577 #[inline]
1578 pub fn abbreviation<'a>(&'a self) -> &'a str {
1579 self.abbrev
1580 }
1581
1582 /// Returns whether daylight saving time is enabled for this time zone
1583 /// transition.
1584 ///
1585 /// Callers should generally treat this as informational only. In
1586 /// particular, not all time zone transitions are related to daylight
1587 /// saving time. For example, some transitions are a result of a region
1588 /// permanently changing their offset from UTC.
1589 ///
1590 /// # Example
1591 ///
1592 /// ```
1593 /// use jiff::{civil, tz::{Dst, TimeZone}};
1594 ///
1595 /// let tz = TimeZone::get("US/Eastern")?;
1596 /// // Get the DST status of the next transition after
1597 /// // 2023-03-09 00:00:00.
1598 /// let start = civil::date(2024, 3, 9).to_zoned(tz.clone())?.timestamp();
1599 /// let next = tz.following(start).next().unwrap();
1600 /// assert_eq!(next.dst(), Dst::Yes);
1601 /// // Or go backwards to find the previous transition.
1602 /// let prev = tz.preceding(start).next().unwrap();
1603 /// assert_eq!(prev.dst(), Dst::No);
1604 ///
1605 /// # Ok::<(), Box<dyn std::error::Error>>(())
1606 /// ```
1607 #[inline]
1608 pub fn dst(&self) -> Dst {
1609 self.dst
1610 }
1611}
1612
1613/// An offset along with DST status and a time zone abbreviation.
1614///
1615/// This information can be computed from a [`TimeZone`] given a [`Timestamp`]
1616/// via [`TimeZone::to_offset_info`].
1617///
1618/// Generally, the extra information associated with the offset is not commonly
1619/// needed. And indeed, inspecting the daylight saving time status of a
1620/// particular instant in a time zone _usually_ leads to bugs. For example, not
1621/// all time zone transitions are the result of daylight saving time. Some are
1622/// the result of permanent changes to the standard UTC offset of a region.
1623///
1624/// This information is available via an API distinct from
1625/// [`TimeZone::to_offset`] because it is not commonly needed and because it
1626/// can sometimes be more expensive to compute.
1627///
1628/// The main use case for daylight saving time status or time zone
1629/// abbreviations is for formatting datetimes in an end user's locale. If you
1630/// want this, consider using the [`icu`] crate via [`jiff-icu`].
1631///
1632/// The lifetime parameter `'t` corresponds to the lifetime of the `TimeZone`
1633/// that this info was extracted from.
1634///
1635/// # Example
1636///
1637/// ```
1638/// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
1639///
1640/// let tz = TimeZone::get("America/New_York")?;
1641///
1642/// // A timestamp in DST in New York.
1643/// let ts = Timestamp::from_second(1_720_493_204)?;
1644/// let info = tz.to_offset_info(ts);
1645/// assert_eq!(info.offset(), tz::offset(-4));
1646/// assert_eq!(info.dst(), Dst::Yes);
1647/// assert_eq!(info.abbreviation(), "EDT");
1648/// assert_eq!(
1649/// info.offset().to_datetime(ts).to_string(),
1650/// "2024-07-08T22:46:44",
1651/// );
1652///
1653/// // A timestamp *not* in DST in New York.
1654/// let ts = Timestamp::from_second(1_704_941_204)?;
1655/// let info = tz.to_offset_info(ts);
1656/// assert_eq!(info.offset(), tz::offset(-5));
1657/// assert_eq!(info.dst(), Dst::No);
1658/// assert_eq!(info.abbreviation(), "EST");
1659/// assert_eq!(
1660/// info.offset().to_datetime(ts).to_string(),
1661/// "2024-01-10T21:46:44",
1662/// );
1663///
1664/// # Ok::<(), Box<dyn std::error::Error>>(())
1665/// ```
1666///
1667/// [`icu`]: https://docs.rs/icu
1668/// [`jiff-icu`]: https://docs.rs/jiff-icu
1669#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1670pub struct TimeZoneOffsetInfo<'t> {
1671 pub(crate) offset: Offset,
1672 pub(crate) dst: Dst,
1673 pub(crate) abbreviation: TimeZoneAbbreviation<'t>,
1674}
1675
1676impl<'t> TimeZoneOffsetInfo<'t> {
1677 /// Returns the offset.
1678 ///
1679 /// The offset is duration, from UTC, that should be used to offset the
1680 /// civil time in a particular location.
1681 ///
1682 /// # Example
1683 ///
1684 /// ```
1685 /// use jiff::{civil, tz::{TimeZone, offset}};
1686 ///
1687 /// let tz = TimeZone::get("US/Eastern")?;
1688 /// // Get the offset for 2023-03-10 00:00:00.
1689 /// let start = civil::date(2024, 3, 10).to_zoned(tz.clone())?.timestamp();
1690 /// let info = tz.to_offset_info(start);
1691 /// assert_eq!(info.offset(), offset(-5));
1692 /// // Go forward a day and notice the offset changes due to DST!
1693 /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
1694 /// let info = tz.to_offset_info(start);
1695 /// assert_eq!(info.offset(), offset(-4));
1696 ///
1697 /// # Ok::<(), Box<dyn std::error::Error>>(())
1698 /// ```
1699 #[inline]
1700 pub fn offset(&self) -> Offset {
1701 self.offset
1702 }
1703
1704 /// Returns the time zone abbreviation corresponding to this offset info.
1705 ///
1706 /// Note that abbreviations can to be ambiguous. For example, the
1707 /// abbreviation `CST` can be used for the time zones `Asia/Shanghai`,
1708 /// `America/Chicago` and `America/Havana`.
1709 ///
1710 /// The lifetime of the string returned is tied to this
1711 /// `TimeZoneOffsetInfo`, which may be shorter than `'t` (the lifetime of
1712 /// the time zone this transition was created from).
1713 ///
1714 /// # Example
1715 ///
1716 /// ```
1717 /// use jiff::{civil, tz::TimeZone};
1718 ///
1719 /// let tz = TimeZone::get("US/Eastern")?;
1720 /// // Get the time zone abbreviation for 2023-03-10 00:00:00.
1721 /// let start = civil::date(2024, 3, 10).to_zoned(tz.clone())?.timestamp();
1722 /// let info = tz.to_offset_info(start);
1723 /// assert_eq!(info.abbreviation(), "EST");
1724 /// // Go forward a day and notice the abbreviation changes due to DST!
1725 /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
1726 /// let info = tz.to_offset_info(start);
1727 /// assert_eq!(info.abbreviation(), "EDT");
1728 ///
1729 /// # Ok::<(), Box<dyn std::error::Error>>(())
1730 /// ```
1731 #[inline]
1732 pub fn abbreviation(&self) -> &str {
1733 self.abbreviation.as_str()
1734 }
1735
1736 /// Returns whether daylight saving time is enabled for this offset
1737 /// info.
1738 ///
1739 /// Callers should generally treat this as informational only. In
1740 /// particular, not all time zone transitions are related to daylight
1741 /// saving time. For example, some transitions are a result of a region
1742 /// permanently changing their offset from UTC.
1743 ///
1744 /// # Example
1745 ///
1746 /// ```
1747 /// use jiff::{civil, tz::{Dst, TimeZone}};
1748 ///
1749 /// let tz = TimeZone::get("US/Eastern")?;
1750 /// // Get the DST status of 2023-03-11 00:00:00.
1751 /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
1752 /// let info = tz.to_offset_info(start);
1753 /// assert_eq!(info.dst(), Dst::Yes);
1754 ///
1755 /// # Ok::<(), Box<dyn std::error::Error>>(())
1756 /// ```
1757 #[inline]
1758 pub fn dst(&self) -> Dst {
1759 self.dst
1760 }
1761}
1762
1763/// An iterator over time zone transitions going backward in time.
1764///
1765/// This iterator is created by [`TimeZone::preceding`].
1766///
1767/// # Example: show the 5 previous time zone transitions
1768///
1769/// This shows how to find the 5 preceding time zone transitions (from a
1770/// particular datetime) for a particular time zone:
1771///
1772/// ```
1773/// use jiff::{tz::offset, Zoned};
1774///
1775/// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1776/// let transitions = now
1777/// .time_zone()
1778/// .preceding(now.timestamp())
1779/// .take(5)
1780/// .map(|t| (
1781/// t.timestamp().to_zoned(now.time_zone().clone()),
1782/// t.offset(),
1783/// t.abbreviation().to_string(),
1784/// ))
1785/// .collect::<Vec<_>>();
1786/// assert_eq!(transitions, vec![
1787/// ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1788/// ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1789/// ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1790/// ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1791/// ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1792/// ]);
1793///
1794/// # Ok::<(), Box<dyn std::error::Error>>(())
1795/// ```
1796#[derive(Clone, Debug)]
1797pub struct TimeZonePrecedingTransitions<'t> {
1798 tz: &'t TimeZone,
1799 cur: Timestamp,
1800}
1801
1802impl<'t> Iterator for TimeZonePrecedingTransitions<'t> {
1803 type Item = TimeZoneTransition<'t>;
1804
1805 fn next(&mut self) -> Option<TimeZoneTransition<'t>> {
1806 let trans = self.tz.previous_transition(self.cur)?;
1807 self.cur = trans.timestamp();
1808 Some(trans)
1809 }
1810}
1811
1812impl<'t> core::iter::FusedIterator for TimeZonePrecedingTransitions<'t> {}
1813
1814/// An iterator over time zone transitions going forward in time.
1815///
1816/// This iterator is created by [`TimeZone::following`].
1817///
1818/// # Example: show the 5 next time zone transitions
1819///
1820/// This shows how to find the 5 following time zone transitions (from a
1821/// particular datetime) for a particular time zone:
1822///
1823/// ```
1824/// use jiff::{tz::offset, Zoned};
1825///
1826/// let now: Zoned = "2024-12-31 18:25-05[US/Eastern]".parse()?;
1827/// let transitions = now
1828/// .time_zone()
1829/// .following(now.timestamp())
1830/// .take(5)
1831/// .map(|t| (
1832/// t.timestamp().to_zoned(now.time_zone().clone()),
1833/// t.offset(),
1834/// t.abbreviation().to_string(),
1835/// ))
1836/// .collect::<Vec<_>>();
1837/// assert_eq!(transitions, vec![
1838/// ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1839/// ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1840/// ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1841/// ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
1842/// ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
1843/// ]);
1844///
1845/// # Ok::<(), Box<dyn std::error::Error>>(())
1846/// ```
1847#[derive(Clone, Debug)]
1848pub struct TimeZoneFollowingTransitions<'t> {
1849 tz: &'t TimeZone,
1850 cur: Timestamp,
1851}
1852
1853impl<'t> Iterator for TimeZoneFollowingTransitions<'t> {
1854 type Item = TimeZoneTransition<'t>;
1855
1856 fn next(&mut self) -> Option<TimeZoneTransition<'t>> {
1857 let trans = self.tz.next_transition(self.cur)?;
1858 self.cur = trans.timestamp();
1859 Some(trans)
1860 }
1861}
1862
1863impl<'t> core::iter::FusedIterator for TimeZoneFollowingTransitions<'t> {}
1864
1865/// A helper type for converting a `TimeZone` to a succinct human readable
1866/// description.
1867///
1868/// This is principally used in error messages in various places.
1869///
1870/// A previous iteration of this was just an `as_str() -> &str` method on
1871/// `TimeZone`, but that's difficult to do without relying on dynamic memory
1872/// allocation (or chunky arrays).
1873pub(crate) struct DiagnosticName<'a>(&'a TimeZone);
1874
1875impl<'a> core::fmt::Display for DiagnosticName<'a> {
1876 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1877 repr::each! {
1878 &self.0.repr,
1879 UTC => write!(f, "UTC"),
1880 UNKNOWN => write!(f, "Etc/Unknown"),
1881 FIXED(offset) => write!(f, "{offset}"),
1882 STATIC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
1883 ARC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
1884 ARC_POSIX(posix) => write!(f, "{posix}"),
1885 }
1886 }
1887}
1888
1889/// A light abstraction over different representations of a time zone
1890/// abbreviation.
1891///
1892/// The lifetime parameter `'t` corresponds to the lifetime of the time zone
1893/// that produced this abbreviation.
1894#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
1895pub(crate) enum TimeZoneAbbreviation<'t> {
1896 /// For when the abbreviation is borrowed directly from other data. For
1897 /// example, from TZif or from POSIX TZ strings.
1898 Borrowed(&'t str),
1899 /// For when the abbreviation has to be derived from other data. For
1900 /// example, from a fixed offset.
1901 ///
1902 /// The idea here is that a `TimeZone` shouldn't need to store the
1903 /// string representation of a fixed offset. Particularly in core-only
1904 /// environments, this is quite wasteful. So we make the string on-demand
1905 /// only when it's requested.
1906 ///
1907 /// An alternative design is to just implement `Display` and reuse
1908 /// `Offset`'s `Display` impl, but then we couldn't offer a `-> &str` API.
1909 /// I feel like that's just a bit overkill, and really just comes from the
1910 /// core-only straight-jacket.
1911 Owned(ArrayStr<9>),
1912}
1913
1914impl<'t> TimeZoneAbbreviation<'t> {
1915 /// Returns this abbreviation as a string borrowed from `self`.
1916 ///
1917 /// Notice that, like `Cow`, the lifetime of the string returned is
1918 /// tied to `self` and thus may be shorter than `'t`.
1919 fn as_str<'a>(&'a self) -> &'a str {
1920 match *self {
1921 TimeZoneAbbreviation::Borrowed(s) => s,
1922 TimeZoneAbbreviation::Owned(ref s) => s.as_str(),
1923 }
1924 }
1925}
1926
1927/// This module defines the internal representation of a `TimeZone`.
1928///
1929/// This module exists to _encapsulate_ the representation rigorously and
1930/// expose a safe and sound API.
1931mod repr {
1932 use core::mem::ManuallyDrop;
1933
1934 use crate::{
1935 tz::tzif::TzifStatic,
1936 util::{constant::unwrap, t},
1937 };
1938 #[cfg(feature = "alloc")]
1939 use crate::{
1940 tz::{posix::PosixTimeZoneOwned, tzif::TzifOwned},
1941 util::sync::Arc,
1942 };
1943
1944 use super::Offset;
1945
1946 // On Rust 1.84+, `StrictProvenancePolyfill` isn't actually used.
1947 #[allow(unused_imports)]
1948 use self::polyfill::{without_provenance, StrictProvenancePolyfill};
1949
1950 /// A macro for "matching" over the time zone representation variants.
1951 ///
1952 /// This macro is safe to use.
1953 ///
1954 /// Note that the `ARC_TZIF` and `ARC_POSIX` branches are automatically
1955 /// removed when `alloc` isn't enabled. Users of this macro needn't handle
1956 /// the `cfg` themselves.
1957 macro_rules! each {
1958 (
1959 $repr:expr,
1960 UTC => $utc:expr,
1961 UNKNOWN => $unknown:expr,
1962 FIXED($offset:ident) => $fixed:expr,
1963 STATIC_TZIF($static_tzif:ident) => $static_tzif_block:expr,
1964 ARC_TZIF($arc_tzif:ident) => $arc_tzif_block:expr,
1965 ARC_POSIX($arc_posix:ident) => $arc_posix_block:expr,
1966 ) => {{
1967 let repr = $repr;
1968 match repr.tag() {
1969 Repr::UTC => $utc,
1970 Repr::UNKNOWN => $unknown,
1971 Repr::FIXED => {
1972 // SAFETY: We've ensured our pointer tag is correct.
1973 let $offset = unsafe { repr.get_fixed() };
1974 $fixed
1975 }
1976 Repr::STATIC_TZIF => {
1977 // SAFETY: We've ensured our pointer tag is correct.
1978 let $static_tzif = unsafe { repr.get_static_tzif() };
1979 $static_tzif_block
1980 }
1981 #[cfg(feature = "alloc")]
1982 Repr::ARC_TZIF => {
1983 // SAFETY: We've ensured our pointer tag is correct.
1984 let $arc_tzif = unsafe { repr.get_arc_tzif() };
1985 $arc_tzif_block
1986 }
1987 #[cfg(feature = "alloc")]
1988 Repr::ARC_POSIX => {
1989 // SAFETY: We've ensured our pointer tag is correct.
1990 let $arc_posix = unsafe { repr.get_arc_posix() };
1991 $arc_posix_block
1992 }
1993 _ => {
1994 debug_assert!(false, "each: invalid time zone repr tag!");
1995 // SAFETY: The constructors for `Repr` guarantee that the
1996 // tag is always one of the values matched above.
1997 unsafe {
1998 core::hint::unreachable_unchecked();
1999 }
2000 }
2001 }
2002 }};
2003 }
2004 pub(super) use each;
2005
2006 /// The internal representation of a `TimeZone`.
2007 ///
2008 /// It has 6 different possible variants: `UTC`, `Etc/Unknown`, fixed
2009 /// offset, `static` TZif, `Arc` TZif or `Arc` POSIX time zone.
2010 ///
2011 /// This design uses pointer tagging so that:
2012 ///
2013 /// * The size of a `TimeZone` stays no bigger than a single word.
2014 /// * In core-only environments, a `TimeZone` can be created from
2015 /// compile-time TZif data without allocating.
2016 /// * UTC, unknown and fixed offset time zone does not require allocating.
2017 /// * We can still alloc for TZif and POSIX time zones created at runtime.
2018 /// (Allocating for TZif at runtime is the intended common case, and
2019 /// corresponds to reading `/usr/share/zoneinfo` entries.)
2020 ///
2021 /// We achieve this through pointer tagging and careful use of a strict
2022 /// provenance polyfill (because of MSRV). We use the lower 4 bits of a
2023 /// pointer to indicate which variant we have. This is sound because we
2024 /// require all types that we allocate for to have a minimum alignment of
2025 /// 8 bytes.
2026 pub(super) struct Repr {
2027 ptr: *const u8,
2028 }
2029
2030 impl Repr {
2031 const BITS: usize = 0b111;
2032 pub(super) const UTC: usize = 1;
2033 pub(super) const UNKNOWN: usize = 2;
2034 pub(super) const FIXED: usize = 3;
2035 pub(super) const STATIC_TZIF: usize = 0;
2036 pub(super) const ARC_TZIF: usize = 4;
2037 pub(super) const ARC_POSIX: usize = 5;
2038
2039 // The minimum alignment required for any heap allocated time zone
2040 // variants. This is related to the number of tags. We have 6 distinct
2041 // values above, which means we need an alignment of at least 6. Since
2042 // alignment must be a power of 2, the smallest possible alignment
2043 // is 8.
2044 const ALIGN: usize = 8;
2045
2046 /// Creates a representation for a `UTC` time zone.
2047 #[inline]
2048 pub(super) const fn utc() -> Repr {
2049 let ptr = without_provenance(Repr::UTC);
2050 Repr { ptr }
2051 }
2052
2053 /// Creates a representation for a `Etc/Unknown` time zone.
2054 #[inline]
2055 pub(super) const fn unknown() -> Repr {
2056 let ptr = without_provenance(Repr::UNKNOWN);
2057 Repr { ptr }
2058 }
2059
2060 /// Creates a representation for a fixed offset time zone.
2061 #[inline]
2062 pub(super) const fn fixed(offset: Offset) -> Repr {
2063 let seconds = offset.seconds_ranged().get_unchecked();
2064 // OK because offset is in -93599..=93599.
2065 let shifted = unwrap!(
2066 seconds.checked_shl(4),
2067 "offset small enough for left shift by 4 bits",
2068 );
2069 assert!(usize::MAX >= 4_294_967_295);
2070 // usize cast is okay because Jiff requires 32-bit.
2071 let ptr = without_provenance((shifted as usize) | Repr::FIXED);
2072 Repr { ptr }
2073 }
2074
2075 /// Creates a representation for a created-at-compile-time TZif time
2076 /// zone.
2077 ///
2078 /// This can only be correctly called by the `jiff-static` proc macro.
2079 #[inline]
2080 pub(super) const fn static_tzif(tzif: &'static TzifStatic) -> Repr {
2081 assert!(core::mem::align_of::<TzifStatic>() >= Repr::ALIGN);
2082 let tzif = (tzif as *const TzifStatic).cast::<u8>();
2083 // We very specifically do no materialize the pointer address here
2084 // because 1) it's UB and 2) the compiler generally prevents. This
2085 // is because in a const context, the specific pointer address
2086 // cannot be relied upon. Yet, we still want to do pointer tagging.
2087 //
2088 // Thankfully, this is the only variant that is a pointer that
2089 // we want to create in a const context. So we just make this
2090 // variant's tag `0`, and thus, no explicit pointer tagging is
2091 // required. (Becuase we ensure the alignment is at least 4, and
2092 // thus the least significant 3 bits are 0.)
2093 //
2094 // If this ends up not working out or if we need to support
2095 // another `static` variant, then we could perhaps to pointer
2096 // tagging with pointer arithmetic (like what the `tagged-pointer`
2097 // crate does). I haven't tried it though and I'm unclear if it
2098 // work.
2099 Repr { ptr: tzif }
2100 }
2101
2102 /// Creates a representation for a TZif time zone.
2103 #[cfg(feature = "alloc")]
2104 #[inline]
2105 pub(super) fn arc_tzif(tzif: Arc<TzifOwned>) -> Repr {
2106 assert!(core::mem::align_of::<TzifOwned>() >= Repr::ALIGN);
2107 let tzif = Arc::into_raw(tzif).cast::<u8>();
2108 assert!(tzif.addr() % 4 == 0);
2109 let ptr = tzif.map_addr(|addr| addr | Repr::ARC_TZIF);
2110 Repr { ptr }
2111 }
2112
2113 /// Creates a representation for a POSIX time zone.
2114 #[cfg(feature = "alloc")]
2115 #[inline]
2116 pub(super) fn arc_posix(posix_tz: Arc<PosixTimeZoneOwned>) -> Repr {
2117 assert!(
2118 core::mem::align_of::<PosixTimeZoneOwned>() >= Repr::ALIGN
2119 );
2120 let posix_tz = Arc::into_raw(posix_tz).cast::<u8>();
2121 assert!(posix_tz.addr() % 4 == 0);
2122 let ptr = posix_tz.map_addr(|addr| addr | Repr::ARC_POSIX);
2123 Repr { ptr }
2124 }
2125
2126 /// Gets the offset representation.
2127 ///
2128 /// # Safety
2129 ///
2130 /// Callers must ensure that the pointer tag is `FIXED`.
2131 #[inline]
2132 pub(super) unsafe fn get_fixed(&self) -> Offset {
2133 #[allow(unstable_name_collisions)]
2134 let addr = self.ptr.addr();
2135 // NOTE: Because of sign extension, we need to case to `i32`
2136 // before shifting.
2137 let seconds = t::SpanZoneOffset::new_unchecked((addr as i32) >> 4);
2138 Offset::from_seconds_ranged(seconds)
2139 }
2140
2141 /// Returns true if and only if this representation corresponds to the
2142 /// `Etc/Unknown` time zone.
2143 #[inline]
2144 pub(super) fn is_unknown(&self) -> bool {
2145 self.tag() == Repr::UNKNOWN
2146 }
2147
2148 /// Gets the static TZif representation.
2149 ///
2150 /// # Safety
2151 ///
2152 /// Callers must ensure that the pointer tag is `STATIC_TZIF`.
2153 #[inline]
2154 pub(super) unsafe fn get_static_tzif(&self) -> &'static TzifStatic {
2155 #[allow(unstable_name_collisions)]
2156 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2157 // SAFETY: Getting a `STATIC_TZIF` tag is only possible when
2158 // `self.ptr` was constructed from a valid and aligned (to at least
2159 // 4 bytes) `&TzifStatic` borrow. Which must be guaranteed by the
2160 // caller. We've also removed the tag bits above, so we must now
2161 // have the original pointer.
2162 unsafe { &*ptr.cast::<TzifStatic>() }
2163 }
2164
2165 /// Gets the `Arc` TZif representation.
2166 ///
2167 /// # Safety
2168 ///
2169 /// Callers must ensure that the pointer tag is `ARC_TZIF`.
2170 #[cfg(feature = "alloc")]
2171 #[inline]
2172 pub(super) unsafe fn get_arc_tzif<'a>(&'a self) -> &'a TzifOwned {
2173 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2174 // SAFETY: Getting a `ARC_TZIF` tag is only possible when
2175 // `self.ptr` was constructed from a valid and aligned
2176 // (to at least 4 bytes) `Arc<TzifOwned>`. We've removed
2177 // the tag bits above, so we must now have the original
2178 // pointer.
2179 let arc = ManuallyDrop::new(unsafe {
2180 Arc::from_raw(ptr.cast::<TzifOwned>())
2181 });
2182 // SAFETY: The lifetime of the pointer returned is always
2183 // valid as long as the strong count on `arc` is at least
2184 // 1. Since the lifetime is no longer than `Repr` itself,
2185 // and a `Repr` being alive implies there is at least 1
2186 // for the strong `Arc` count, it follows that the lifetime
2187 // returned here is correct.
2188 unsafe { &*Arc::as_ptr(&arc) }
2189 }
2190
2191 /// Gets the `Arc` POSIX time zone representation.
2192 ///
2193 /// # Safety
2194 ///
2195 /// Callers must ensure that the pointer tag is `ARC_POSIX`.
2196 #[cfg(feature = "alloc")]
2197 #[inline]
2198 pub(super) unsafe fn get_arc_posix<'a>(
2199 &'a self,
2200 ) -> &'a PosixTimeZoneOwned {
2201 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2202 // SAFETY: Getting a `ARC_POSIX` tag is only possible when
2203 // `self.ptr` was constructed from a valid and aligned (to at least
2204 // 4 bytes) `Arc<PosixTimeZoneOwned>`. We've removed the tag
2205 // bits above, so we must now have the original pointer.
2206 let arc = ManuallyDrop::new(unsafe {
2207 Arc::from_raw(ptr.cast::<PosixTimeZoneOwned>())
2208 });
2209 // SAFETY: The lifetime of the pointer returned is always
2210 // valid as long as the strong count on `arc` is at least
2211 // 1. Since the lifetime is no longer than `Repr` itself,
2212 // and a `Repr` being alive implies there is at least 1
2213 // for the strong `Arc` count, it follows that the lifetime
2214 // returned here is correct.
2215 unsafe { &*Arc::as_ptr(&arc) }
2216 }
2217
2218 /// Returns the tag on the representation's pointer.
2219 ///
2220 /// The value is guaranteed to be one of the constant tag values.
2221 #[inline]
2222 pub(super) fn tag(&self) -> usize {
2223 #[allow(unstable_name_collisions)]
2224 {
2225 self.ptr.addr() & Repr::BITS
2226 }
2227 }
2228
2229 /// Returns a dumb copy of this representation.
2230 ///
2231 /// # Safety
2232 ///
2233 /// Callers must ensure that this representation's tag is UTC,
2234 /// UNKNOWN, FIXED or STATIC_TZIF.
2235 ///
2236 /// Namely, this specifically does not increment the ref count for
2237 /// the `Arc` pointers when the tag is `ARC_TZIF` or `ARC_POSIX`.
2238 /// This means that incorrect usage of this routine can lead to
2239 /// use-after-free.
2240 ///
2241 /// NOTE: It would be nice if we could make this `copy` routine safe,
2242 /// or at least panic if it's misused. But to do that, you need to know
2243 /// the time zone variant. And to know the time zone variant, you need
2244 /// to "look" at the tag in the pointer. And looking at the address of
2245 /// a pointer in a `const` context is precarious.
2246 #[inline]
2247 pub(super) const unsafe fn copy(&self) -> Repr {
2248 Repr { ptr: self.ptr }
2249 }
2250 }
2251
2252 // SAFETY: We use automic reference counting.
2253 unsafe impl Send for Repr {}
2254 // SAFETY: We don't use an interior mutability and otherwise don't permit
2255 // any kind of mutation (other than for an `Arc` managing its ref counts)
2256 // of a `Repr`.
2257 unsafe impl Sync for Repr {}
2258
2259 impl core::fmt::Debug for Repr {
2260 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
2261 each! {
2262 self,
2263 UTC => write!(f, "UTC"),
2264 UNKNOWN => write!(f, "Etc/Unknown"),
2265 FIXED(offset) => write!(f, "{offset:?}"),
2266 STATIC_TZIF(tzif) => {
2267 // The full debug output is a bit much, so constrain it.
2268 let field = tzif.name().unwrap_or("Local");
2269 f.debug_tuple("TZif").field(&field).finish()
2270 },
2271 ARC_TZIF(tzif) => {
2272 // The full debug output is a bit much, so constrain it.
2273 let field = tzif.name().unwrap_or("Local");
2274 f.debug_tuple("TZif").field(&field).finish()
2275 },
2276 ARC_POSIX(posix) => write!(f, "Posix({posix})"),
2277 }
2278 }
2279 }
2280
2281 impl Clone for Repr {
2282 #[inline]
2283 fn clone(&self) -> Repr {
2284 // This `match` is written in an exhaustive fashion so that if
2285 // a new tag is added, it should be explicitly considered here.
2286 match self.tag() {
2287 // These are all `Copy` and can just be memcpy'd as-is.
2288 Repr::UTC
2289 | Repr::UNKNOWN
2290 | Repr::FIXED
2291 | Repr::STATIC_TZIF => Repr { ptr: self.ptr },
2292 #[cfg(feature = "alloc")]
2293 Repr::ARC_TZIF => {
2294 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2295 // SAFETY: Getting a `ARC_TZIF` tag is only possible when
2296 // `self.ptr` was constructed from a valid and aligned
2297 // (to at least 4 bytes) `Arc<TzifOwned>`. We've removed
2298 // the tag bits above, so we must now have the original
2299 // pointer.
2300 unsafe {
2301 Arc::increment_strong_count(ptr.cast::<TzifOwned>());
2302 }
2303 Repr { ptr: self.ptr }
2304 }
2305 #[cfg(feature = "alloc")]
2306 Repr::ARC_POSIX => {
2307 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2308 // SAFETY: Getting a `ARC_POSIX` tag is only possible when
2309 // `self.ptr` was constructed from a valid and aligned (to
2310 // at least 4 bytes) `Arc<PosixTimeZoneOwned>`. We've
2311 // removed the tag bits above, so we must now have the
2312 // original pointer.
2313 unsafe {
2314 Arc::increment_strong_count(
2315 ptr.cast::<PosixTimeZoneOwned>(),
2316 );
2317 }
2318 Repr { ptr: self.ptr }
2319 }
2320 _ => {
2321 debug_assert!(false, "clone: invalid time zone repr tag!");
2322 // SAFETY: The constructors for `Repr` guarantee that the
2323 // tag is always one of the values matched above.
2324 unsafe {
2325 core::hint::unreachable_unchecked();
2326 }
2327 }
2328 }
2329 }
2330 }
2331
2332 impl Drop for Repr {
2333 #[inline]
2334 fn drop(&mut self) {
2335 // This `match` is written in an exhaustive fashion so that if
2336 // a new tag is added, it should be explicitly considered here.
2337 match self.tag() {
2338 // These are all `Copy` and have no destructor.
2339 Repr::UTC
2340 | Repr::UNKNOWN
2341 | Repr::FIXED
2342 | Repr::STATIC_TZIF => {}
2343 #[cfg(feature = "alloc")]
2344 Repr::ARC_TZIF => {
2345 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2346 // SAFETY: Getting a `ARC_TZIF` tag is only possible when
2347 // `self.ptr` was constructed from a valid and aligned
2348 // (to at least 4 bytes) `Arc<TzifOwned>`. We've removed
2349 // the tag bits above, so we must now have the original
2350 // pointer.
2351 unsafe {
2352 Arc::decrement_strong_count(ptr.cast::<TzifOwned>());
2353 }
2354 }
2355 #[cfg(feature = "alloc")]
2356 Repr::ARC_POSIX => {
2357 let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
2358 // SAFETY: Getting a `ARC_POSIX` tag is only possible when
2359 // `self.ptr` was constructed from a valid and aligned (to
2360 // at least 4 bytes) `Arc<PosixTimeZoneOwned>`. We've
2361 // removed the tag bits above, so we must now have the
2362 // original pointer.
2363 unsafe {
2364 Arc::decrement_strong_count(
2365 ptr.cast::<PosixTimeZoneOwned>(),
2366 );
2367 }
2368 }
2369 _ => {
2370 debug_assert!(false, "drop: invalid time zone repr tag!");
2371 // SAFETY: The constructors for `Repr` guarantee that the
2372 // tag is always one of the values matched above.
2373 unsafe {
2374 core::hint::unreachable_unchecked();
2375 }
2376 }
2377 }
2378 }
2379 }
2380
2381 impl Eq for Repr {}
2382
2383 impl PartialEq for Repr {
2384 fn eq(&self, other: &Repr) -> bool {
2385 if self.tag() != other.tag() {
2386 return false;
2387 }
2388 each! {
2389 self,
2390 UTC => true,
2391 UNKNOWN => true,
2392 // SAFETY: OK, because we know the tags are equivalent and
2393 // `self` has a `FIXED` tag.
2394 FIXED(offset) => offset == unsafe { other.get_fixed() },
2395 // SAFETY: OK, because we know the tags are equivalent and
2396 // `self` has a `STATIC_TZIF` tag.
2397 STATIC_TZIF(tzif) => tzif == unsafe { other.get_static_tzif() },
2398 // SAFETY: OK, because we know the tags are equivalent and
2399 // `self` has an `ARC_TZIF` tag.
2400 ARC_TZIF(tzif) => tzif == unsafe { other.get_arc_tzif() },
2401 // SAFETY: OK, because we know the tags are equivalent and
2402 // `self` has an `ARC_POSIX` tag.
2403 ARC_POSIX(posix) => posix == unsafe { other.get_arc_posix() },
2404 }
2405 }
2406 }
2407
2408 /// This is a polyfill for a small subset of std's strict provenance APIs.
2409 ///
2410 /// The strict provenance APIs in `core` were stabilized in Rust 1.84,
2411 /// but it will likely be a while before Jiff can use them. (At time of
2412 /// writing, 2025-02-24, Jiff's MSRV is Rust 1.70.)
2413 ///
2414 /// The `const` requirement is also why these are non-generic free
2415 /// functions and not defined via an extension trait. It's also why we
2416 /// don't have the useful `map_addr` routine (which is directly relevant to
2417 /// our pointer tagging use case).
2418 mod polyfill {
2419 pub(super) const fn without_provenance(addr: usize) -> *const u8 {
2420 // SAFETY: Every valid `usize` is also a valid pointer (but not
2421 // necessarily legal to dereference).
2422 //
2423 // MSRV(1.84): We *really* ought to be using
2424 // `core::ptr::without_provenance` here, but Jiff's MSRV prevents
2425 // us.
2426 unsafe { core::mem::transmute(addr) }
2427 }
2428
2429 // On Rust 1.84+, `StrictProvenancePolyfill` isn't actually used.
2430 #[allow(dead_code)]
2431 pub(super) trait StrictProvenancePolyfill:
2432 Sized + Clone + Copy
2433 {
2434 fn addr(&self) -> usize;
2435 fn with_addr(&self, addr: usize) -> Self;
2436 fn map_addr(&self, map: impl FnOnce(usize) -> usize) -> Self {
2437 self.with_addr(map(self.addr()))
2438 }
2439 }
2440
2441 impl StrictProvenancePolyfill for *const u8 {
2442 fn addr(&self) -> usize {
2443 // SAFETY: Pointer-to-integer transmutes are valid (if you are
2444 // okay with losing the provenance).
2445 //
2446 // The implementation in std says that this isn't guaranteed to
2447 // be sound outside of std, but I'm not sure how else to do it.
2448 // In practice, this seems likely fine?
2449 unsafe { core::mem::transmute(self.cast::<()>()) }
2450 }
2451
2452 fn with_addr(&self, address: usize) -> Self {
2453 let self_addr = self.addr() as isize;
2454 let dest_addr = address as isize;
2455 let offset = dest_addr.wrapping_sub(self_addr);
2456 self.wrapping_offset(offset)
2457 }
2458 }
2459 }
2460}
2461
2462#[cfg(test)]
2463mod tests {
2464 #[cfg(feature = "alloc")]
2465 use crate::tz::testdata::TzifTestFile;
2466 use crate::{civil::date, tz::offset};
2467
2468 use super::*;
2469
2470 fn unambiguous(offset_hours: i8) -> AmbiguousOffset {
2471 let offset = offset(offset_hours);
2472 o_unambiguous(offset)
2473 }
2474
2475 fn gap(
2476 earlier_offset_hours: i8,
2477 later_offset_hours: i8,
2478 ) -> AmbiguousOffset {
2479 let earlier = offset(earlier_offset_hours);
2480 let later = offset(later_offset_hours);
2481 o_gap(earlier, later)
2482 }
2483
2484 fn fold(
2485 earlier_offset_hours: i8,
2486 later_offset_hours: i8,
2487 ) -> AmbiguousOffset {
2488 let earlier = offset(earlier_offset_hours);
2489 let later = offset(later_offset_hours);
2490 o_fold(earlier, later)
2491 }
2492
2493 fn o_unambiguous(offset: Offset) -> AmbiguousOffset {
2494 AmbiguousOffset::Unambiguous { offset }
2495 }
2496
2497 fn o_gap(earlier: Offset, later: Offset) -> AmbiguousOffset {
2498 AmbiguousOffset::Gap { before: earlier, after: later }
2499 }
2500
2501 fn o_fold(earlier: Offset, later: Offset) -> AmbiguousOffset {
2502 AmbiguousOffset::Fold { before: earlier, after: later }
2503 }
2504
2505 #[cfg(feature = "alloc")]
2506 #[test]
2507 fn time_zone_tzif_to_ambiguous_timestamp() {
2508 let tests: &[(&str, &[_])] = &[
2509 (
2510 "America/New_York",
2511 &[
2512 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
2513 ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
2514 ((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
2515 ((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
2516 ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
2517 ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
2518 ((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
2519 ((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
2520 ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
2521 ],
2522 ),
2523 (
2524 "Europe/Dublin",
2525 &[
2526 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(1)),
2527 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
2528 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
2529 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
2530 ((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
2531 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
2532 ((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
2533 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
2534 ((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
2535 ],
2536 ),
2537 (
2538 "Australia/Tasmania",
2539 &[
2540 ((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
2541 ((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
2542 ((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
2543 ((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
2544 ((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
2545 ((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
2546 ((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
2547 ((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
2548 ((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
2549 ],
2550 ),
2551 (
2552 "Antarctica/Troll",
2553 &[
2554 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
2555 // test the gap
2556 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
2557 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
2558 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
2559 // still in the gap!
2560 ((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
2561 ((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
2562 // finally out
2563 ((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
2564 // test the fold
2565 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
2566 ((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
2567 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
2568 // still in the fold!
2569 ((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
2570 ((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
2571 // finally out
2572 ((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
2573 ],
2574 ),
2575 (
2576 "America/St_Johns",
2577 &[
2578 (
2579 (1969, 12, 31, 20, 30, 0, 0),
2580 o_unambiguous(-Offset::hms(3, 30, 0)),
2581 ),
2582 (
2583 (2024, 3, 10, 1, 59, 59, 999_999_999),
2584 o_unambiguous(-Offset::hms(3, 30, 0)),
2585 ),
2586 (
2587 (2024, 3, 10, 2, 0, 0, 0),
2588 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
2589 ),
2590 (
2591 (2024, 3, 10, 2, 59, 59, 999_999_999),
2592 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
2593 ),
2594 (
2595 (2024, 3, 10, 3, 0, 0, 0),
2596 o_unambiguous(-Offset::hms(2, 30, 0)),
2597 ),
2598 (
2599 (2024, 11, 3, 0, 59, 59, 999_999_999),
2600 o_unambiguous(-Offset::hms(2, 30, 0)),
2601 ),
2602 (
2603 (2024, 11, 3, 1, 0, 0, 0),
2604 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
2605 ),
2606 (
2607 (2024, 11, 3, 1, 59, 59, 999_999_999),
2608 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
2609 ),
2610 (
2611 (2024, 11, 3, 2, 0, 0, 0),
2612 o_unambiguous(-Offset::hms(3, 30, 0)),
2613 ),
2614 ],
2615 ),
2616 // This time zone has an interesting transition where it jumps
2617 // backwards a full day at 1867-10-19T15:30:00.
2618 (
2619 "America/Sitka",
2620 &[
2621 ((1969, 12, 31, 16, 0, 0, 0), unambiguous(-8)),
2622 (
2623 (-9999, 1, 2, 16, 58, 46, 0),
2624 o_unambiguous(Offset::hms(14, 58, 47)),
2625 ),
2626 (
2627 (1867, 10, 18, 15, 29, 59, 0),
2628 o_unambiguous(Offset::hms(14, 58, 47)),
2629 ),
2630 (
2631 (1867, 10, 18, 15, 30, 0, 0),
2632 // A fold of 24 hours!!!
2633 o_fold(
2634 Offset::hms(14, 58, 47),
2635 -Offset::hms(9, 1, 13),
2636 ),
2637 ),
2638 (
2639 (1867, 10, 19, 15, 29, 59, 999_999_999),
2640 // Still in the fold...
2641 o_fold(
2642 Offset::hms(14, 58, 47),
2643 -Offset::hms(9, 1, 13),
2644 ),
2645 ),
2646 (
2647 (1867, 10, 19, 15, 30, 0, 0),
2648 // Finally out.
2649 o_unambiguous(-Offset::hms(9, 1, 13)),
2650 ),
2651 ],
2652 ),
2653 // As with to_datetime, we test every possible transition
2654 // point here since this time zone has a small number of them.
2655 (
2656 "Pacific/Honolulu",
2657 &[
2658 (
2659 (1896, 1, 13, 11, 59, 59, 0),
2660 o_unambiguous(-Offset::hms(10, 31, 26)),
2661 ),
2662 (
2663 (1896, 1, 13, 12, 0, 0, 0),
2664 o_gap(
2665 -Offset::hms(10, 31, 26),
2666 -Offset::hms(10, 30, 0),
2667 ),
2668 ),
2669 (
2670 (1896, 1, 13, 12, 1, 25, 0),
2671 o_gap(
2672 -Offset::hms(10, 31, 26),
2673 -Offset::hms(10, 30, 0),
2674 ),
2675 ),
2676 (
2677 (1896, 1, 13, 12, 1, 26, 0),
2678 o_unambiguous(-Offset::hms(10, 30, 0)),
2679 ),
2680 (
2681 (1933, 4, 30, 1, 59, 59, 0),
2682 o_unambiguous(-Offset::hms(10, 30, 0)),
2683 ),
2684 (
2685 (1933, 4, 30, 2, 0, 0, 0),
2686 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
2687 ),
2688 (
2689 (1933, 4, 30, 2, 59, 59, 0),
2690 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
2691 ),
2692 (
2693 (1933, 4, 30, 3, 0, 0, 0),
2694 o_unambiguous(-Offset::hms(9, 30, 0)),
2695 ),
2696 (
2697 (1933, 5, 21, 10, 59, 59, 0),
2698 o_unambiguous(-Offset::hms(9, 30, 0)),
2699 ),
2700 (
2701 (1933, 5, 21, 11, 0, 0, 0),
2702 o_fold(
2703 -Offset::hms(9, 30, 0),
2704 -Offset::hms(10, 30, 0),
2705 ),
2706 ),
2707 (
2708 (1933, 5, 21, 11, 59, 59, 0),
2709 o_fold(
2710 -Offset::hms(9, 30, 0),
2711 -Offset::hms(10, 30, 0),
2712 ),
2713 ),
2714 (
2715 (1933, 5, 21, 12, 0, 0, 0),
2716 o_unambiguous(-Offset::hms(10, 30, 0)),
2717 ),
2718 (
2719 (1942, 2, 9, 1, 59, 59, 0),
2720 o_unambiguous(-Offset::hms(10, 30, 0)),
2721 ),
2722 (
2723 (1942, 2, 9, 2, 0, 0, 0),
2724 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
2725 ),
2726 (
2727 (1942, 2, 9, 2, 59, 59, 0),
2728 o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
2729 ),
2730 (
2731 (1942, 2, 9, 3, 0, 0, 0),
2732 o_unambiguous(-Offset::hms(9, 30, 0)),
2733 ),
2734 (
2735 (1945, 8, 14, 13, 29, 59, 0),
2736 o_unambiguous(-Offset::hms(9, 30, 0)),
2737 ),
2738 (
2739 (1945, 8, 14, 13, 30, 0, 0),
2740 o_unambiguous(-Offset::hms(9, 30, 0)),
2741 ),
2742 (
2743 (1945, 8, 14, 13, 30, 1, 0),
2744 o_unambiguous(-Offset::hms(9, 30, 0)),
2745 ),
2746 (
2747 (1945, 9, 30, 0, 59, 59, 0),
2748 o_unambiguous(-Offset::hms(9, 30, 0)),
2749 ),
2750 (
2751 (1945, 9, 30, 1, 0, 0, 0),
2752 o_fold(
2753 -Offset::hms(9, 30, 0),
2754 -Offset::hms(10, 30, 0),
2755 ),
2756 ),
2757 (
2758 (1945, 9, 30, 1, 59, 59, 0),
2759 o_fold(
2760 -Offset::hms(9, 30, 0),
2761 -Offset::hms(10, 30, 0),
2762 ),
2763 ),
2764 (
2765 (1945, 9, 30, 2, 0, 0, 0),
2766 o_unambiguous(-Offset::hms(10, 30, 0)),
2767 ),
2768 (
2769 (1947, 6, 8, 1, 59, 59, 0),
2770 o_unambiguous(-Offset::hms(10, 30, 0)),
2771 ),
2772 (
2773 (1947, 6, 8, 2, 0, 0, 0),
2774 o_gap(-Offset::hms(10, 30, 0), -offset(10)),
2775 ),
2776 (
2777 (1947, 6, 8, 2, 29, 59, 0),
2778 o_gap(-Offset::hms(10, 30, 0), -offset(10)),
2779 ),
2780 ((1947, 6, 8, 2, 30, 0, 0), unambiguous(-10)),
2781 ],
2782 ),
2783 ];
2784 for &(tzname, datetimes_to_ambiguous) in tests {
2785 let test_file = TzifTestFile::get(tzname);
2786 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
2787 for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
2788 let (year, month, day, hour, min, sec, nano) = datetime;
2789 let dt = date(year, month, day).at(hour, min, sec, nano);
2790 let got = tz.to_ambiguous_zoned(dt);
2791 assert_eq!(
2792 got.offset(),
2793 ambiguous_kind,
2794 "\nTZ: {tzname}\ndatetime: \
2795 {year:04}-{month:02}-{day:02}T\
2796 {hour:02}:{min:02}:{sec:02}.{nano:09}",
2797 );
2798 }
2799 }
2800 }
2801
2802 #[cfg(feature = "alloc")]
2803 #[test]
2804 fn time_zone_tzif_to_datetime() {
2805 let o = |hours| offset(hours);
2806 let tests: &[(&str, &[_])] = &[
2807 (
2808 "America/New_York",
2809 &[
2810 ((0, 0), o(-5), "EST", (1969, 12, 31, 19, 0, 0, 0)),
2811 (
2812 (1710052200, 0),
2813 o(-5),
2814 "EST",
2815 (2024, 3, 10, 1, 30, 0, 0),
2816 ),
2817 (
2818 (1710053999, 999_999_999),
2819 o(-5),
2820 "EST",
2821 (2024, 3, 10, 1, 59, 59, 999_999_999),
2822 ),
2823 ((1710054000, 0), o(-4), "EDT", (2024, 3, 10, 3, 0, 0, 0)),
2824 (
2825 (1710055800, 0),
2826 o(-4),
2827 "EDT",
2828 (2024, 3, 10, 3, 30, 0, 0),
2829 ),
2830 ((1730610000, 0), o(-4), "EDT", (2024, 11, 3, 1, 0, 0, 0)),
2831 (
2832 (1730611800, 0),
2833 o(-4),
2834 "EDT",
2835 (2024, 11, 3, 1, 30, 0, 0),
2836 ),
2837 (
2838 (1730613599, 999_999_999),
2839 o(-4),
2840 "EDT",
2841 (2024, 11, 3, 1, 59, 59, 999_999_999),
2842 ),
2843 ((1730613600, 0), o(-5), "EST", (2024, 11, 3, 1, 0, 0, 0)),
2844 (
2845 (1730615400, 0),
2846 o(-5),
2847 "EST",
2848 (2024, 11, 3, 1, 30, 0, 0),
2849 ),
2850 ],
2851 ),
2852 (
2853 "Australia/Tasmania",
2854 &[
2855 ((0, 0), o(11), "AEDT", (1970, 1, 1, 11, 0, 0, 0)),
2856 (
2857 (1728142200, 0),
2858 o(10),
2859 "AEST",
2860 (2024, 10, 6, 1, 30, 0, 0),
2861 ),
2862 (
2863 (1728143999, 999_999_999),
2864 o(10),
2865 "AEST",
2866 (2024, 10, 6, 1, 59, 59, 999_999_999),
2867 ),
2868 (
2869 (1728144000, 0),
2870 o(11),
2871 "AEDT",
2872 (2024, 10, 6, 3, 0, 0, 0),
2873 ),
2874 (
2875 (1728145800, 0),
2876 o(11),
2877 "AEDT",
2878 (2024, 10, 6, 3, 30, 0, 0),
2879 ),
2880 ((1712415600, 0), o(11), "AEDT", (2024, 4, 7, 2, 0, 0, 0)),
2881 (
2882 (1712417400, 0),
2883 o(11),
2884 "AEDT",
2885 (2024, 4, 7, 2, 30, 0, 0),
2886 ),
2887 (
2888 (1712419199, 999_999_999),
2889 o(11),
2890 "AEDT",
2891 (2024, 4, 7, 2, 59, 59, 999_999_999),
2892 ),
2893 ((1712419200, 0), o(10), "AEST", (2024, 4, 7, 2, 0, 0, 0)),
2894 (
2895 (1712421000, 0),
2896 o(10),
2897 "AEST",
2898 (2024, 4, 7, 2, 30, 0, 0),
2899 ),
2900 ],
2901 ),
2902 // Pacific/Honolulu is small eough that we just test every
2903 // possible instant before, at and after each transition.
2904 (
2905 "Pacific/Honolulu",
2906 &[
2907 (
2908 (-2334101315, 0),
2909 -Offset::hms(10, 31, 26),
2910 "LMT",
2911 (1896, 1, 13, 11, 59, 59, 0),
2912 ),
2913 (
2914 (-2334101314, 0),
2915 -Offset::hms(10, 30, 0),
2916 "HST",
2917 (1896, 1, 13, 12, 1, 26, 0),
2918 ),
2919 (
2920 (-2334101313, 0),
2921 -Offset::hms(10, 30, 0),
2922 "HST",
2923 (1896, 1, 13, 12, 1, 27, 0),
2924 ),
2925 (
2926 (-1157283001, 0),
2927 -Offset::hms(10, 30, 0),
2928 "HST",
2929 (1933, 4, 30, 1, 59, 59, 0),
2930 ),
2931 (
2932 (-1157283000, 0),
2933 -Offset::hms(9, 30, 0),
2934 "HDT",
2935 (1933, 4, 30, 3, 0, 0, 0),
2936 ),
2937 (
2938 (-1157282999, 0),
2939 -Offset::hms(9, 30, 0),
2940 "HDT",
2941 (1933, 4, 30, 3, 0, 1, 0),
2942 ),
2943 (
2944 (-1155436201, 0),
2945 -Offset::hms(9, 30, 0),
2946 "HDT",
2947 (1933, 5, 21, 11, 59, 59, 0),
2948 ),
2949 (
2950 (-1155436200, 0),
2951 -Offset::hms(10, 30, 0),
2952 "HST",
2953 (1933, 5, 21, 11, 0, 0, 0),
2954 ),
2955 (
2956 (-1155436199, 0),
2957 -Offset::hms(10, 30, 0),
2958 "HST",
2959 (1933, 5, 21, 11, 0, 1, 0),
2960 ),
2961 (
2962 (-880198201, 0),
2963 -Offset::hms(10, 30, 0),
2964 "HST",
2965 (1942, 2, 9, 1, 59, 59, 0),
2966 ),
2967 (
2968 (-880198200, 0),
2969 -Offset::hms(9, 30, 0),
2970 "HWT",
2971 (1942, 2, 9, 3, 0, 0, 0),
2972 ),
2973 (
2974 (-880198199, 0),
2975 -Offset::hms(9, 30, 0),
2976 "HWT",
2977 (1942, 2, 9, 3, 0, 1, 0),
2978 ),
2979 (
2980 (-769395601, 0),
2981 -Offset::hms(9, 30, 0),
2982 "HWT",
2983 (1945, 8, 14, 13, 29, 59, 0),
2984 ),
2985 (
2986 (-769395600, 0),
2987 -Offset::hms(9, 30, 0),
2988 "HPT",
2989 (1945, 8, 14, 13, 30, 0, 0),
2990 ),
2991 (
2992 (-769395599, 0),
2993 -Offset::hms(9, 30, 0),
2994 "HPT",
2995 (1945, 8, 14, 13, 30, 1, 0),
2996 ),
2997 (
2998 (-765376201, 0),
2999 -Offset::hms(9, 30, 0),
3000 "HPT",
3001 (1945, 9, 30, 1, 59, 59, 0),
3002 ),
3003 (
3004 (-765376200, 0),
3005 -Offset::hms(10, 30, 0),
3006 "HST",
3007 (1945, 9, 30, 1, 0, 0, 0),
3008 ),
3009 (
3010 (-765376199, 0),
3011 -Offset::hms(10, 30, 0),
3012 "HST",
3013 (1945, 9, 30, 1, 0, 1, 0),
3014 ),
3015 (
3016 (-712150201, 0),
3017 -Offset::hms(10, 30, 0),
3018 "HST",
3019 (1947, 6, 8, 1, 59, 59, 0),
3020 ),
3021 // At this point, we hit the last transition and the POSIX
3022 // TZ string takes over.
3023 (
3024 (-712150200, 0),
3025 -Offset::hms(10, 0, 0),
3026 "HST",
3027 (1947, 6, 8, 2, 30, 0, 0),
3028 ),
3029 (
3030 (-712150199, 0),
3031 -Offset::hms(10, 0, 0),
3032 "HST",
3033 (1947, 6, 8, 2, 30, 1, 0),
3034 ),
3035 ],
3036 ),
3037 // This time zone has an interesting transition where it jumps
3038 // backwards a full day at 1867-10-19T15:30:00.
3039 (
3040 "America/Sitka",
3041 &[
3042 ((0, 0), o(-8), "PST", (1969, 12, 31, 16, 0, 0, 0)),
3043 (
3044 (-377705023201, 0),
3045 Offset::hms(14, 58, 47),
3046 "LMT",
3047 (-9999, 1, 2, 16, 58, 46, 0),
3048 ),
3049 (
3050 (-3225223728, 0),
3051 Offset::hms(14, 58, 47),
3052 "LMT",
3053 (1867, 10, 19, 15, 29, 59, 0),
3054 ),
3055 // Notice the 24 hour time jump backwards a whole day!
3056 (
3057 (-3225223727, 0),
3058 -Offset::hms(9, 1, 13),
3059 "LMT",
3060 (1867, 10, 18, 15, 30, 0, 0),
3061 ),
3062 (
3063 (-3225223726, 0),
3064 -Offset::hms(9, 1, 13),
3065 "LMT",
3066 (1867, 10, 18, 15, 30, 1, 0),
3067 ),
3068 ],
3069 ),
3070 ];
3071 for &(tzname, timestamps_to_datetimes) in tests {
3072 let test_file = TzifTestFile::get(tzname);
3073 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
3074 for &((unix_sec, unix_nano), offset, abbrev, datetime) in
3075 timestamps_to_datetimes
3076 {
3077 let (year, month, day, hour, min, sec, nano) = datetime;
3078 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
3079 let info = tz.to_offset_info(timestamp);
3080 assert_eq!(
3081 info.offset(),
3082 offset,
3083 "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
3084 );
3085 assert_eq!(
3086 info.abbreviation(),
3087 abbrev,
3088 "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
3089 );
3090 assert_eq!(
3091 info.offset().to_datetime(timestamp),
3092 date(year, month, day).at(hour, min, sec, nano),
3093 "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
3094 );
3095 }
3096 }
3097 }
3098
3099 #[cfg(feature = "alloc")]
3100 #[test]
3101 fn time_zone_posix_to_ambiguous_timestamp() {
3102 let tests: &[(&str, &[_])] = &[
3103 // America/New_York, but a utopia in which DST is abolished.
3104 (
3105 "EST5",
3106 &[
3107 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
3108 ((2024, 3, 10, 2, 0, 0, 0), unambiguous(-5)),
3109 ],
3110 ),
3111 // The standard DST rule for America/New_York.
3112 (
3113 "EST5EDT,M3.2.0,M11.1.0",
3114 &[
3115 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
3116 ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
3117 ((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
3118 ((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
3119 ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
3120 ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
3121 ((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
3122 ((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
3123 ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
3124 ],
3125 ),
3126 // A bit of a nonsensical America/New_York that has DST, but whose
3127 // offset is equivalent to standard time. Having the same offset
3128 // means there's never any ambiguity.
3129 (
3130 "EST5EDT5,M3.2.0,M11.1.0",
3131 &[
3132 ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
3133 ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
3134 ((2024, 3, 10, 2, 0, 0, 0), unambiguous(-5)),
3135 ((2024, 3, 10, 2, 59, 59, 999_999_999), unambiguous(-5)),
3136 ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-5)),
3137 ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-5)),
3138 ((2024, 11, 3, 1, 0, 0, 0), unambiguous(-5)),
3139 ((2024, 11, 3, 1, 59, 59, 999_999_999), unambiguous(-5)),
3140 ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
3141 ],
3142 ),
3143 // This is Europe/Dublin's rule. It's interesting because its
3144 // DST is an offset behind standard time. (DST is usually one hour
3145 // ahead of standard time.)
3146 (
3147 "IST-1GMT0,M10.5.0,M3.5.0/1",
3148 &[
3149 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
3150 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
3151 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
3152 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
3153 ((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
3154 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
3155 ((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
3156 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
3157 ((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
3158 ],
3159 ),
3160 // This is Australia/Tasmania's rule. We chose this because it's
3161 // in the southern hemisphere where DST still skips ahead one hour,
3162 // but it usually starts in the fall and ends in the spring.
3163 (
3164 "AEST-10AEDT,M10.1.0,M4.1.0/3",
3165 &[
3166 ((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
3167 ((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
3168 ((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
3169 ((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
3170 ((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
3171 ((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
3172 ((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
3173 ((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
3174 ((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
3175 ],
3176 ),
3177 // This is Antarctica/Troll's rule. We chose this one because its
3178 // DST transition is 2 hours instead of the standard 1 hour. This
3179 // means gaps and folds are twice as long as they usually are. And
3180 // it means there are 22 hour and 26 hour days, respectively. Wow!
3181 (
3182 "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
3183 &[
3184 ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
3185 // test the gap
3186 ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
3187 ((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
3188 ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
3189 // still in the gap!
3190 ((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
3191 ((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
3192 // finally out
3193 ((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
3194 // test the fold
3195 ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
3196 ((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
3197 ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
3198 // still in the fold!
3199 ((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
3200 ((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
3201 // finally out
3202 ((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
3203 ],
3204 ),
3205 // This is America/St_Johns' rule, which has an offset with
3206 // non-zero minutes *and* a DST transition rule. (Indian Standard
3207 // Time is the one I'm more familiar with, but it turns out IST
3208 // does not have DST!)
3209 (
3210 "NST3:30NDT,M3.2.0,M11.1.0",
3211 &[
3212 (
3213 (1969, 12, 31, 20, 30, 0, 0),
3214 o_unambiguous(-Offset::hms(3, 30, 0)),
3215 ),
3216 (
3217 (2024, 3, 10, 1, 59, 59, 999_999_999),
3218 o_unambiguous(-Offset::hms(3, 30, 0)),
3219 ),
3220 (
3221 (2024, 3, 10, 2, 0, 0, 0),
3222 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
3223 ),
3224 (
3225 (2024, 3, 10, 2, 59, 59, 999_999_999),
3226 o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
3227 ),
3228 (
3229 (2024, 3, 10, 3, 0, 0, 0),
3230 o_unambiguous(-Offset::hms(2, 30, 0)),
3231 ),
3232 (
3233 (2024, 11, 3, 0, 59, 59, 999_999_999),
3234 o_unambiguous(-Offset::hms(2, 30, 0)),
3235 ),
3236 (
3237 (2024, 11, 3, 1, 0, 0, 0),
3238 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
3239 ),
3240 (
3241 (2024, 11, 3, 1, 59, 59, 999_999_999),
3242 o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
3243 ),
3244 (
3245 (2024, 11, 3, 2, 0, 0, 0),
3246 o_unambiguous(-Offset::hms(3, 30, 0)),
3247 ),
3248 ],
3249 ),
3250 ];
3251 for &(posix_tz, datetimes_to_ambiguous) in tests {
3252 let tz = TimeZone::posix(posix_tz).unwrap();
3253 for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
3254 let (year, month, day, hour, min, sec, nano) = datetime;
3255 let dt = date(year, month, day).at(hour, min, sec, nano);
3256 let got = tz.to_ambiguous_zoned(dt);
3257 assert_eq!(
3258 got.offset(),
3259 ambiguous_kind,
3260 "\nTZ: {posix_tz}\ndatetime: \
3261 {year:04}-{month:02}-{day:02}T\
3262 {hour:02}:{min:02}:{sec:02}.{nano:09}",
3263 );
3264 }
3265 }
3266 }
3267
3268 #[cfg(feature = "alloc")]
3269 #[test]
3270 fn time_zone_posix_to_datetime() {
3271 let o = |hours| offset(hours);
3272 let tests: &[(&str, &[_])] = &[
3273 ("EST5", &[((0, 0), o(-5), (1969, 12, 31, 19, 0, 0, 0))]),
3274 (
3275 // From America/New_York
3276 "EST5EDT,M3.2.0,M11.1.0",
3277 &[
3278 ((0, 0), o(-5), (1969, 12, 31, 19, 0, 0, 0)),
3279 ((1710052200, 0), o(-5), (2024, 3, 10, 1, 30, 0, 0)),
3280 (
3281 (1710053999, 999_999_999),
3282 o(-5),
3283 (2024, 3, 10, 1, 59, 59, 999_999_999),
3284 ),
3285 ((1710054000, 0), o(-4), (2024, 3, 10, 3, 0, 0, 0)),
3286 ((1710055800, 0), o(-4), (2024, 3, 10, 3, 30, 0, 0)),
3287 ((1730610000, 0), o(-4), (2024, 11, 3, 1, 0, 0, 0)),
3288 ((1730611800, 0), o(-4), (2024, 11, 3, 1, 30, 0, 0)),
3289 (
3290 (1730613599, 999_999_999),
3291 o(-4),
3292 (2024, 11, 3, 1, 59, 59, 999_999_999),
3293 ),
3294 ((1730613600, 0), o(-5), (2024, 11, 3, 1, 0, 0, 0)),
3295 ((1730615400, 0), o(-5), (2024, 11, 3, 1, 30, 0, 0)),
3296 ],
3297 ),
3298 (
3299 // From Australia/Tasmania
3300 //
3301 // We chose this because it's a time zone in the southern
3302 // hemisphere with DST. Unlike the northern hemisphere, its DST
3303 // starts in the fall and ends in the spring. In the northern
3304 // hemisphere, we typically start DST in the spring and end it
3305 // in the fall.
3306 "AEST-10AEDT,M10.1.0,M4.1.0/3",
3307 &[
3308 ((0, 0), o(11), (1970, 1, 1, 11, 0, 0, 0)),
3309 ((1728142200, 0), o(10), (2024, 10, 6, 1, 30, 0, 0)),
3310 (
3311 (1728143999, 999_999_999),
3312 o(10),
3313 (2024, 10, 6, 1, 59, 59, 999_999_999),
3314 ),
3315 ((1728144000, 0), o(11), (2024, 10, 6, 3, 0, 0, 0)),
3316 ((1728145800, 0), o(11), (2024, 10, 6, 3, 30, 0, 0)),
3317 ((1712415600, 0), o(11), (2024, 4, 7, 2, 0, 0, 0)),
3318 ((1712417400, 0), o(11), (2024, 4, 7, 2, 30, 0, 0)),
3319 (
3320 (1712419199, 999_999_999),
3321 o(11),
3322 (2024, 4, 7, 2, 59, 59, 999_999_999),
3323 ),
3324 ((1712419200, 0), o(10), (2024, 4, 7, 2, 0, 0, 0)),
3325 ((1712421000, 0), o(10), (2024, 4, 7, 2, 30, 0, 0)),
3326 ],
3327 ),
3328 (
3329 // Uses the maximum possible offset. A sloppy read of POSIX
3330 // seems to indicate the maximum offset is 24:59:59, but since
3331 // DST defaults to 1 hour ahead of standard time, it's possible
3332 // to use 24:59:59 for standard time, omit the DST offset, and
3333 // thus get a DST offset of 25:59:59.
3334 "XXX-24:59:59YYY,M3.2.0,M11.1.0",
3335 &[
3336 // 2024-01-05T00:00:00+00
3337 (
3338 (1704412800, 0),
3339 Offset::hms(24, 59, 59),
3340 (2024, 1, 6, 0, 59, 59, 0),
3341 ),
3342 // 2024-06-05T00:00:00+00 (DST)
3343 (
3344 (1717545600, 0),
3345 Offset::hms(25, 59, 59),
3346 (2024, 6, 6, 1, 59, 59, 0),
3347 ),
3348 ],
3349 ),
3350 ];
3351 for &(posix_tz, timestamps_to_datetimes) in tests {
3352 let tz = TimeZone::posix(posix_tz).unwrap();
3353 for &((unix_sec, unix_nano), offset, datetime) in
3354 timestamps_to_datetimes
3355 {
3356 let (year, month, day, hour, min, sec, nano) = datetime;
3357 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
3358 assert_eq!(
3359 tz.to_offset(timestamp),
3360 offset,
3361 "\ntimestamp({unix_sec}, {unix_nano})",
3362 );
3363 assert_eq!(
3364 tz.to_datetime(timestamp),
3365 date(year, month, day).at(hour, min, sec, nano),
3366 "\ntimestamp({unix_sec}, {unix_nano})",
3367 );
3368 }
3369 }
3370 }
3371
3372 #[test]
3373 fn time_zone_fixed_to_datetime() {
3374 let tz = offset(-5).to_time_zone();
3375 let unix_epoch = Timestamp::new(0, 0).unwrap();
3376 assert_eq!(
3377 tz.to_datetime(unix_epoch),
3378 date(1969, 12, 31).at(19, 0, 0, 0),
3379 );
3380
3381 let tz = Offset::from_seconds(93_599).unwrap().to_time_zone();
3382 let timestamp = Timestamp::new(253402207200, 999_999_999).unwrap();
3383 assert_eq!(
3384 tz.to_datetime(timestamp),
3385 date(9999, 12, 31).at(23, 59, 59, 999_999_999),
3386 );
3387
3388 let tz = Offset::from_seconds(-93_599).unwrap().to_time_zone();
3389 let timestamp = Timestamp::new(-377705023201, 0).unwrap();
3390 assert_eq!(
3391 tz.to_datetime(timestamp),
3392 date(-9999, 1, 1).at(0, 0, 0, 0),
3393 );
3394 }
3395
3396 #[test]
3397 fn time_zone_fixed_to_timestamp() {
3398 let tz = offset(-5).to_time_zone();
3399 let dt = date(1969, 12, 31).at(19, 0, 0, 0);
3400 assert_eq!(
3401 tz.to_zoned(dt).unwrap().timestamp(),
3402 Timestamp::new(0, 0).unwrap()
3403 );
3404
3405 let tz = Offset::from_seconds(93_599).unwrap().to_time_zone();
3406 let dt = date(9999, 12, 31).at(23, 59, 59, 999_999_999);
3407 assert_eq!(
3408 tz.to_zoned(dt).unwrap().timestamp(),
3409 Timestamp::new(253402207200, 999_999_999).unwrap(),
3410 );
3411 let tz = Offset::from_seconds(93_598).unwrap().to_time_zone();
3412 assert!(tz.to_zoned(dt).is_err());
3413
3414 let tz = Offset::from_seconds(-93_599).unwrap().to_time_zone();
3415 let dt = date(-9999, 1, 1).at(0, 0, 0, 0);
3416 assert_eq!(
3417 tz.to_zoned(dt).unwrap().timestamp(),
3418 Timestamp::new(-377705023201, 0).unwrap(),
3419 );
3420 let tz = Offset::from_seconds(-93_598).unwrap().to_time_zone();
3421 assert!(tz.to_zoned(dt).is_err());
3422 }
3423
3424 #[cfg(feature = "alloc")]
3425 #[test]
3426 fn time_zone_tzif_previous_transition() {
3427 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
3428 (
3429 "UTC",
3430 &[
3431 ("1969-12-31T19Z", None),
3432 ("2024-03-10T02Z", None),
3433 ("-009999-12-01 00Z", None),
3434 ("9999-12-01 00Z", None),
3435 ],
3436 ),
3437 (
3438 "America/New_York",
3439 &[
3440 ("2024-03-10 08Z", Some("2024-03-10 07Z")),
3441 ("2024-03-10 07:00:00.000000001Z", Some("2024-03-10 07Z")),
3442 ("2024-03-10 07Z", Some("2023-11-05 06Z")),
3443 ("2023-11-05 06Z", Some("2023-03-12 07Z")),
3444 ("-009999-01-31 00Z", None),
3445 ("9999-12-01 00Z", Some("9999-11-07 06Z")),
3446 // While at present we have "fat" TZif files for our
3447 // testdata, it's conceivable they could be swapped to
3448 // "slim." In which case, the tests above will mostly just
3449 // be testing POSIX TZ strings and not the TZif logic. So
3450 // below, we include times that will be in slim (i.e.,
3451 // historical times the precede the current DST rule).
3452 ("1969-12-31 19Z", Some("1969-10-26 06Z")),
3453 ("2000-04-02 08Z", Some("2000-04-02 07Z")),
3454 ("2000-04-02 07:00:00.000000001Z", Some("2000-04-02 07Z")),
3455 ("2000-04-02 07Z", Some("1999-10-31 06Z")),
3456 ("1999-10-31 06Z", Some("1999-04-04 07Z")),
3457 ],
3458 ),
3459 (
3460 "Australia/Tasmania",
3461 &[
3462 ("2010-04-03 17Z", Some("2010-04-03 16Z")),
3463 ("2010-04-03 16:00:00.000000001Z", Some("2010-04-03 16Z")),
3464 ("2010-04-03 16Z", Some("2009-10-03 16Z")),
3465 ("2009-10-03 16Z", Some("2009-04-04 16Z")),
3466 ("-009999-01-31 00Z", None),
3467 ("9999-12-01 00Z", Some("9999-10-02 16Z")),
3468 // Tests for historical data from tzdb. No POSIX TZ.
3469 ("2000-03-25 17Z", Some("2000-03-25 16Z")),
3470 ("2000-03-25 16:00:00.000000001Z", Some("2000-03-25 16Z")),
3471 ("2000-03-25 16Z", Some("1999-10-02 16Z")),
3472 ("1999-10-02 16Z", Some("1999-03-27 16Z")),
3473 ],
3474 ),
3475 // This is Europe/Dublin's rule. It's interesting because its
3476 // DST is an offset behind standard time. (DST is usually one hour
3477 // ahead of standard time.)
3478 (
3479 "Europe/Dublin",
3480 &[
3481 ("2010-03-28 02Z", Some("2010-03-28 01Z")),
3482 ("2010-03-28 01:00:00.000000001Z", Some("2010-03-28 01Z")),
3483 ("2010-03-28 01Z", Some("2009-10-25 01Z")),
3484 ("2009-10-25 01Z", Some("2009-03-29 01Z")),
3485 ("-009999-01-31 00Z", None),
3486 ("9999-12-01 00Z", Some("9999-10-31 01Z")),
3487 // Tests for historical data from tzdb. No POSIX TZ.
3488 ("1990-03-25 02Z", Some("1990-03-25 01Z")),
3489 ("1990-03-25 01:00:00.000000001Z", Some("1990-03-25 01Z")),
3490 ("1990-03-25 01Z", Some("1989-10-29 01Z")),
3491 ("1989-10-25 01Z", Some("1989-03-26 01Z")),
3492 ],
3493 ),
3494 (
3495 // Sao Paulo eliminated DST in 2019, so the previous transition
3496 // from 2024 is several years back.
3497 "America/Sao_Paulo",
3498 &[("2024-03-10 08Z", Some("2019-02-17 02Z"))],
3499 ),
3500 ];
3501 for &(tzname, prev_trans) in tests {
3502 if tzname != "America/Sao_Paulo" {
3503 continue;
3504 }
3505 let test_file = TzifTestFile::get(tzname);
3506 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
3507 for (given, expected) in prev_trans {
3508 let given: Timestamp = given.parse().unwrap();
3509 let expected =
3510 expected.map(|s| s.parse::<Timestamp>().unwrap());
3511 let got = tz.previous_transition(given).map(|t| t.timestamp());
3512 assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
3513 }
3514 }
3515 }
3516
3517 #[cfg(feature = "alloc")]
3518 #[test]
3519 fn time_zone_tzif_next_transition() {
3520 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
3521 (
3522 "UTC",
3523 &[
3524 ("1969-12-31T19Z", None),
3525 ("2024-03-10T02Z", None),
3526 ("-009999-12-01 00Z", None),
3527 ("9999-12-01 00Z", None),
3528 ],
3529 ),
3530 (
3531 "America/New_York",
3532 &[
3533 ("2024-03-10 06Z", Some("2024-03-10 07Z")),
3534 ("2024-03-10 06:59:59.999999999Z", Some("2024-03-10 07Z")),
3535 ("2024-03-10 07Z", Some("2024-11-03 06Z")),
3536 ("2024-11-03 06Z", Some("2025-03-09 07Z")),
3537 ("-009999-12-01 00Z", Some("1883-11-18 17Z")),
3538 ("9999-12-01 00Z", None),
3539 // While at present we have "fat" TZif files for our
3540 // testdata, it's conceivable they could be swapped to
3541 // "slim." In which case, the tests above will mostly just
3542 // be testing POSIX TZ strings and not the TZif logic. So
3543 // below, we include times that will be in slim (i.e.,
3544 // historical times the precede the current DST rule).
3545 ("1969-12-31 19Z", Some("1970-04-26 07Z")),
3546 ("2000-04-02 06Z", Some("2000-04-02 07Z")),
3547 ("2000-04-02 06:59:59.999999999Z", Some("2000-04-02 07Z")),
3548 ("2000-04-02 07Z", Some("2000-10-29 06Z")),
3549 ("2000-10-29 06Z", Some("2001-04-01 07Z")),
3550 ],
3551 ),
3552 (
3553 "Australia/Tasmania",
3554 &[
3555 ("2010-04-03 15Z", Some("2010-04-03 16Z")),
3556 ("2010-04-03 15:59:59.999999999Z", Some("2010-04-03 16Z")),
3557 ("2010-04-03 16Z", Some("2010-10-02 16Z")),
3558 ("2010-10-02 16Z", Some("2011-04-02 16Z")),
3559 ("-009999-12-01 00Z", Some("1895-08-31 14:10:44Z")),
3560 ("9999-12-01 00Z", None),
3561 // Tests for historical data from tzdb. No POSIX TZ.
3562 ("2000-03-25 15Z", Some("2000-03-25 16Z")),
3563 ("2000-03-25 15:59:59.999999999Z", Some("2000-03-25 16Z")),
3564 ("2000-03-25 16Z", Some("2000-08-26 16Z")),
3565 ("2000-08-26 16Z", Some("2001-03-24 16Z")),
3566 ],
3567 ),
3568 (
3569 "Europe/Dublin",
3570 &[
3571 ("2010-03-28 00Z", Some("2010-03-28 01Z")),
3572 ("2010-03-28 00:59:59.999999999Z", Some("2010-03-28 01Z")),
3573 ("2010-03-28 01Z", Some("2010-10-31 01Z")),
3574 ("2010-10-31 01Z", Some("2011-03-27 01Z")),
3575 ("-009999-12-01 00Z", Some("1880-08-02 00:25:21Z")),
3576 ("9999-12-01 00Z", None),
3577 // Tests for historical data from tzdb. No POSIX TZ.
3578 ("1990-03-25 00Z", Some("1990-03-25 01Z")),
3579 ("1990-03-25 00:59:59.999999999Z", Some("1990-03-25 01Z")),
3580 ("1990-03-25 01Z", Some("1990-10-28 01Z")),
3581 ("1990-10-28 01Z", Some("1991-03-31 01Z")),
3582 ],
3583 ),
3584 (
3585 // Sao Paulo eliminated DST in 2019, so the next transition
3586 // from 2024 no longer exists.
3587 "America/Sao_Paulo",
3588 &[("2024-03-10 08Z", None)],
3589 ),
3590 ];
3591 for &(tzname, next_trans) in tests {
3592 let test_file = TzifTestFile::get(tzname);
3593 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
3594 for (given, expected) in next_trans {
3595 let given: Timestamp = given.parse().unwrap();
3596 let expected =
3597 expected.map(|s| s.parse::<Timestamp>().unwrap());
3598 let got = tz.next_transition(given).map(|t| t.timestamp());
3599 assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
3600 }
3601 }
3602 }
3603
3604 #[cfg(feature = "alloc")]
3605 #[test]
3606 fn time_zone_posix_previous_transition() {
3607 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
3608 // America/New_York, but a utopia in which DST is abolished. There
3609 // are no time zone transitions, so next_transition always returns
3610 // None.
3611 (
3612 "EST5",
3613 &[
3614 ("1969-12-31T19Z", None),
3615 ("2024-03-10T02Z", None),
3616 ("-009999-12-01 00Z", None),
3617 ("9999-12-01 00Z", None),
3618 ],
3619 ),
3620 // The standard DST rule for America/New_York.
3621 (
3622 "EST5EDT,M3.2.0,M11.1.0",
3623 &[
3624 ("1969-12-31 19Z", Some("1969-11-02 06Z")),
3625 ("2024-03-10 08Z", Some("2024-03-10 07Z")),
3626 ("2024-03-10 07:00:00.000000001Z", Some("2024-03-10 07Z")),
3627 ("2024-03-10 07Z", Some("2023-11-05 06Z")),
3628 ("2023-11-05 06Z", Some("2023-03-12 07Z")),
3629 ("-009999-01-31 00Z", None),
3630 ("9999-12-01 00Z", Some("9999-11-07 06Z")),
3631 ],
3632 ),
3633 (
3634 // From Australia/Tasmania
3635 "AEST-10AEDT,M10.1.0,M4.1.0/3",
3636 &[
3637 ("2010-04-03 17Z", Some("2010-04-03 16Z")),
3638 ("2010-04-03 16:00:00.000000001Z", Some("2010-04-03 16Z")),
3639 ("2010-04-03 16Z", Some("2009-10-03 16Z")),
3640 ("2009-10-03 16Z", Some("2009-04-04 16Z")),
3641 ("-009999-01-31 00Z", None),
3642 ("9999-12-01 00Z", Some("9999-10-02 16Z")),
3643 ],
3644 ),
3645 // This is Europe/Dublin's rule. It's interesting because its
3646 // DST is an offset behind standard time. (DST is usually one hour
3647 // ahead of standard time.)
3648 (
3649 "IST-1GMT0,M10.5.0,M3.5.0/1",
3650 &[
3651 ("2010-03-28 02Z", Some("2010-03-28 01Z")),
3652 ("2010-03-28 01:00:00.000000001Z", Some("2010-03-28 01Z")),
3653 ("2010-03-28 01Z", Some("2009-10-25 01Z")),
3654 ("2009-10-25 01Z", Some("2009-03-29 01Z")),
3655 ("-009999-01-31 00Z", None),
3656 ("9999-12-01 00Z", Some("9999-10-31 01Z")),
3657 ],
3658 ),
3659 ];
3660 for &(posix_tz, prev_trans) in tests {
3661 let tz = TimeZone::posix(posix_tz).unwrap();
3662 for (given, expected) in prev_trans {
3663 let given: Timestamp = given.parse().unwrap();
3664 let expected =
3665 expected.map(|s| s.parse::<Timestamp>().unwrap());
3666 let got = tz.previous_transition(given).map(|t| t.timestamp());
3667 assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
3668 }
3669 }
3670 }
3671
3672 #[cfg(feature = "alloc")]
3673 #[test]
3674 fn time_zone_posix_next_transition() {
3675 let tests: &[(&str, &[(&str, Option<&str>)])] = &[
3676 // America/New_York, but a utopia in which DST is abolished. There
3677 // are no time zone transitions, so next_transition always returns
3678 // None.
3679 (
3680 "EST5",
3681 &[
3682 ("1969-12-31T19Z", None),
3683 ("2024-03-10T02Z", None),
3684 ("-009999-12-01 00Z", None),
3685 ("9999-12-01 00Z", None),
3686 ],
3687 ),
3688 // The standard DST rule for America/New_York.
3689 (
3690 "EST5EDT,M3.2.0,M11.1.0",
3691 &[
3692 ("1969-12-31 19Z", Some("1970-03-08 07Z")),
3693 ("2024-03-10 06Z", Some("2024-03-10 07Z")),
3694 ("2024-03-10 06:59:59.999999999Z", Some("2024-03-10 07Z")),
3695 ("2024-03-10 07Z", Some("2024-11-03 06Z")),
3696 ("2024-11-03 06Z", Some("2025-03-09 07Z")),
3697 ("-009999-12-01 00Z", Some("-009998-03-10 07Z")),
3698 ("9999-12-01 00Z", None),
3699 ],
3700 ),
3701 (
3702 // From Australia/Tasmania
3703 "AEST-10AEDT,M10.1.0,M4.1.0/3",
3704 &[
3705 ("2010-04-03 15Z", Some("2010-04-03 16Z")),
3706 ("2010-04-03 15:59:59.999999999Z", Some("2010-04-03 16Z")),
3707 ("2010-04-03 16Z", Some("2010-10-02 16Z")),
3708 ("2010-10-02 16Z", Some("2011-04-02 16Z")),
3709 ("-009999-12-01 00Z", Some("-009998-04-06 16Z")),
3710 ("9999-12-01 00Z", None),
3711 ],
3712 ),
3713 // This is Europe/Dublin's rule. It's interesting because its
3714 // DST is an offset behind standard time. (DST is usually one hour
3715 // ahead of standard time.)
3716 (
3717 "IST-1GMT0,M10.5.0,M3.5.0/1",
3718 &[
3719 ("2010-03-28 00Z", Some("2010-03-28 01Z")),
3720 ("2010-03-28 00:59:59.999999999Z", Some("2010-03-28 01Z")),
3721 ("2010-03-28 01Z", Some("2010-10-31 01Z")),
3722 ("2010-10-31 01Z", Some("2011-03-27 01Z")),
3723 ("-009999-12-01 00Z", Some("-009998-03-31 01Z")),
3724 ("9999-12-01 00Z", None),
3725 ],
3726 ),
3727 ];
3728 for &(posix_tz, next_trans) in tests {
3729 let tz = TimeZone::posix(posix_tz).unwrap();
3730 for (given, expected) in next_trans {
3731 let given: Timestamp = given.parse().unwrap();
3732 let expected =
3733 expected.map(|s| s.parse::<Timestamp>().unwrap());
3734 let got = tz.next_transition(given).map(|t| t.timestamp());
3735 assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
3736 }
3737 }
3738 }
3739
3740 /// This tests that the size of a time zone is kept at a single word.
3741 ///
3742 /// This is important because every jiff::Zoned has a TimeZone inside of
3743 /// it, and we want to keep its size as small as we can.
3744 #[test]
3745 fn time_zone_size() {
3746 #[cfg(feature = "alloc")]
3747 {
3748 let word = core::mem::size_of::<usize>();
3749 assert_eq!(word, core::mem::size_of::<TimeZone>());
3750 }
3751 #[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
3752 {
3753 #[cfg(debug_assertions)]
3754 {
3755 assert_eq!(8, core::mem::size_of::<TimeZone>());
3756 }
3757 #[cfg(not(debug_assertions))]
3758 {
3759 // This asserts the same value as the alloc value above, but
3760 // it wasn't always this way, which is why it's written out
3761 // separately. Moreover, in theory, I'd be open to regressing
3762 // this value if it led to an improvement in alloc-mode. But
3763 // more likely, it would be nice to decrease this size in
3764 // non-alloc modes.
3765 assert_eq!(8, core::mem::size_of::<TimeZone>());
3766 }
3767 }
3768 }
3769
3770 /// This tests a few other cases for `TimeZone::to_offset` that
3771 /// probably aren't worth showing in doctest examples.
3772 #[test]
3773 fn time_zone_to_offset() {
3774 let ts = Timestamp::from_second(123456789).unwrap();
3775
3776 let tz = TimeZone::fixed(offset(-5));
3777 let info = tz.to_offset_info(ts);
3778 assert_eq!(info.offset(), offset(-5));
3779 assert_eq!(info.dst(), Dst::No);
3780 assert_eq!(info.abbreviation(), "-05");
3781
3782 let tz = TimeZone::fixed(offset(5));
3783 let info = tz.to_offset_info(ts);
3784 assert_eq!(info.offset(), offset(5));
3785 assert_eq!(info.dst(), Dst::No);
3786 assert_eq!(info.abbreviation(), "+05");
3787
3788 let tz = TimeZone::fixed(offset(-12));
3789 let info = tz.to_offset_info(ts);
3790 assert_eq!(info.offset(), offset(-12));
3791 assert_eq!(info.dst(), Dst::No);
3792 assert_eq!(info.abbreviation(), "-12");
3793
3794 let tz = TimeZone::fixed(offset(12));
3795 let info = tz.to_offset_info(ts);
3796 assert_eq!(info.offset(), offset(12));
3797 assert_eq!(info.dst(), Dst::No);
3798 assert_eq!(info.abbreviation(), "+12");
3799
3800 let tz = TimeZone::fixed(offset(0));
3801 let info = tz.to_offset_info(ts);
3802 assert_eq!(info.offset(), offset(0));
3803 assert_eq!(info.dst(), Dst::No);
3804 assert_eq!(info.abbreviation(), "UTC");
3805 }
3806
3807 /// This tests a few other cases for `TimeZone::to_fixed_offset` that
3808 /// probably aren't worth showing in doctest examples.
3809 #[test]
3810 fn time_zone_to_fixed_offset() {
3811 let tz = TimeZone::UTC;
3812 assert_eq!(tz.to_fixed_offset().unwrap(), Offset::UTC);
3813
3814 let offset = Offset::from_hours(1).unwrap();
3815 let tz = TimeZone::fixed(offset);
3816 assert_eq!(tz.to_fixed_offset().unwrap(), offset);
3817
3818 #[cfg(feature = "alloc")]
3819 {
3820 let tz = TimeZone::posix("EST5").unwrap();
3821 assert!(tz.to_fixed_offset().is_err());
3822
3823 let test_file = TzifTestFile::get("America/New_York");
3824 let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
3825 assert!(tz.to_fixed_offset().is_err());
3826 }
3827 }
3828}