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