use crate::{
civil::DateTime,
error::{tz::timezone::Error as E, Error},
tz::{
ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned},
offset::{Dst, Offset},
},
util::{array_str::ArrayStr, sync::Arc},
Timestamp, Zoned,
};
use crate::tz::posix::PosixTimeZoneOwned;
use self::repr::Repr;
#[derive(Clone, Eq, PartialEq)]
pub struct TimeZone {
repr: Repr,
}
impl TimeZone {
pub const UTC: TimeZone = TimeZone { repr: Repr::utc() };
#[inline]
pub fn system() -> TimeZone {
match TimeZone::try_system() {
Ok(tz) => tz,
Err(_err) => {
warn!(
"failed to get system time zone, \
falling back to `Etc/Unknown` \
(which behaves like UTC): {_err}",
);
TimeZone::unknown()
}
}
}
#[inline]
pub fn try_system() -> Result<TimeZone, Error> {
#[cfg(not(feature = "tz-system"))]
{
Err(Error::from(crate::error::CrateFeatureError::TzSystem)
.context(E::FailedSystem))
}
#[cfg(feature = "tz-system")]
{
crate::tz::system::get(crate::tz::db())
}
}
#[inline]
pub fn get(time_zone_name: &str) -> Result<TimeZone, Error> {
crate::tz::db().get(time_zone_name)
}
#[inline]
pub const fn fixed(offset: Offset) -> TimeZone {
if offset.seconds() == 0 {
return TimeZone::UTC;
}
let repr = Repr::fixed(offset);
TimeZone { repr }
}
#[cfg(feature = "alloc")]
pub fn posix(posix_tz_string: &str) -> Result<TimeZone, Error> {
let posix_tz = PosixTimeZoneOwned::parse(posix_tz_string)?;
Ok(TimeZone::from_posix_tz(posix_tz))
}
#[cfg(feature = "alloc")]
pub(crate) fn from_posix_tz(posix: PosixTimeZoneOwned) -> TimeZone {
let repr = Repr::arc_posix(Arc::new(posix));
TimeZone { repr }
}
#[cfg(feature = "alloc")]
pub fn tzif(name: &str, data: &[u8]) -> Result<TimeZone, Error> {
use alloc::string::ToString;
let name = name.to_string();
let tzif = crate::tz::tzif::Tzif::parse(Some(name), data)?;
let repr = Repr::arc_tzif(Arc::new(tzif));
Ok(TimeZone { repr })
}
pub const fn unknown() -> TimeZone {
let repr = Repr::unknown();
TimeZone { repr }
}
#[cfg(feature = "tz-system")]
pub(crate) fn tzif_system(data: &[u8]) -> Result<TimeZone, Error> {
let tzif = crate::tz::tzif::Tzif::parse(None, data)?;
let repr = Repr::arc_tzif(Arc::new(tzif));
Ok(TimeZone { repr })
}
#[inline]
pub(crate) fn diagnostic_name(&self) -> DiagnosticName<'_> {
DiagnosticName(self)
}
#[cfg(feature = "serde")]
#[inline]
pub(crate) fn has_succinct_serialization(&self) -> bool {
repr::each! {
&self.repr,
UTC => true,
UNKNOWN => true,
FIXED(_offset) => true,
STATIC_TZIF(tzif) => tzif.name().is_some(),
ARC_TZIF(tzif) => tzif.name().is_some(),
ARC_POSIX(_posix) => true,
}
}
#[inline]
pub fn iana_name(&self) -> Option<&str> {
repr::each! {
&self.repr,
UTC => Some("UTC"),
UNKNOWN => None,
FIXED(_offset) => None,
STATIC_TZIF(tzif) => tzif.name(),
ARC_TZIF(tzif) => tzif.name(),
ARC_POSIX(_posix) => None,
}
}
#[inline]
pub fn is_unknown(&self) -> bool {
self.repr.is_unknown()
}
#[inline]
pub(crate) fn posix_tz(&self) -> Option<&PosixTimeZoneOwned> {
repr::each! {
&self.repr,
UTC => None,
UNKNOWN => None,
FIXED(_offset) => None,
STATIC_TZIF(_tzif) => None,
ARC_TZIF(_tzif) => None,
ARC_POSIX(posix) => Some(posix),
}
}
#[inline]
pub fn to_datetime(&self, timestamp: Timestamp) -> DateTime {
self.to_offset(timestamp).to_datetime(timestamp)
}
#[inline]
pub fn to_offset(&self, timestamp: Timestamp) -> Offset {
repr::each! {
&self.repr,
UTC => Offset::UTC,
UNKNOWN => Offset::UTC,
FIXED(offset) => offset,
STATIC_TZIF(tzif) => tzif.to_offset(timestamp),
ARC_TZIF(tzif) => tzif.to_offset(timestamp),
ARC_POSIX(posix) => posix.to_offset(timestamp),
}
}
#[inline]
pub fn to_offset_info<'t>(
&'t self,
timestamp: Timestamp,
) -> TimeZoneOffsetInfo<'t> {
repr::each! {
&self.repr,
UTC => TimeZoneOffsetInfo {
offset: Offset::UTC,
dst: Dst::No,
abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
},
UNKNOWN => TimeZoneOffsetInfo {
offset: Offset::UTC,
dst: Dst::No,
abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
},
FIXED(offset) => {
let abbreviation =
TimeZoneAbbreviation::Owned(offset.to_array_str());
TimeZoneOffsetInfo {
offset,
dst: Dst::No,
abbreviation,
}
},
STATIC_TZIF(tzif) => tzif.to_offset_info(timestamp),
ARC_TZIF(tzif) => tzif.to_offset_info(timestamp),
ARC_POSIX(posix) => posix.to_offset_info(timestamp),
}
}
#[inline]
pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
let mkerr = || {
Error::from(E::ConvertNonFixed { kind: self.kind_description() })
};
repr::each! {
&self.repr,
UTC => Ok(Offset::UTC),
UNKNOWN => Ok(Offset::UTC),
FIXED(offset) => Ok(offset),
STATIC_TZIF(_tzif) => Err(mkerr()),
ARC_TZIF(_tzif) => Err(mkerr()),
ARC_POSIX(_posix) => Err(mkerr()),
}
}
#[inline]
pub fn to_zoned(&self, dt: DateTime) -> Result<Zoned, Error> {
self.to_ambiguous_zoned(dt).compatible()
}
#[inline]
pub fn to_ambiguous_zoned(&self, dt: DateTime) -> AmbiguousZoned {
self.clone().into_ambiguous_zoned(dt)
}
#[inline]
pub fn into_ambiguous_zoned(self, dt: DateTime) -> AmbiguousZoned {
self.to_ambiguous_timestamp(dt).into_ambiguous_zoned(self)
}
#[inline]
pub fn to_timestamp(&self, dt: DateTime) -> Result<Timestamp, Error> {
self.to_ambiguous_timestamp(dt).compatible()
}
#[inline]
pub fn to_ambiguous_timestamp(&self, dt: DateTime) -> AmbiguousTimestamp {
let ambiguous_kind = repr::each! {
&self.repr,
UTC => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
UNKNOWN => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
FIXED(offset) => AmbiguousOffset::Unambiguous { offset },
STATIC_TZIF(tzif) => tzif.to_ambiguous_kind(dt),
ARC_TZIF(tzif) => tzif.to_ambiguous_kind(dt),
ARC_POSIX(posix) => posix.to_ambiguous_kind(dt),
};
AmbiguousTimestamp::new(dt, ambiguous_kind)
}
#[inline]
pub fn preceding<'t>(
&'t self,
timestamp: Timestamp,
) -> TimeZonePrecedingTransitions<'t> {
TimeZonePrecedingTransitions { tz: self, cur: timestamp }
}
#[inline]
pub fn following<'t>(
&'t self,
timestamp: Timestamp,
) -> TimeZoneFollowingTransitions<'t> {
TimeZoneFollowingTransitions { tz: self, cur: timestamp }
}
#[inline]
fn previous_transition<'t>(
&'t self,
timestamp: Timestamp,
) -> Option<TimeZoneTransition<'t>> {
repr::each! {
&self.repr,
UTC => None,
UNKNOWN => None,
FIXED(_offset) => None,
STATIC_TZIF(tzif) => tzif.previous_transition(timestamp),
ARC_TZIF(tzif) => tzif.previous_transition(timestamp),
ARC_POSIX(posix) => posix.previous_transition(timestamp),
}
}
#[inline]
fn next_transition<'t>(
&'t self,
timestamp: Timestamp,
) -> Option<TimeZoneTransition<'t>> {
repr::each! {
&self.repr,
UTC => None,
UNKNOWN => None,
FIXED(_offset) => None,
STATIC_TZIF(tzif) => tzif.next_transition(timestamp),
ARC_TZIF(tzif) => tzif.next_transition(timestamp),
ARC_POSIX(posix) => posix.next_transition(timestamp),
}
}
fn kind_description(&self) -> &'static str {
repr::each! {
&self.repr,
UTC => "UTC",
UNKNOWN => "Etc/Unknown",
FIXED(_offset) => "fixed",
STATIC_TZIF(_tzif) => "IANA",
ARC_TZIF(_tzif) => "IANA",
ARC_POSIX(_posix) => "POSIX",
}
}
}
#[doc(hidden)]
impl TimeZone {
pub const fn __internal_from_tzif(
tzif: &'static crate::tz::tzif::TzifStatic,
) -> TimeZone {
let repr = Repr::static_tzif(tzif);
TimeZone { repr }
}
#[inline]
pub const unsafe fn copy(&self) -> TimeZone {
unsafe { TimeZone { repr: self.repr.copy() } }
}
}
impl core::fmt::Debug for TimeZone {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_tuple("TimeZone").field(&self.repr).finish()
}
}
#[derive(Clone, Debug)]
pub struct TimeZoneTransition<'t> {
pub(crate) timestamp: Timestamp,
pub(crate) offset: Offset,
pub(crate) abbrev: &'t str,
pub(crate) dst: Dst,
}
impl<'t> TimeZoneTransition<'t> {
#[inline]
pub fn timestamp(&self) -> Timestamp {
self.timestamp
}
#[inline]
pub fn offset(&self) -> Offset {
self.offset
}
#[inline]
pub fn abbreviation<'a>(&'a self) -> &'a str {
self.abbrev
}
#[inline]
pub fn dst(&self) -> Dst {
self.dst
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TimeZoneOffsetInfo<'t> {
pub(crate) offset: Offset,
pub(crate) dst: Dst,
pub(crate) abbreviation: TimeZoneAbbreviation<'t>,
}
impl<'t> TimeZoneOffsetInfo<'t> {
#[inline]
pub fn offset(&self) -> Offset {
self.offset
}
#[inline]
pub fn abbreviation(&self) -> &str {
self.abbreviation.as_str()
}
#[inline]
pub fn dst(&self) -> Dst {
self.dst
}
}
#[derive(Clone, Debug)]
pub struct TimeZonePrecedingTransitions<'t> {
tz: &'t TimeZone,
cur: Timestamp,
}
impl<'t> Iterator for TimeZonePrecedingTransitions<'t> {
type Item = TimeZoneTransition<'t>;
fn next(&mut self) -> Option<TimeZoneTransition<'t>> {
let trans = self.tz.previous_transition(self.cur)?;
self.cur = trans.timestamp();
Some(trans)
}
}
impl<'t> core::iter::FusedIterator for TimeZonePrecedingTransitions<'t> {}
#[derive(Clone, Debug)]
pub struct TimeZoneFollowingTransitions<'t> {
tz: &'t TimeZone,
cur: Timestamp,
}
impl<'t> Iterator for TimeZoneFollowingTransitions<'t> {
type Item = TimeZoneTransition<'t>;
fn next(&mut self) -> Option<TimeZoneTransition<'t>> {
let trans = self.tz.next_transition(self.cur)?;
self.cur = trans.timestamp();
Some(trans)
}
}
impl<'t> core::iter::FusedIterator for TimeZoneFollowingTransitions<'t> {}
pub(crate) struct DiagnosticName<'a>(&'a TimeZone);
impl<'a> core::fmt::Display for DiagnosticName<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
repr::each! {
&self.0.repr,
UTC => f.write_str("UTC"),
UNKNOWN => f.write_str("Etc/Unknown"),
FIXED(offset) => offset.fmt(f),
STATIC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
ARC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
ARC_POSIX(posix) => posix.fmt(f),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub(crate) enum TimeZoneAbbreviation<'t> {
Borrowed(&'t str),
Owned(ArrayStr<9>),
}
impl<'t> TimeZoneAbbreviation<'t> {
fn as_str<'a>(&'a self) -> &'a str {
match *self {
TimeZoneAbbreviation::Borrowed(s) => s,
TimeZoneAbbreviation::Owned(ref s) => s.as_str(),
}
}
}
#[allow(unstable_name_collisions)]
mod repr {
use core::mem::ManuallyDrop;
use crate::{tz::tzif::TzifStatic, util::constant::unwrap};
#[cfg(feature = "alloc")]
use crate::{
tz::{posix::PosixTimeZoneOwned, tzif::TzifOwned},
util::sync::Arc,
};
use super::Offset;
#[allow(unused_imports)]
use self::polyfill::{without_provenance, StrictProvenancePolyfill};
macro_rules! each {
(
$repr:expr,
UTC => $utc:expr,
UNKNOWN => $unknown:expr,
FIXED($offset:ident) => $fixed:expr,
STATIC_TZIF($static_tzif:ident) => $static_tzif_block:expr,
ARC_TZIF($arc_tzif:ident) => $arc_tzif_block:expr,
ARC_POSIX($arc_posix:ident) => $arc_posix_block:expr,
) => {{
let repr = $repr;
match repr.tag() {
Repr::UTC => $utc,
Repr::UNKNOWN => $unknown,
Repr::FIXED => {
let $offset = unsafe { repr.get_fixed() };
$fixed
}
Repr::STATIC_TZIF => {
let $static_tzif = unsafe { repr.get_static_tzif() };
$static_tzif_block
}
#[cfg(feature = "alloc")]
Repr::ARC_TZIF => {
let $arc_tzif = unsafe { repr.get_arc_tzif() };
$arc_tzif_block
}
#[cfg(feature = "alloc")]
Repr::ARC_POSIX => {
let $arc_posix = unsafe { repr.get_arc_posix() };
$arc_posix_block
}
_ => {
debug_assert!(false, "each: invalid time zone repr tag!");
unsafe {
core::hint::unreachable_unchecked();
}
}
}
}};
}
pub(super) use each;
pub(super) struct Repr {
ptr: *const u8,
}
impl Repr {
const BITS: usize = 0b111;
pub(super) const UTC: usize = 1;
pub(super) const UNKNOWN: usize = 2;
pub(super) const FIXED: usize = 3;
pub(super) const STATIC_TZIF: usize = 0;
pub(super) const ARC_TZIF: usize = 4;
pub(super) const ARC_POSIX: usize = 5;
const ALIGN: usize = 8;
#[inline]
pub(super) const fn utc() -> Repr {
let ptr = without_provenance(Repr::UTC);
Repr { ptr }
}
#[inline]
pub(super) const fn unknown() -> Repr {
let ptr = without_provenance(Repr::UNKNOWN);
Repr { ptr }
}
#[inline]
pub(super) const fn fixed(offset: Offset) -> Repr {
let seconds = offset.seconds();
let shifted = unwrap!(
seconds.checked_shl(4),
"offset small enough for left shift by 4 bits",
);
assert!(usize::MAX >= 4_294_967_295);
let ptr = without_provenance((shifted as usize) | Repr::FIXED);
Repr { ptr }
}
#[inline]
pub(super) const fn static_tzif(tzif: &'static TzifStatic) -> Repr {
assert!(core::mem::align_of::<TzifStatic>() >= Repr::ALIGN);
let tzif = (tzif as *const TzifStatic).cast::<u8>();
Repr { ptr: tzif }
}
#[cfg(feature = "alloc")]
#[inline]
pub(super) fn arc_tzif(tzif: Arc<TzifOwned>) -> Repr {
assert!(core::mem::align_of::<TzifOwned>() >= Repr::ALIGN);
let tzif = Arc::into_raw(tzif).cast::<u8>();
assert!(tzif.addr() % 4 == 0);
let ptr = tzif.map_addr(|addr| addr | Repr::ARC_TZIF);
Repr { ptr }
}
#[cfg(feature = "alloc")]
#[inline]
pub(super) fn arc_posix(posix_tz: Arc<PosixTimeZoneOwned>) -> Repr {
assert!(
core::mem::align_of::<PosixTimeZoneOwned>() >= Repr::ALIGN
);
let posix_tz = Arc::into_raw(posix_tz).cast::<u8>();
assert!(posix_tz.addr() % 4 == 0);
let ptr = posix_tz.map_addr(|addr| addr | Repr::ARC_POSIX);
Repr { ptr }
}
#[inline]
pub(super) unsafe fn get_fixed(&self) -> Offset {
#[allow(unstable_name_collisions)]
let addr = self.ptr.addr();
Offset::from_seconds_unchecked((addr as i32) >> 4)
}
#[inline]
pub(super) fn is_unknown(&self) -> bool {
self.tag() == Repr::UNKNOWN
}
#[inline]
pub(super) unsafe fn get_static_tzif(&self) -> &'static TzifStatic {
#[allow(unstable_name_collisions)]
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
unsafe { &*ptr.cast::<TzifStatic>() }
}
#[cfg(feature = "alloc")]
#[inline]
pub(super) unsafe fn get_arc_tzif<'a>(&'a self) -> &'a TzifOwned {
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
let arc = ManuallyDrop::new(unsafe {
Arc::from_raw(ptr.cast::<TzifOwned>())
});
unsafe { &*Arc::as_ptr(&arc) }
}
#[cfg(feature = "alloc")]
#[inline]
pub(super) unsafe fn get_arc_posix<'a>(
&'a self,
) -> &'a PosixTimeZoneOwned {
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
let arc = ManuallyDrop::new(unsafe {
Arc::from_raw(ptr.cast::<PosixTimeZoneOwned>())
});
unsafe { &*Arc::as_ptr(&arc) }
}
#[inline]
pub(super) fn tag(&self) -> usize {
#[allow(unstable_name_collisions)]
{
self.ptr.addr() & Repr::BITS
}
}
#[inline]
pub(super) const unsafe fn copy(&self) -> Repr {
Repr { ptr: self.ptr }
}
}
unsafe impl Send for Repr {}
unsafe impl Sync for Repr {}
impl core::fmt::Debug for Repr {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
each! {
self,
UTC => f.write_str("UTC"),
UNKNOWN => f.write_str("Etc/Unknown"),
FIXED(offset) => core::fmt::Debug::fmt(&offset, f),
STATIC_TZIF(tzif) => {
let field = tzif.name().unwrap_or("Local");
f.debug_tuple("TZif").field(&field).finish()
},
ARC_TZIF(tzif) => {
let field = tzif.name().unwrap_or("Local");
f.debug_tuple("TZif").field(&field).finish()
},
ARC_POSIX(posix) => {
f.write_str("Posix(")?;
core::fmt::Display::fmt(&posix, f)?;
f.write_str(")")
},
}
}
}
impl Clone for Repr {
#[inline]
fn clone(&self) -> Repr {
match self.tag() {
Repr::UTC
| Repr::UNKNOWN
| Repr::FIXED
| Repr::STATIC_TZIF => Repr { ptr: self.ptr },
#[cfg(feature = "alloc")]
Repr::ARC_TZIF => {
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
unsafe {
Arc::increment_strong_count(ptr.cast::<TzifOwned>());
}
Repr { ptr: self.ptr }
}
#[cfg(feature = "alloc")]
Repr::ARC_POSIX => {
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
unsafe {
Arc::increment_strong_count(
ptr.cast::<PosixTimeZoneOwned>(),
);
}
Repr { ptr: self.ptr }
}
_ => {
debug_assert!(false, "clone: invalid time zone repr tag!");
unsafe {
core::hint::unreachable_unchecked();
}
}
}
}
}
impl Drop for Repr {
#[inline]
fn drop(&mut self) {
match self.tag() {
Repr::UTC
| Repr::UNKNOWN
| Repr::FIXED
| Repr::STATIC_TZIF => {}
#[cfg(feature = "alloc")]
Repr::ARC_TZIF => {
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
unsafe {
Arc::decrement_strong_count(ptr.cast::<TzifOwned>());
}
}
#[cfg(feature = "alloc")]
Repr::ARC_POSIX => {
let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
unsafe {
Arc::decrement_strong_count(
ptr.cast::<PosixTimeZoneOwned>(),
);
}
}
_ => {
debug_assert!(false, "drop: invalid time zone repr tag!");
unsafe {
core::hint::unreachable_unchecked();
}
}
}
}
}
impl Eq for Repr {}
impl PartialEq for Repr {
fn eq(&self, other: &Repr) -> bool {
if self.tag() != other.tag() {
return false;
}
each! {
self,
UTC => true,
UNKNOWN => true,
FIXED(offset) => offset == unsafe { other.get_fixed() },
STATIC_TZIF(tzif) => tzif == unsafe { other.get_static_tzif() },
ARC_TZIF(tzif) => tzif == unsafe { other.get_arc_tzif() },
ARC_POSIX(posix) => posix == unsafe { other.get_arc_posix() },
}
}
}
mod polyfill {
pub(super) const fn without_provenance(addr: usize) -> *const u8 {
#[allow(integer_to_ptr_transmutes)]
unsafe {
core::mem::transmute(addr)
}
}
#[allow(dead_code)]
pub(super) trait StrictProvenancePolyfill:
Sized + Clone + Copy
{
fn addr(&self) -> usize;
fn with_addr(&self, addr: usize) -> Self;
fn map_addr(&self, map: impl FnOnce(usize) -> usize) -> Self {
self.with_addr(map(self.addr()))
}
}
impl StrictProvenancePolyfill for *const u8 {
fn addr(&self) -> usize {
unsafe { core::mem::transmute(self.cast::<()>()) }
}
fn with_addr(&self, address: usize) -> Self {
let self_addr = self.addr() as isize;
let dest_addr = address as isize;
let offset = dest_addr.wrapping_sub(self_addr);
self.wrapping_offset(offset)
}
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
use crate::tz::testdata::TzifTestFile;
use crate::{civil::date, tz::offset};
use super::*;
fn unambiguous(offset_hours: i8) -> AmbiguousOffset {
let offset = offset(offset_hours);
o_unambiguous(offset)
}
fn gap(
earlier_offset_hours: i8,
later_offset_hours: i8,
) -> AmbiguousOffset {
let earlier = offset(earlier_offset_hours);
let later = offset(later_offset_hours);
o_gap(earlier, later)
}
fn fold(
earlier_offset_hours: i8,
later_offset_hours: i8,
) -> AmbiguousOffset {
let earlier = offset(earlier_offset_hours);
let later = offset(later_offset_hours);
o_fold(earlier, later)
}
fn o_unambiguous(offset: Offset) -> AmbiguousOffset {
AmbiguousOffset::Unambiguous { offset }
}
fn o_gap(earlier: Offset, later: Offset) -> AmbiguousOffset {
AmbiguousOffset::Gap { before: earlier, after: later }
}
fn o_fold(earlier: Offset, later: Offset) -> AmbiguousOffset {
AmbiguousOffset::Fold { before: earlier, after: later }
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_tzif_to_ambiguous_timestamp() {
let tests: &[(&str, &[_])] = &[
(
"America/New_York",
&[
((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
],
),
(
"Europe/Dublin",
&[
((1970, 1, 1, 0, 0, 0, 0), unambiguous(1)),
((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
],
),
(
"Australia/Tasmania",
&[
((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
],
),
(
"Antarctica/Troll",
&[
((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
],
),
(
"America/St_Johns",
&[
(
(1969, 12, 31, 20, 30, 0, 0),
o_unambiguous(-Offset::hms(3, 30, 0)),
),
(
(2024, 3, 10, 1, 59, 59, 999_999_999),
o_unambiguous(-Offset::hms(3, 30, 0)),
),
(
(2024, 3, 10, 2, 0, 0, 0),
o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
),
(
(2024, 3, 10, 2, 59, 59, 999_999_999),
o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
),
(
(2024, 3, 10, 3, 0, 0, 0),
o_unambiguous(-Offset::hms(2, 30, 0)),
),
(
(2024, 11, 3, 0, 59, 59, 999_999_999),
o_unambiguous(-Offset::hms(2, 30, 0)),
),
(
(2024, 11, 3, 1, 0, 0, 0),
o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
),
(
(2024, 11, 3, 1, 59, 59, 999_999_999),
o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
),
(
(2024, 11, 3, 2, 0, 0, 0),
o_unambiguous(-Offset::hms(3, 30, 0)),
),
],
),
(
"America/Sitka",
&[
((1969, 12, 31, 16, 0, 0, 0), unambiguous(-8)),
(
(-9999, 1, 2, 16, 58, 46, 0),
o_unambiguous(Offset::hms(14, 58, 47)),
),
(
(1867, 10, 18, 15, 29, 59, 0),
o_unambiguous(Offset::hms(14, 58, 47)),
),
(
(1867, 10, 18, 15, 30, 0, 0),
o_fold(
Offset::hms(14, 58, 47),
-Offset::hms(9, 1, 13),
),
),
(
(1867, 10, 19, 15, 29, 59, 999_999_999),
o_fold(
Offset::hms(14, 58, 47),
-Offset::hms(9, 1, 13),
),
),
(
(1867, 10, 19, 15, 30, 0, 0),
o_unambiguous(-Offset::hms(9, 1, 13)),
),
],
),
(
"Pacific/Honolulu",
&[
(
(1896, 1, 13, 11, 59, 59, 0),
o_unambiguous(-Offset::hms(10, 31, 26)),
),
(
(1896, 1, 13, 12, 0, 0, 0),
o_gap(
-Offset::hms(10, 31, 26),
-Offset::hms(10, 30, 0),
),
),
(
(1896, 1, 13, 12, 1, 25, 0),
o_gap(
-Offset::hms(10, 31, 26),
-Offset::hms(10, 30, 0),
),
),
(
(1896, 1, 13, 12, 1, 26, 0),
o_unambiguous(-Offset::hms(10, 30, 0)),
),
(
(1933, 4, 30, 1, 59, 59, 0),
o_unambiguous(-Offset::hms(10, 30, 0)),
),
(
(1933, 4, 30, 2, 0, 0, 0),
o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
),
(
(1933, 4, 30, 2, 59, 59, 0),
o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
),
(
(1933, 4, 30, 3, 0, 0, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1933, 5, 21, 10, 59, 59, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1933, 5, 21, 11, 0, 0, 0),
o_fold(
-Offset::hms(9, 30, 0),
-Offset::hms(10, 30, 0),
),
),
(
(1933, 5, 21, 11, 59, 59, 0),
o_fold(
-Offset::hms(9, 30, 0),
-Offset::hms(10, 30, 0),
),
),
(
(1933, 5, 21, 12, 0, 0, 0),
o_unambiguous(-Offset::hms(10, 30, 0)),
),
(
(1942, 2, 9, 1, 59, 59, 0),
o_unambiguous(-Offset::hms(10, 30, 0)),
),
(
(1942, 2, 9, 2, 0, 0, 0),
o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
),
(
(1942, 2, 9, 2, 59, 59, 0),
o_gap(-Offset::hms(10, 30, 0), -Offset::hms(9, 30, 0)),
),
(
(1942, 2, 9, 3, 0, 0, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1945, 8, 14, 13, 29, 59, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1945, 8, 14, 13, 30, 0, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1945, 8, 14, 13, 30, 1, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1945, 9, 30, 0, 59, 59, 0),
o_unambiguous(-Offset::hms(9, 30, 0)),
),
(
(1945, 9, 30, 1, 0, 0, 0),
o_fold(
-Offset::hms(9, 30, 0),
-Offset::hms(10, 30, 0),
),
),
(
(1945, 9, 30, 1, 59, 59, 0),
o_fold(
-Offset::hms(9, 30, 0),
-Offset::hms(10, 30, 0),
),
),
(
(1945, 9, 30, 2, 0, 0, 0),
o_unambiguous(-Offset::hms(10, 30, 0)),
),
(
(1947, 6, 8, 1, 59, 59, 0),
o_unambiguous(-Offset::hms(10, 30, 0)),
),
(
(1947, 6, 8, 2, 0, 0, 0),
o_gap(-Offset::hms(10, 30, 0), -offset(10)),
),
(
(1947, 6, 8, 2, 29, 59, 0),
o_gap(-Offset::hms(10, 30, 0), -offset(10)),
),
((1947, 6, 8, 2, 30, 0, 0), unambiguous(-10)),
],
),
];
for &(tzname, datetimes_to_ambiguous) in tests {
let test_file = TzifTestFile::get(tzname);
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
let (year, month, day, hour, min, sec, nano) = datetime;
let dt = date(year, month, day).at(hour, min, sec, nano);
let got = tz.to_ambiguous_zoned(dt);
assert_eq!(
got.offset(),
ambiguous_kind,
"\nTZ: {tzname}\ndatetime: \
{year:04}-{month:02}-{day:02}T\
{hour:02}:{min:02}:{sec:02}.{nano:09}",
);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_tzif_to_datetime() {
let o = |hours| offset(hours);
let tests: &[(&str, &[_])] = &[
(
"America/New_York",
&[
((0, 0), o(-5), "EST", (1969, 12, 31, 19, 0, 0, 0)),
(
(1710052200, 0),
o(-5),
"EST",
(2024, 3, 10, 1, 30, 0, 0),
),
(
(1710053999, 999_999_999),
o(-5),
"EST",
(2024, 3, 10, 1, 59, 59, 999_999_999),
),
((1710054000, 0), o(-4), "EDT", (2024, 3, 10, 3, 0, 0, 0)),
(
(1710055800, 0),
o(-4),
"EDT",
(2024, 3, 10, 3, 30, 0, 0),
),
((1730610000, 0), o(-4), "EDT", (2024, 11, 3, 1, 0, 0, 0)),
(
(1730611800, 0),
o(-4),
"EDT",
(2024, 11, 3, 1, 30, 0, 0),
),
(
(1730613599, 999_999_999),
o(-4),
"EDT",
(2024, 11, 3, 1, 59, 59, 999_999_999),
),
((1730613600, 0), o(-5), "EST", (2024, 11, 3, 1, 0, 0, 0)),
(
(1730615400, 0),
o(-5),
"EST",
(2024, 11, 3, 1, 30, 0, 0),
),
],
),
(
"Australia/Tasmania",
&[
((0, 0), o(11), "AEDT", (1970, 1, 1, 11, 0, 0, 0)),
(
(1728142200, 0),
o(10),
"AEST",
(2024, 10, 6, 1, 30, 0, 0),
),
(
(1728143999, 999_999_999),
o(10),
"AEST",
(2024, 10, 6, 1, 59, 59, 999_999_999),
),
(
(1728144000, 0),
o(11),
"AEDT",
(2024, 10, 6, 3, 0, 0, 0),
),
(
(1728145800, 0),
o(11),
"AEDT",
(2024, 10, 6, 3, 30, 0, 0),
),
((1712415600, 0), o(11), "AEDT", (2024, 4, 7, 2, 0, 0, 0)),
(
(1712417400, 0),
o(11),
"AEDT",
(2024, 4, 7, 2, 30, 0, 0),
),
(
(1712419199, 999_999_999),
o(11),
"AEDT",
(2024, 4, 7, 2, 59, 59, 999_999_999),
),
((1712419200, 0), o(10), "AEST", (2024, 4, 7, 2, 0, 0, 0)),
(
(1712421000, 0),
o(10),
"AEST",
(2024, 4, 7, 2, 30, 0, 0),
),
],
),
(
"Pacific/Honolulu",
&[
(
(-2334101315, 0),
-Offset::hms(10, 31, 26),
"LMT",
(1896, 1, 13, 11, 59, 59, 0),
),
(
(-2334101314, 0),
-Offset::hms(10, 30, 0),
"HST",
(1896, 1, 13, 12, 1, 26, 0),
),
(
(-2334101313, 0),
-Offset::hms(10, 30, 0),
"HST",
(1896, 1, 13, 12, 1, 27, 0),
),
(
(-1157283001, 0),
-Offset::hms(10, 30, 0),
"HST",
(1933, 4, 30, 1, 59, 59, 0),
),
(
(-1157283000, 0),
-Offset::hms(9, 30, 0),
"HDT",
(1933, 4, 30, 3, 0, 0, 0),
),
(
(-1157282999, 0),
-Offset::hms(9, 30, 0),
"HDT",
(1933, 4, 30, 3, 0, 1, 0),
),
(
(-1155436201, 0),
-Offset::hms(9, 30, 0),
"HDT",
(1933, 5, 21, 11, 59, 59, 0),
),
(
(-1155436200, 0),
-Offset::hms(10, 30, 0),
"HST",
(1933, 5, 21, 11, 0, 0, 0),
),
(
(-1155436199, 0),
-Offset::hms(10, 30, 0),
"HST",
(1933, 5, 21, 11, 0, 1, 0),
),
(
(-880198201, 0),
-Offset::hms(10, 30, 0),
"HST",
(1942, 2, 9, 1, 59, 59, 0),
),
(
(-880198200, 0),
-Offset::hms(9, 30, 0),
"HWT",
(1942, 2, 9, 3, 0, 0, 0),
),
(
(-880198199, 0),
-Offset::hms(9, 30, 0),
"HWT",
(1942, 2, 9, 3, 0, 1, 0),
),
(
(-769395601, 0),
-Offset::hms(9, 30, 0),
"HWT",
(1945, 8, 14, 13, 29, 59, 0),
),
(
(-769395600, 0),
-Offset::hms(9, 30, 0),
"HPT",
(1945, 8, 14, 13, 30, 0, 0),
),
(
(-769395599, 0),
-Offset::hms(9, 30, 0),
"HPT",
(1945, 8, 14, 13, 30, 1, 0),
),
(
(-765376201, 0),
-Offset::hms(9, 30, 0),
"HPT",
(1945, 9, 30, 1, 59, 59, 0),
),
(
(-765376200, 0),
-Offset::hms(10, 30, 0),
"HST",
(1945, 9, 30, 1, 0, 0, 0),
),
(
(-765376199, 0),
-Offset::hms(10, 30, 0),
"HST",
(1945, 9, 30, 1, 0, 1, 0),
),
(
(-712150201, 0),
-Offset::hms(10, 30, 0),
"HST",
(1947, 6, 8, 1, 59, 59, 0),
),
(
(-712150200, 0),
-Offset::hms(10, 0, 0),
"HST",
(1947, 6, 8, 2, 30, 0, 0),
),
(
(-712150199, 0),
-Offset::hms(10, 0, 0),
"HST",
(1947, 6, 8, 2, 30, 1, 0),
),
],
),
(
"America/Sitka",
&[
((0, 0), o(-8), "PST", (1969, 12, 31, 16, 0, 0, 0)),
(
(-377705023201, 0),
Offset::hms(14, 58, 47),
"LMT",
(-9999, 1, 2, 16, 58, 46, 0),
),
(
(-3225223728, 0),
Offset::hms(14, 58, 47),
"LMT",
(1867, 10, 19, 15, 29, 59, 0),
),
(
(-3225223727, 0),
-Offset::hms(9, 1, 13),
"LMT",
(1867, 10, 18, 15, 30, 0, 0),
),
(
(-3225223726, 0),
-Offset::hms(9, 1, 13),
"LMT",
(1867, 10, 18, 15, 30, 1, 0),
),
],
),
];
for &(tzname, timestamps_to_datetimes) in tests {
let test_file = TzifTestFile::get(tzname);
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
for &((unix_sec, unix_nano), offset, abbrev, datetime) in
timestamps_to_datetimes
{
let (year, month, day, hour, min, sec, nano) = datetime;
let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
let info = tz.to_offset_info(timestamp);
assert_eq!(
info.offset(),
offset,
"\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
);
assert_eq!(
info.abbreviation(),
abbrev,
"\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
);
assert_eq!(
info.offset().to_datetime(timestamp),
date(year, month, day).at(hour, min, sec, nano),
"\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_posix_to_ambiguous_timestamp() {
let tests: &[(&str, &[_])] = &[
(
"EST5",
&[
((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
((2024, 3, 10, 2, 0, 0, 0), unambiguous(-5)),
],
),
(
"EST5EDT,M3.2.0,M11.1.0",
&[
((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
],
),
(
"EST5EDT5,M3.2.0,M11.1.0",
&[
((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
((2024, 3, 10, 2, 0, 0, 0), unambiguous(-5)),
((2024, 3, 10, 2, 59, 59, 999_999_999), unambiguous(-5)),
((2024, 3, 10, 3, 0, 0, 0), unambiguous(-5)),
((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-5)),
((2024, 11, 3, 1, 0, 0, 0), unambiguous(-5)),
((2024, 11, 3, 1, 59, 59, 999_999_999), unambiguous(-5)),
((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
],
),
(
"IST-1GMT0,M10.5.0,M3.5.0/1",
&[
((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
],
),
(
"AEST-10AEDT,M10.1.0,M4.1.0/3",
&[
((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
],
),
(
"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
&[
((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
],
),
(
"NST3:30NDT,M3.2.0,M11.1.0",
&[
(
(1969, 12, 31, 20, 30, 0, 0),
o_unambiguous(-Offset::hms(3, 30, 0)),
),
(
(2024, 3, 10, 1, 59, 59, 999_999_999),
o_unambiguous(-Offset::hms(3, 30, 0)),
),
(
(2024, 3, 10, 2, 0, 0, 0),
o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
),
(
(2024, 3, 10, 2, 59, 59, 999_999_999),
o_gap(-Offset::hms(3, 30, 0), -Offset::hms(2, 30, 0)),
),
(
(2024, 3, 10, 3, 0, 0, 0),
o_unambiguous(-Offset::hms(2, 30, 0)),
),
(
(2024, 11, 3, 0, 59, 59, 999_999_999),
o_unambiguous(-Offset::hms(2, 30, 0)),
),
(
(2024, 11, 3, 1, 0, 0, 0),
o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
),
(
(2024, 11, 3, 1, 59, 59, 999_999_999),
o_fold(-Offset::hms(2, 30, 0), -Offset::hms(3, 30, 0)),
),
(
(2024, 11, 3, 2, 0, 0, 0),
o_unambiguous(-Offset::hms(3, 30, 0)),
),
],
),
];
for &(posix_tz, datetimes_to_ambiguous) in tests {
let tz = TimeZone::posix(posix_tz).unwrap();
for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
let (year, month, day, hour, min, sec, nano) = datetime;
let dt = date(year, month, day).at(hour, min, sec, nano);
let got = tz.to_ambiguous_zoned(dt);
assert_eq!(
got.offset(),
ambiguous_kind,
"\nTZ: {posix_tz}\ndatetime: \
{year:04}-{month:02}-{day:02}T\
{hour:02}:{min:02}:{sec:02}.{nano:09}",
);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_posix_to_datetime() {
let o = |hours| offset(hours);
let tests: &[(&str, &[_])] = &[
("EST5", &[((0, 0), o(-5), (1969, 12, 31, 19, 0, 0, 0))]),
(
"EST5EDT,M3.2.0,M11.1.0",
&[
((0, 0), o(-5), (1969, 12, 31, 19, 0, 0, 0)),
((1710052200, 0), o(-5), (2024, 3, 10, 1, 30, 0, 0)),
(
(1710053999, 999_999_999),
o(-5),
(2024, 3, 10, 1, 59, 59, 999_999_999),
),
((1710054000, 0), o(-4), (2024, 3, 10, 3, 0, 0, 0)),
((1710055800, 0), o(-4), (2024, 3, 10, 3, 30, 0, 0)),
((1730610000, 0), o(-4), (2024, 11, 3, 1, 0, 0, 0)),
((1730611800, 0), o(-4), (2024, 11, 3, 1, 30, 0, 0)),
(
(1730613599, 999_999_999),
o(-4),
(2024, 11, 3, 1, 59, 59, 999_999_999),
),
((1730613600, 0), o(-5), (2024, 11, 3, 1, 0, 0, 0)),
((1730615400, 0), o(-5), (2024, 11, 3, 1, 30, 0, 0)),
],
),
(
"AEST-10AEDT,M10.1.0,M4.1.0/3",
&[
((0, 0), o(11), (1970, 1, 1, 11, 0, 0, 0)),
((1728142200, 0), o(10), (2024, 10, 6, 1, 30, 0, 0)),
(
(1728143999, 999_999_999),
o(10),
(2024, 10, 6, 1, 59, 59, 999_999_999),
),
((1728144000, 0), o(11), (2024, 10, 6, 3, 0, 0, 0)),
((1728145800, 0), o(11), (2024, 10, 6, 3, 30, 0, 0)),
((1712415600, 0), o(11), (2024, 4, 7, 2, 0, 0, 0)),
((1712417400, 0), o(11), (2024, 4, 7, 2, 30, 0, 0)),
(
(1712419199, 999_999_999),
o(11),
(2024, 4, 7, 2, 59, 59, 999_999_999),
),
((1712419200, 0), o(10), (2024, 4, 7, 2, 0, 0, 0)),
((1712421000, 0), o(10), (2024, 4, 7, 2, 30, 0, 0)),
],
),
(
"XXX-24:59:59YYY,M3.2.0,M11.1.0",
&[
(
(1704412800, 0),
Offset::hms(24, 59, 59),
(2024, 1, 6, 0, 59, 59, 0),
),
(
(1717545600, 0),
Offset::hms(25, 59, 59),
(2024, 6, 6, 1, 59, 59, 0),
),
],
),
];
for &(posix_tz, timestamps_to_datetimes) in tests {
let tz = TimeZone::posix(posix_tz).unwrap();
for &((unix_sec, unix_nano), offset, datetime) in
timestamps_to_datetimes
{
let (year, month, day, hour, min, sec, nano) = datetime;
let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
assert_eq!(
tz.to_offset(timestamp),
offset,
"\ntimestamp({unix_sec}, {unix_nano})",
);
assert_eq!(
tz.to_datetime(timestamp),
date(year, month, day).at(hour, min, sec, nano),
"\ntimestamp({unix_sec}, {unix_nano})",
);
}
}
}
#[test]
fn time_zone_fixed_to_datetime() {
let tz = offset(-5).to_time_zone();
let unix_epoch = Timestamp::new(0, 0).unwrap();
assert_eq!(
tz.to_datetime(unix_epoch),
date(1969, 12, 31).at(19, 0, 0, 0),
);
let tz = Offset::from_seconds(93_599).unwrap().to_time_zone();
let timestamp = Timestamp::new(253402207200, 999_999_999).unwrap();
assert_eq!(
tz.to_datetime(timestamp),
date(9999, 12, 31).at(23, 59, 59, 999_999_999),
);
let tz = Offset::from_seconds(-93_599).unwrap().to_time_zone();
let timestamp = Timestamp::new(-377705023201, 0).unwrap();
assert_eq!(
tz.to_datetime(timestamp),
date(-9999, 1, 1).at(0, 0, 0, 0),
);
}
#[test]
fn time_zone_fixed_to_timestamp() {
let tz = offset(-5).to_time_zone();
let dt = date(1969, 12, 31).at(19, 0, 0, 0);
assert_eq!(
tz.to_zoned(dt).unwrap().timestamp(),
Timestamp::new(0, 0).unwrap()
);
let tz = Offset::from_seconds(93_599).unwrap().to_time_zone();
let dt = date(9999, 12, 31).at(23, 59, 59, 999_999_999);
assert_eq!(
tz.to_zoned(dt).unwrap().timestamp(),
Timestamp::new(253402207200, 999_999_999).unwrap(),
);
let tz = Offset::from_seconds(93_598).unwrap().to_time_zone();
assert!(tz.to_zoned(dt).is_err());
let tz = Offset::from_seconds(-93_599).unwrap().to_time_zone();
let dt = date(-9999, 1, 1).at(0, 0, 0, 0);
assert_eq!(
tz.to_zoned(dt).unwrap().timestamp(),
Timestamp::new(-377705023201, 0).unwrap(),
);
let tz = Offset::from_seconds(-93_598).unwrap().to_time_zone();
assert!(tz.to_zoned(dt).is_err());
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_tzif_previous_transition() {
let tests: &[(&str, &[(&str, Option<&str>)])] = &[
(
"UTC",
&[
("1969-12-31T19Z", None),
("2024-03-10T02Z", None),
("-009999-12-01 00Z", None),
("9999-12-01 00Z", None),
],
),
(
"America/New_York",
&[
("2024-03-10 08Z", Some("2024-03-10 07Z")),
("2024-03-10 07:00:00.000000001Z", Some("2024-03-10 07Z")),
("2024-03-10 07Z", Some("2023-11-05 06Z")),
("2023-11-05 06Z", Some("2023-03-12 07Z")),
("-009999-01-31 00Z", None),
("9999-12-01 00Z", Some("9999-11-07 06Z")),
("1969-12-31 19Z", Some("1969-10-26 06Z")),
("2000-04-02 08Z", Some("2000-04-02 07Z")),
("2000-04-02 07:00:00.000000001Z", Some("2000-04-02 07Z")),
("2000-04-02 07Z", Some("1999-10-31 06Z")),
("1999-10-31 06Z", Some("1999-04-04 07Z")),
],
),
(
"Australia/Tasmania",
&[
("2010-04-03 17Z", Some("2010-04-03 16Z")),
("2010-04-03 16:00:00.000000001Z", Some("2010-04-03 16Z")),
("2010-04-03 16Z", Some("2009-10-03 16Z")),
("2009-10-03 16Z", Some("2009-04-04 16Z")),
("-009999-01-31 00Z", None),
("9999-12-01 00Z", Some("9999-10-02 16Z")),
("2000-03-25 17Z", Some("2000-03-25 16Z")),
("2000-03-25 16:00:00.000000001Z", Some("2000-03-25 16Z")),
("2000-03-25 16Z", Some("1999-10-02 16Z")),
("1999-10-02 16Z", Some("1999-03-27 16Z")),
],
),
(
"Europe/Dublin",
&[
("2010-03-28 02Z", Some("2010-03-28 01Z")),
("2010-03-28 01:00:00.000000001Z", Some("2010-03-28 01Z")),
("2010-03-28 01Z", Some("2009-10-25 01Z")),
("2009-10-25 01Z", Some("2009-03-29 01Z")),
("-009999-01-31 00Z", None),
("9999-12-01 00Z", Some("9999-10-31 01Z")),
("1990-03-25 02Z", Some("1990-03-25 01Z")),
("1990-03-25 01:00:00.000000001Z", Some("1990-03-25 01Z")),
("1990-03-25 01Z", Some("1989-10-29 01Z")),
("1989-10-25 01Z", Some("1989-03-26 01Z")),
],
),
(
"America/Sao_Paulo",
&[("2024-03-10 08Z", Some("2019-02-17 02Z"))],
),
];
for &(tzname, prev_trans) in tests {
if tzname != "America/Sao_Paulo" {
continue;
}
let test_file = TzifTestFile::get(tzname);
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
for (given, expected) in prev_trans {
let given: Timestamp = given.parse().unwrap();
let expected =
expected.map(|s| s.parse::<Timestamp>().unwrap());
let got = tz.previous_transition(given).map(|t| t.timestamp());
assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_tzif_next_transition() {
let tests: &[(&str, &[(&str, Option<&str>)])] = &[
(
"UTC",
&[
("1969-12-31T19Z", None),
("2024-03-10T02Z", None),
("-009999-12-01 00Z", None),
("9999-12-01 00Z", None),
],
),
(
"America/New_York",
&[
("2024-03-10 06Z", Some("2024-03-10 07Z")),
("2024-03-10 06:59:59.999999999Z", Some("2024-03-10 07Z")),
("2024-03-10 07Z", Some("2024-11-03 06Z")),
("2024-11-03 06Z", Some("2025-03-09 07Z")),
("-009999-12-01 00Z", Some("1883-11-18 17Z")),
("9999-12-01 00Z", None),
("1969-12-31 19Z", Some("1970-04-26 07Z")),
("2000-04-02 06Z", Some("2000-04-02 07Z")),
("2000-04-02 06:59:59.999999999Z", Some("2000-04-02 07Z")),
("2000-04-02 07Z", Some("2000-10-29 06Z")),
("2000-10-29 06Z", Some("2001-04-01 07Z")),
],
),
(
"Australia/Tasmania",
&[
("2010-04-03 15Z", Some("2010-04-03 16Z")),
("2010-04-03 15:59:59.999999999Z", Some("2010-04-03 16Z")),
("2010-04-03 16Z", Some("2010-10-02 16Z")),
("2010-10-02 16Z", Some("2011-04-02 16Z")),
("-009999-12-01 00Z", Some("1895-08-31 14:10:44Z")),
("9999-12-01 00Z", None),
("2000-03-25 15Z", Some("2000-03-25 16Z")),
("2000-03-25 15:59:59.999999999Z", Some("2000-03-25 16Z")),
("2000-03-25 16Z", Some("2000-08-26 16Z")),
("2000-08-26 16Z", Some("2001-03-24 16Z")),
],
),
(
"Europe/Dublin",
&[
("2010-03-28 00Z", Some("2010-03-28 01Z")),
("2010-03-28 00:59:59.999999999Z", Some("2010-03-28 01Z")),
("2010-03-28 01Z", Some("2010-10-31 01Z")),
("2010-10-31 01Z", Some("2011-03-27 01Z")),
("-009999-12-01 00Z", Some("1880-08-02 00:25:21Z")),
("9999-12-01 00Z", None),
("1990-03-25 00Z", Some("1990-03-25 01Z")),
("1990-03-25 00:59:59.999999999Z", Some("1990-03-25 01Z")),
("1990-03-25 01Z", Some("1990-10-28 01Z")),
("1990-10-28 01Z", Some("1991-03-31 01Z")),
],
),
(
"America/Sao_Paulo",
&[("2024-03-10 08Z", None)],
),
];
for &(tzname, next_trans) in tests {
let test_file = TzifTestFile::get(tzname);
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
for (given, expected) in next_trans {
let given: Timestamp = given.parse().unwrap();
let expected =
expected.map(|s| s.parse::<Timestamp>().unwrap());
let got = tz.next_transition(given).map(|t| t.timestamp());
assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_posix_previous_transition() {
let tests: &[(&str, &[(&str, Option<&str>)])] = &[
(
"EST5",
&[
("1969-12-31T19Z", None),
("2024-03-10T02Z", None),
("-009999-12-01 00Z", None),
("9999-12-01 00Z", None),
],
),
(
"EST5EDT,M3.2.0,M11.1.0",
&[
("1969-12-31 19Z", Some("1969-11-02 06Z")),
("2024-03-10 08Z", Some("2024-03-10 07Z")),
("2024-03-10 07:00:00.000000001Z", Some("2024-03-10 07Z")),
("2024-03-10 07Z", Some("2023-11-05 06Z")),
("2023-11-05 06Z", Some("2023-03-12 07Z")),
("-009999-01-31 00Z", None),
("9999-12-01 00Z", Some("9999-11-07 06Z")),
],
),
(
"AEST-10AEDT,M10.1.0,M4.1.0/3",
&[
("2010-04-03 17Z", Some("2010-04-03 16Z")),
("2010-04-03 16:00:00.000000001Z", Some("2010-04-03 16Z")),
("2010-04-03 16Z", Some("2009-10-03 16Z")),
("2009-10-03 16Z", Some("2009-04-04 16Z")),
("-009999-01-31 00Z", None),
("9999-12-01 00Z", Some("9999-10-02 16Z")),
],
),
(
"IST-1GMT0,M10.5.0,M3.5.0/1",
&[
("2010-03-28 02Z", Some("2010-03-28 01Z")),
("2010-03-28 01:00:00.000000001Z", Some("2010-03-28 01Z")),
("2010-03-28 01Z", Some("2009-10-25 01Z")),
("2009-10-25 01Z", Some("2009-03-29 01Z")),
("-009999-01-31 00Z", None),
("9999-12-01 00Z", Some("9999-10-31 01Z")),
],
),
];
for &(posix_tz, prev_trans) in tests {
let tz = TimeZone::posix(posix_tz).unwrap();
for (given, expected) in prev_trans {
let given: Timestamp = given.parse().unwrap();
let expected =
expected.map(|s| s.parse::<Timestamp>().unwrap());
let got = tz.previous_transition(given).map(|t| t.timestamp());
assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_posix_next_transition() {
let tests: &[(&str, &[(&str, Option<&str>)])] = &[
(
"EST5",
&[
("1969-12-31T19Z", None),
("2024-03-10T02Z", None),
("-009999-12-01 00Z", None),
("9999-12-01 00Z", None),
],
),
(
"EST5EDT,M3.2.0,M11.1.0",
&[
("1969-12-31 19Z", Some("1970-03-08 07Z")),
("2024-03-10 06Z", Some("2024-03-10 07Z")),
("2024-03-10 06:59:59.999999999Z", Some("2024-03-10 07Z")),
("2024-03-10 07Z", Some("2024-11-03 06Z")),
("2024-11-03 06Z", Some("2025-03-09 07Z")),
("-009999-12-01 00Z", Some("-009998-03-10 07Z")),
("9999-12-01 00Z", None),
],
),
(
"AEST-10AEDT,M10.1.0,M4.1.0/3",
&[
("2010-04-03 15Z", Some("2010-04-03 16Z")),
("2010-04-03 15:59:59.999999999Z", Some("2010-04-03 16Z")),
("2010-04-03 16Z", Some("2010-10-02 16Z")),
("2010-10-02 16Z", Some("2011-04-02 16Z")),
("-009999-12-01 00Z", Some("-009998-04-06 16Z")),
("9999-12-01 00Z", None),
],
),
(
"IST-1GMT0,M10.5.0,M3.5.0/1",
&[
("2010-03-28 00Z", Some("2010-03-28 01Z")),
("2010-03-28 00:59:59.999999999Z", Some("2010-03-28 01Z")),
("2010-03-28 01Z", Some("2010-10-31 01Z")),
("2010-10-31 01Z", Some("2011-03-27 01Z")),
("-009999-12-01 00Z", Some("-009998-03-31 01Z")),
("9999-12-01 00Z", None),
],
),
];
for &(posix_tz, next_trans) in tests {
let tz = TimeZone::posix(posix_tz).unwrap();
for (given, expected) in next_trans {
let given: Timestamp = given.parse().unwrap();
let expected =
expected.map(|s| s.parse::<Timestamp>().unwrap());
let got = tz.next_transition(given).map(|t| t.timestamp());
assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
}
}
}
#[test]
fn time_zone_size() {
#[cfg(feature = "alloc")]
{
let word = core::mem::size_of::<usize>();
assert_eq!(word, core::mem::size_of::<TimeZone>());
}
#[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
{
#[cfg(debug_assertions)]
{
assert_eq!(8, core::mem::size_of::<TimeZone>());
}
#[cfg(not(debug_assertions))]
{
assert_eq!(8, core::mem::size_of::<TimeZone>());
}
}
}
#[test]
fn time_zone_to_offset() {
let ts = Timestamp::from_second(123456789).unwrap();
let tz = TimeZone::fixed(offset(-5));
let info = tz.to_offset_info(ts);
assert_eq!(info.offset(), offset(-5));
assert_eq!(info.dst(), Dst::No);
assert_eq!(info.abbreviation(), "-05");
let tz = TimeZone::fixed(offset(5));
let info = tz.to_offset_info(ts);
assert_eq!(info.offset(), offset(5));
assert_eq!(info.dst(), Dst::No);
assert_eq!(info.abbreviation(), "+05");
let tz = TimeZone::fixed(offset(-12));
let info = tz.to_offset_info(ts);
assert_eq!(info.offset(), offset(-12));
assert_eq!(info.dst(), Dst::No);
assert_eq!(info.abbreviation(), "-12");
let tz = TimeZone::fixed(offset(12));
let info = tz.to_offset_info(ts);
assert_eq!(info.offset(), offset(12));
assert_eq!(info.dst(), Dst::No);
assert_eq!(info.abbreviation(), "+12");
let tz = TimeZone::fixed(offset(0));
let info = tz.to_offset_info(ts);
assert_eq!(info.offset(), offset(0));
assert_eq!(info.dst(), Dst::No);
assert_eq!(info.abbreviation(), "UTC");
}
#[test]
fn time_zone_to_fixed_offset() {
let tz = TimeZone::UTC;
assert_eq!(tz.to_fixed_offset().unwrap(), Offset::UTC);
let offset = Offset::from_hours(1).unwrap();
let tz = TimeZone::fixed(offset);
assert_eq!(tz.to_fixed_offset().unwrap(), offset);
#[cfg(feature = "alloc")]
{
let tz = TimeZone::posix("EST5").unwrap();
assert!(tz.to_fixed_offset().is_err());
let test_file = TzifTestFile::get("America/New_York");
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
assert!(tz.to_fixed_offset().is_err());
}
}
#[cfg(feature = "alloc")]
#[test]
fn time_zone_following_boa_vista() {
use alloc::{vec, vec::Vec};
let test_file = TzifTestFile::get("America/Boa_Vista");
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
let last4: Vec<Timestamp> = vec![
"1999-10-03T04Z".parse().unwrap(),
"2000-02-27T03Z".parse().unwrap(),
"2000-10-08T04Z".parse().unwrap(),
"2000-10-15T03Z".parse().unwrap(),
];
let start: Timestamp = "2001-01-01T00Z".parse().unwrap();
let mut transitions: Vec<Timestamp> =
tz.preceding(start).take(4).map(|t| t.timestamp()).collect();
transitions.reverse();
assert_eq!(transitions, last4);
let start: Timestamp = "1990-01-01T00Z".parse().unwrap();
let transitions: Vec<Timestamp> =
tz.following(start).map(|t| t.timestamp()).collect();
assert_eq!(transitions, last4);
}
#[cfg(feature = "alloc")]
#[test]
fn regression_tzif_parse_panic() {
_ = TimeZone::tzif(
"",
&[
84, 90, 105, 102, 6, 0, 5, 35, 84, 10, 77, 0, 0, 0, 84, 82,
105, 102, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 82, 28, 77, 0, 0, 90, 105,
78, 0, 0, 0, 0, 0, 0, 0, 84, 90, 105, 102, 0, 0, 5, 0, 84, 90,
105, 84, 77, 10, 0, 0, 0, 15, 93, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 82, 0, 64, 1, 0,
0, 2, 0, 0, 0, 0, 0, 0, 126, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 0, 0, 0, 0, 0,
0, 160, 109, 1, 0, 90, 105, 102, 0, 0, 5, 0, 87, 90, 105, 84,
77, 10, 0, 0, 0, 0, 0, 122, 102, 105, 0, 0, 0, 0, 0, 0, 0, 0,
2, 0, 0, 0, 0, 0, 0, 5, 82, 0, 0, 0, 0, 0, 2, 0, 0, 90, 105,
102, 0, 0, 5, 0, 84, 90, 105, 84, 77, 10, 0, 0, 0, 102, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 90, 195, 190, 10, 84,
90, 77, 49, 84, 90, 105, 102, 49, 44, 74, 51, 44, 50, 10,
],
);
}
#[cfg(feature = "alloc")]
#[test]
fn regression_tz_lookup_datetime_min() {
use alloc::string::ToString;
let test_file = TzifTestFile::get("America/Boa_Vista");
let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
let err = tz.to_timestamp(DateTime::MIN).unwrap_err();
assert_eq!(
err.to_string(),
"converting datetime with time zone offset `-04:02:40` to timestamp overflowed: parameter 'Unix timestamp seconds' is not in the required range of -377705023201..=253402207200",
);
}
}