use alloc::{
boxed::Box,
string::{String, ToString},
sync::Arc,
};
use crate::{
civil::DateTime,
error::{err, Error, ErrorContext},
Timestamp, Zoned,
};
use self::{posix::ReasonablePosixTimeZone, tzif::Tzif};
pub use self::{
db::{db, TimeZoneDatabase, TimeZoneNameIter},
offset::{Dst, Offset, OffsetArithmetic, OffsetConflict},
};
mod db;
mod offset;
mod posix;
#[cfg(feature = "tz-system")]
mod system;
#[cfg(test)]
mod testdata;
mod tzif;
#[cfg(test)]
mod zic;
#[derive(Clone, Eq, PartialEq)]
pub struct TimeZone {
kind: Option<Arc<TimeZoneKind>>,
}
impl TimeZone {
pub const UTC: TimeZone = TimeZone { kind: None };
#[inline]
pub fn system() -> TimeZone {
match TimeZone::try_system() {
Ok(tz) => tz,
Err(_err) => {
warn!(
"failed to get system time zone, \
falling back to UTC: {_err}",
);
TimeZone::UTC
}
}
}
#[inline]
pub fn try_system() -> Result<TimeZone, Error> {
#[cfg(not(feature = "tz-system"))]
{
Err(err!(
"failed to get system time zone since 'tz-system' \
crate feature is not enabled",
))
}
#[cfg(feature = "tz-system")]
{
self::system::get(db())
}
}
#[inline]
pub fn get(time_zone_name: &str) -> Result<TimeZone, Error> {
db().get(time_zone_name)
}
#[inline]
pub fn fixed(offset: Offset) -> TimeZone {
if offset == Offset::UTC {
return TimeZone::UTC;
}
let fixed = TimeZoneFixed::new(offset);
let kind = TimeZoneKind::Fixed(fixed);
TimeZone { kind: Some(Arc::new(kind)) }
}
pub fn posix(posix_tz_string: &str) -> Result<TimeZone, Error> {
let posix = TimeZonePosix::new(posix_tz_string)?;
let kind = TimeZoneKind::Posix(posix);
Ok(TimeZone { kind: Some(Arc::new(kind)) })
}
pub fn tzif(name: &str, data: &[u8]) -> Result<TimeZone, Error> {
let tzif = TimeZoneTzif::new(Some(name.to_string()), data)?;
let kind = TimeZoneKind::Tzif(tzif);
Ok(TimeZone { kind: Some(Arc::new(kind)) })
}
fn tzif_system(data: &[u8]) -> Result<TimeZone, Error> {
let tzif = TimeZoneTzif::new(None, data)?;
let kind = TimeZoneKind::Tzif(tzif);
Ok(TimeZone { kind: Some(Arc::new(kind)) })
}
#[inline]
pub(crate) fn diagnostic_name(&self) -> &str {
let Some(ref kind) = self.kind else { return "UTC" };
match **kind {
TimeZoneKind::Fixed(ref tz) => tz.name(),
TimeZoneKind::Posix(ref tz) => tz.name(),
TimeZoneKind::Tzif(ref tz) => tz.name().unwrap_or("Local"),
}
}
#[inline]
pub fn iana_name(&self) -> Option<&str> {
let Some(ref kind) = self.kind else { return Some("UTC") };
match **kind {
TimeZoneKind::Tzif(ref tz) => tz.name(),
_ => None,
}
}
#[inline]
pub fn to_datetime(&self, timestamp: Timestamp) -> DateTime {
let (offset, _, _) = self.to_offset(timestamp);
offset.to_datetime(timestamp)
}
#[inline]
pub fn to_offset(&self, timestamp: Timestamp) -> (Offset, Dst, &str) {
let Some(ref kind) = self.kind else {
return (Offset::UTC, Dst::No, "UTC");
};
match **kind {
TimeZoneKind::Fixed(ref tz) => (tz.offset(), Dst::No, tz.name()),
TimeZoneKind::Posix(ref tz) => tz.to_offset(timestamp),
TimeZoneKind::Tzif(ref tz) => tz.to_offset(timestamp),
}
}
#[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 = match self.kind {
None => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
Some(ref kind) => match **kind {
TimeZoneKind::Fixed(ref tz) => {
AmbiguousOffset::Unambiguous { offset: tz.offset() }
}
TimeZoneKind::Posix(ref tz) => tz.to_ambiguous_kind(dt),
TimeZoneKind::Tzif(ref tz) => tz.to_ambiguous_kind(dt),
},
};
AmbiguousTimestamp::new(dt, ambiguous_kind)
}
#[allow(dead_code)]
#[inline]
fn previous_transition(&self, timestamp: Timestamp) -> Option<Timestamp> {
match **self.kind.as_ref()? {
TimeZoneKind::Fixed(_) => None,
TimeZoneKind::Posix(ref tz) => tz.previous_transition(timestamp),
TimeZoneKind::Tzif(ref tz) => tz.previous_transition(timestamp),
}
}
#[allow(dead_code)]
#[inline]
fn next_transition(&self, timestamp: Timestamp) -> Option<Timestamp> {
match **self.kind.as_ref()? {
TimeZoneKind::Fixed(_) => None,
TimeZoneKind::Posix(ref tz) => tz.next_transition(timestamp),
TimeZoneKind::Tzif(ref tz) => tz.next_transition(timestamp),
}
}
}
impl core::fmt::Debug for TimeZone {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let field: &dyn core::fmt::Debug = match self.kind {
None => &"UTC",
Some(ref kind) => match &**kind {
TimeZoneKind::Fixed(ref tz) => tz,
TimeZoneKind::Posix(ref tz) => tz,
TimeZoneKind::Tzif(ref tz) => tz,
},
};
f.debug_tuple("TimeZone").field(field).finish()
}
}
#[derive(Debug, Eq, PartialEq)]
enum TimeZoneKind {
Fixed(TimeZoneFixed),
Posix(TimeZonePosix),
Tzif(TimeZoneTzif),
}
#[derive(Clone)]
struct TimeZoneFixed {
offset: Offset,
name: Box<str>,
}
impl TimeZoneFixed {
#[inline]
fn new(offset: Offset) -> TimeZoneFixed {
let name = offset.to_string().into();
TimeZoneFixed { offset, name }
}
#[inline]
fn name(&self) -> &str {
&self.name
}
#[inline]
fn offset(&self) -> Offset {
self.offset
}
}
impl core::fmt::Debug for TimeZoneFixed {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_tuple("Fixed").field(&self.offset()).finish()
}
}
impl Eq for TimeZoneFixed {}
impl PartialEq for TimeZoneFixed {
#[inline]
fn eq(&self, rhs: &TimeZoneFixed) -> bool {
self.offset() == rhs.offset()
}
}
#[derive(Eq, PartialEq)]
struct TimeZonePosix {
name: Box<str>,
posix: ReasonablePosixTimeZone,
}
impl TimeZonePosix {
#[inline]
fn new(s: &str) -> Result<TimeZonePosix, Error> {
let iana_tz = posix::IanaTz::parse_v3plus(s)?;
Ok(TimeZonePosix::from(iana_tz.into_tz()))
}
#[inline]
fn name(&self) -> &str {
&self.name
}
#[inline]
fn to_offset(&self, timestamp: Timestamp) -> (Offset, Dst, &str) {
self.posix.to_offset(timestamp)
}
#[inline]
fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
self.posix.to_ambiguous_kind(dt)
}
#[inline]
fn previous_transition(&self, timestamp: Timestamp) -> Option<Timestamp> {
self.posix.previous_transition(timestamp)
}
#[inline]
fn next_transition(&self, timestamp: Timestamp) -> Option<Timestamp> {
self.posix.next_transition(timestamp)
}
}
impl From<ReasonablePosixTimeZone> for TimeZonePosix {
#[inline]
fn from(posix: ReasonablePosixTimeZone) -> TimeZonePosix {
let name = posix.as_str().to_string().into();
TimeZonePosix { name, posix }
}
}
impl core::fmt::Debug for TimeZonePosix {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_tuple("Posix").field(&self.posix.as_str()).finish()
}
}
#[derive(Eq, PartialEq)]
struct TimeZoneTzif {
tzif: Tzif,
}
impl TimeZoneTzif {
#[inline]
fn new(name: Option<String>, bytes: &[u8]) -> Result<TimeZoneTzif, Error> {
let tzif = Tzif::parse(name, bytes)?;
Ok(TimeZoneTzif { tzif })
}
#[inline]
fn name(&self) -> Option<&str> {
self.tzif.name()
}
#[inline]
fn to_offset(&self, timestamp: Timestamp) -> (Offset, Dst, &str) {
self.tzif.to_offset(timestamp)
}
#[inline]
fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
self.tzif.to_ambiguous_kind(dt)
}
#[inline]
fn previous_transition(&self, timestamp: Timestamp) -> Option<Timestamp> {
self.tzif.previous_transition(timestamp)
}
#[inline]
fn next_transition(&self, timestamp: Timestamp) -> Option<Timestamp> {
self.tzif.next_transition(timestamp)
}
}
impl core::fmt::Debug for TimeZoneTzif {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_tuple("TZif").field(&self.name().unwrap_or("Local")).finish()
}
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub enum Disambiguation {
#[default]
Compatible,
Earlier,
Later,
Reject,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AmbiguousOffset {
Unambiguous {
offset: Offset,
},
Gap {
before: Offset,
after: Offset,
},
Fold {
before: Offset,
after: Offset,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AmbiguousTimestamp {
dt: DateTime,
offset: AmbiguousOffset,
}
impl AmbiguousTimestamp {
#[inline]
fn new(dt: DateTime, kind: AmbiguousOffset) -> AmbiguousTimestamp {
AmbiguousTimestamp { dt, offset: kind }
}
#[inline]
pub fn datetime(&self) -> DateTime {
self.dt
}
#[inline]
pub fn offset(&self) -> AmbiguousOffset {
self.offset
}
#[inline]
pub fn is_ambiguous(&self) -> bool {
!matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
}
#[inline]
pub fn compatible(self) -> Result<Timestamp, Error> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, .. } => before,
AmbiguousOffset::Fold { before, .. } => before,
};
offset.to_timestamp(self.dt)
}
#[inline]
pub fn earlier(self) -> Result<Timestamp, Error> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { after, .. } => after,
AmbiguousOffset::Fold { before, .. } => before,
};
offset.to_timestamp(self.dt)
}
#[inline]
pub fn later(self) -> Result<Timestamp, Error> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, .. } => before,
AmbiguousOffset::Fold { after, .. } => after,
};
offset.to_timestamp(self.dt)
}
#[inline]
pub fn unambiguous(self) -> Result<Timestamp, Error> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, after } => {
return Err(err!(
"the datetime {dt} is ambiguous since it falls into \
a gap between offsets {before} and {after}",
dt = self.dt,
));
}
AmbiguousOffset::Fold { before, after } => {
return Err(err!(
"the datetime {dt} is ambiguous since it falls into \
a fold between offsets {before} and {after}",
dt = self.dt,
));
}
};
offset.to_timestamp(self.dt)
}
#[inline]
pub fn disambiguate(
self,
option: Disambiguation,
) -> Result<Timestamp, Error> {
match option {
Disambiguation::Compatible => self.compatible(),
Disambiguation::Earlier => self.earlier(),
Disambiguation::Later => self.later(),
Disambiguation::Reject => self.unambiguous(),
}
}
#[inline]
fn into_ambiguous_zoned(self, tz: TimeZone) -> AmbiguousZoned {
AmbiguousZoned::new(self, tz)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AmbiguousZoned {
ts: AmbiguousTimestamp,
tz: TimeZone,
}
impl AmbiguousZoned {
#[inline]
fn new(ts: AmbiguousTimestamp, tz: TimeZone) -> AmbiguousZoned {
AmbiguousZoned { ts, tz }
}
#[inline]
pub fn time_zone(&self) -> &TimeZone {
&self.tz
}
#[inline]
pub fn into_time_zone(self) -> TimeZone {
self.tz
}
#[inline]
pub fn datetime(&self) -> DateTime {
self.ts.datetime()
}
#[inline]
pub fn offset(&self) -> AmbiguousOffset {
self.ts.offset
}
#[inline]
pub fn is_ambiguous(&self) -> bool {
!matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
}
#[inline]
pub fn compatible(self) -> Result<Zoned, Error> {
let ts = self.ts.compatible().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
#[inline]
pub fn earlier(self) -> Result<Zoned, Error> {
let ts = self.ts.earlier().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
#[inline]
pub fn later(self) -> Result<Zoned, Error> {
let ts = self.ts.later().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
#[inline]
pub fn unambiguous(self) -> Result<Zoned, Error> {
let ts = self.ts.unambiguous().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
Ok(ts.to_zoned(self.tz))
}
#[inline]
pub fn disambiguate(self, option: Disambiguation) -> Result<Zoned, Error> {
match option {
Disambiguation::Compatible => self.compatible(),
Disambiguation::Earlier => self.earlier(),
Disambiguation::Later => self.later(),
Disambiguation::Reject => self.unambiguous(),
}
}
}
#[inline]
pub const fn offset(hours: i8) -> Offset {
Offset::constant(hours)
}
#[cfg(test)]
mod tests {
use crate::{civil::date, tz::testdata::TzifTestFile};
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 }
}
#[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}",
);
}
}
}
#[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 (got_offset, _, got_abbrev) = tz.to_offset(timestamp);
assert_eq!(
got_offset, offset,
"\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
);
assert_eq!(
got_abbrev, abbrev,
"\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
);
assert_eq!(
got_offset.to_datetime(timestamp),
date(year, month, day).at(hour, min, sec, nano),
"\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
);
}
}
}
#[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}",
);
}
}
}
#[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).0,
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());
}
#[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")),
],
),
];
for &(tzname, prev_trans) in tests {
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);
assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
}
}
}
#[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")),
],
),
];
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);
assert_eq!(got, expected, "\nTZ: {tzname}\ngiven: {given}");
}
}
}
#[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);
assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
}
}
}
#[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);
assert_eq!(got, expected, "\nTZ: {posix_tz}\ngiven: {given}");
}
}
}
#[test]
fn time_zone_size() {
let word = core::mem::size_of::<usize>();
assert_eq!(word, core::mem::size_of::<TimeZone>());
}
}