diff options
Diffstat (limited to 'src/offset')
-rw-r--r-- | src/offset/fixed.rs | 212 | ||||
-rw-r--r-- | src/offset/local.rs | 227 | ||||
-rw-r--r-- | src/offset/local/mod.rs | 537 | ||||
-rw-r--r-- | src/offset/local/tz_info/mod.rs | 116 | ||||
-rw-r--r-- | src/offset/local/tz_info/parser.rs | 333 | ||||
-rw-r--r-- | src/offset/local/tz_info/rule.rs | 1045 | ||||
-rw-r--r-- | src/offset/local/tz_info/timezone.rs | 950 | ||||
-rw-r--r-- | src/offset/local/unix.rs | 171 | ||||
-rw-r--r-- | src/offset/local/win_bindings.rs | 71 | ||||
-rw-r--r-- | src/offset/local/win_bindings.txt | 7 | ||||
-rw-r--r-- | src/offset/local/windows.rs | 262 | ||||
-rw-r--r-- | src/offset/mod.rs | 308 | ||||
-rw-r--r-- | src/offset/utc.rs | 98 |
13 files changed, 3884 insertions, 453 deletions
diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs index 83f42a1..8f37558 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -4,22 +4,29 @@ //! The time zone which has a fixed offset from UTC. use core::fmt; -use core::ops::{Add, Sub}; -use oldtime::Duration as OldDuration; +use core::str::FromStr; + +#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] +use rkyv::{Archive, Deserialize, Serialize}; use super::{LocalResult, Offset, TimeZone}; -use div::div_mod_floor; -use naive::{NaiveDate, NaiveDateTime, NaiveTime}; -use DateTime; -use Timelike; +use crate::format::{scan, ParseError, OUT_OF_RANGE}; +use crate::naive::{NaiveDate, NaiveDateTime}; /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. /// /// Using the [`TimeZone`](./trait.TimeZone.html) methods /// on a `FixedOffset` struct is the preferred way to construct -/// `DateTime<FixedOffset>` instances. See the [`east`](#method.east) and -/// [`west`](#method.west) methods for examples. +/// `DateTime<FixedOffset>` instances. See the [`east_opt`](#method.east_opt) and +/// [`west_opt`](#method.west_opt) methods for examples. #[derive(PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), + derive(Archive, Deserialize, Serialize), + archive(compare(PartialEq)), + archive_attr(derive(Clone, Copy, PartialEq, Eq, Hash, Debug)) +)] +#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] pub struct FixedOffset { local_minus_utc: i32, } @@ -29,16 +36,8 @@ impl FixedOffset { /// The negative `secs` means the Western Hemisphere. /// /// Panics on the out-of-bound `secs`. - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{FixedOffset, TimeZone}; - /// let hour = 3600; - /// let datetime = FixedOffset::east(5 * hour).ymd(2016, 11, 08) - /// .and_hms(0, 0, 0); - /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00") - /// ~~~~ + #[deprecated(since = "0.4.23", note = "use `east_opt()` instead")] + #[must_use] pub fn east(secs: i32) -> FixedOffset { FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds") } @@ -47,7 +46,21 @@ impl FixedOffset { /// The negative `secs` means the Western Hemisphere. /// /// Returns `None` on the out-of-bound `secs`. - pub fn east_opt(secs: i32) -> Option<FixedOffset> { + /// + /// # Example + /// + #[cfg_attr(not(feature = "std"), doc = "```ignore")] + #[cfg_attr(feature = "std", doc = "```")] + /// use chrono::{FixedOffset, TimeZone}; + /// let hour = 3600; + /// let datetime = FixedOffset::east_opt(5 * hour) + /// .unwrap() + /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0) + /// .unwrap(); + /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00") + /// ``` + #[must_use] + pub const fn east_opt(secs: i32) -> Option<FixedOffset> { if -86_400 < secs && secs < 86_400 { Some(FixedOffset { local_minus_utc: secs }) } else { @@ -59,16 +72,8 @@ impl FixedOffset { /// The negative `secs` means the Eastern Hemisphere. /// /// Panics on the out-of-bound `secs`. - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{FixedOffset, TimeZone}; - /// let hour = 3600; - /// let datetime = FixedOffset::west(5 * hour).ymd(2016, 11, 08) - /// .and_hms(0, 0, 0); - /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00") - /// ~~~~ + #[deprecated(since = "0.4.23", note = "use `west_opt()` instead")] + #[must_use] pub fn west(secs: i32) -> FixedOffset { FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds") } @@ -77,7 +82,21 @@ impl FixedOffset { /// The negative `secs` means the Eastern Hemisphere. /// /// Returns `None` on the out-of-bound `secs`. - pub fn west_opt(secs: i32) -> Option<FixedOffset> { + /// + /// # Example + /// + #[cfg_attr(not(feature = "std"), doc = "```ignore")] + #[cfg_attr(feature = "std", doc = "```")] + /// use chrono::{FixedOffset, TimeZone}; + /// let hour = 3600; + /// let datetime = FixedOffset::west_opt(5 * hour) + /// .unwrap() + /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0) + /// .unwrap(); + /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00") + /// ``` + #[must_use] + pub const fn west_opt(secs: i32) -> Option<FixedOffset> { if -86_400 < secs && secs < 86_400 { Some(FixedOffset { local_minus_utc: -secs }) } else { @@ -87,17 +106,26 @@ impl FixedOffset { /// Returns the number of seconds to add to convert from UTC to the local time. #[inline] - pub fn local_minus_utc(&self) -> i32 { + pub const fn local_minus_utc(&self) -> i32 { self.local_minus_utc } /// Returns the number of seconds to add to convert from the local time to UTC. #[inline] - pub fn utc_minus_local(&self) -> i32 { + pub const fn utc_minus_local(&self) -> i32 { -self.local_minus_utc } } +/// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime). +impl FromStr for FixedOffset { + type Err = ParseError; + fn from_str(s: &str) -> Result<Self, Self::Err> { + let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?; + Self::east_opt(offset).ok_or(OUT_OF_RANGE) + } +} + impl TimeZone for FixedOffset { type Offset = FixedOffset; @@ -130,8 +158,10 @@ impl fmt::Debug for FixedOffset { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let offset = self.local_minus_utc; let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) }; - let (mins, sec) = div_mod_floor(offset, 60); - let (hour, min) = div_mod_floor(mins, 60); + let sec = offset.rem_euclid(60); + let mins = offset.div_euclid(60); + let min = mins.rem_euclid(60); + let hour = mins.div_euclid(60); if sec == 0 { write!(f, "{}{:02}:{:02}", sign, hour, min) } else { @@ -146,99 +176,63 @@ impl fmt::Display for FixedOffset { } } -// addition or subtraction of FixedOffset to/from Timelike values is the same as -// adding or subtracting the offset's local_minus_utc value -// but keep keeps the leap second information. -// this should be implemented more efficiently, but for the time being, this is generic right now. - -fn add_with_leapsecond<T>(lhs: &T, rhs: i32) -> T -where - T: Timelike + Add<OldDuration, Output = T>, -{ - // extract and temporarily remove the fractional part and later recover it - let nanos = lhs.nanosecond(); - let lhs = lhs.with_nanosecond(0).unwrap(); - (lhs + OldDuration::seconds(i64::from(rhs))).with_nanosecond(nanos).unwrap() -} - -impl Add<FixedOffset> for NaiveTime { - type Output = NaiveTime; - - #[inline] - fn add(self, rhs: FixedOffset) -> NaiveTime { - add_with_leapsecond(&self, rhs.local_minus_utc) - } -} - -impl Sub<FixedOffset> for NaiveTime { - type Output = NaiveTime; - - #[inline] - fn sub(self, rhs: FixedOffset) -> NaiveTime { - add_with_leapsecond(&self, -rhs.local_minus_utc) - } -} - -impl Add<FixedOffset> for NaiveDateTime { - type Output = NaiveDateTime; - - #[inline] - fn add(self, rhs: FixedOffset) -> NaiveDateTime { - add_with_leapsecond(&self, rhs.local_minus_utc) - } -} - -impl Sub<FixedOffset> for NaiveDateTime { - type Output = NaiveDateTime; - - #[inline] - fn sub(self, rhs: FixedOffset) -> NaiveDateTime { - add_with_leapsecond(&self, -rhs.local_minus_utc) - } -} - -impl<Tz: TimeZone> Add<FixedOffset> for DateTime<Tz> { - type Output = DateTime<Tz>; - - #[inline] - fn add(self, rhs: FixedOffset) -> DateTime<Tz> { - add_with_leapsecond(&self, rhs.local_minus_utc) - } -} - -impl<Tz: TimeZone> Sub<FixedOffset> for DateTime<Tz> { - type Output = DateTime<Tz>; - - #[inline] - fn sub(self, rhs: FixedOffset) -> DateTime<Tz> { - add_with_leapsecond(&self, -rhs.local_minus_utc) +#[cfg(all(feature = "arbitrary", feature = "std"))] +impl arbitrary::Arbitrary<'_> for FixedOffset { + fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset> { + let secs = u.int_in_range(-86_399..=86_399)?; + let fixed_offset = FixedOffset::east_opt(secs) + .expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous."); + Ok(fixed_offset) } } #[cfg(test)] mod tests { use super::FixedOffset; - use offset::TimeZone; + use crate::offset::TimeZone; + use std::str::FromStr; #[test] fn test_date_extreme_offset() { // starting from 0.3 we don't have an offset exceeding one day. // this makes everything easier! + let offset = FixedOffset::east_opt(86399).unwrap(); assert_eq!( - format!("{:?}", FixedOffset::east(86399).ymd(2012, 2, 29)), - "2012-02-29+23:59:59".to_string() + format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), + "2012-02-29T05:06:07+23:59:59" ); + let offset = FixedOffset::east_opt(-86399).unwrap(); assert_eq!( - format!("{:?}", FixedOffset::east(86399).ymd(2012, 2, 29).and_hms(5, 6, 7)), - "2012-02-29T05:06:07+23:59:59".to_string() + format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), + "2012-02-29T05:06:07-23:59:59" ); + let offset = FixedOffset::west_opt(86399).unwrap(); assert_eq!( - format!("{:?}", FixedOffset::west(86399).ymd(2012, 3, 4)), - "2012-03-04-23:59:59".to_string() + format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), + "2012-03-04T05:06:07-23:59:59" ); + let offset = FixedOffset::west_opt(-86399).unwrap(); assert_eq!( - format!("{:?}", FixedOffset::west(86399).ymd(2012, 3, 4).and_hms(5, 6, 7)), - "2012-03-04T05:06:07-23:59:59".to_string() + format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), + "2012-03-04T05:06:07+23:59:59" ); } + + #[test] + fn test_parse_offset() { + let offset = FixedOffset::from_str("-0500").unwrap(); + assert_eq!(offset.local_minus_utc, -5 * 3600); + let offset = FixedOffset::from_str("-08:00").unwrap(); + assert_eq!(offset.local_minus_utc, -8 * 3600); + let offset = FixedOffset::from_str("+06:30").unwrap(); + assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800); + } + + #[test] + #[cfg(feature = "rkyv-validation")] + fn test_rkyv_validation() { + let offset = FixedOffset::from_str("-0500").unwrap(); + let bytes = rkyv::to_bytes::<_, 4>(&offset).unwrap(); + assert_eq!(rkyv::from_bytes::<FixedOffset>(&bytes).unwrap(), offset); + } } diff --git a/src/offset/local.rs b/src/offset/local.rs deleted file mode 100644 index 1abb3a9..0000000 --- a/src/offset/local.rs +++ /dev/null @@ -1,227 +0,0 @@ -// This is a part of Chrono. -// See README.md and LICENSE.txt for details. - -//! The local (system) time zone. - -#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] -use sys::{self, Timespec}; - -use super::fixed::FixedOffset; -use super::{LocalResult, TimeZone}; -#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] -use naive::NaiveTime; -use naive::{NaiveDate, NaiveDateTime}; -use {Date, DateTime}; -#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] -use {Datelike, Timelike}; - -/// Converts a `time::Tm` struct into the timezone-aware `DateTime`. -/// This assumes that `time` is working correctly, i.e. any error is fatal. -#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] -fn tm_to_datetime(mut tm: sys::Tm) -> DateTime<Local> { - if tm.tm_sec >= 60 { - tm.tm_nsec += (tm.tm_sec - 59) * 1_000_000_000; - tm.tm_sec = 59; - } - - #[cfg(not(windows))] - fn tm_to_naive_date(tm: &sys::Tm) -> NaiveDate { - // from_yo is more efficient than from_ymd (since it's the internal representation). - NaiveDate::from_yo(tm.tm_year + 1900, tm.tm_yday as u32 + 1) - } - - #[cfg(windows)] - fn tm_to_naive_date(tm: &sys::Tm) -> NaiveDate { - // ...but tm_yday is broken in Windows (issue #85) - NaiveDate::from_ymd(tm.tm_year + 1900, tm.tm_mon as u32 + 1, tm.tm_mday as u32) - } - - let date = tm_to_naive_date(&tm); - let time = NaiveTime::from_hms_nano( - tm.tm_hour as u32, - tm.tm_min as u32, - tm.tm_sec as u32, - tm.tm_nsec as u32, - ); - let offset = FixedOffset::east(tm.tm_utcoff); - DateTime::from_utc(date.and_time(time) - offset, offset) -} - -/// Converts a local `NaiveDateTime` to the `time::Timespec`. -#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] -fn datetime_to_timespec(d: &NaiveDateTime, local: bool) -> sys::Timespec { - // well, this exploits an undocumented `Tm::to_timespec` behavior - // to get the exact function we want (either `timegm` or `mktime`). - // the number 1 is arbitrary but should be non-zero to trigger `mktime`. - let tm_utcoff = if local { 1 } else { 0 }; - - let tm = sys::Tm { - tm_sec: d.second() as i32, - tm_min: d.minute() as i32, - tm_hour: d.hour() as i32, - tm_mday: d.day() as i32, - tm_mon: d.month0() as i32, // yes, C is that strange... - tm_year: d.year() - 1900, // this doesn't underflow, we know that d is `NaiveDateTime`. - tm_wday: 0, // to_local ignores this - tm_yday: 0, // and this - tm_isdst: -1, - tm_utcoff: tm_utcoff, - // do not set this, OS APIs are heavily inconsistent in terms of leap second handling - tm_nsec: 0, - }; - - tm.to_timespec() -} - -/// The local timescale. This is implemented via the standard `time` crate. -/// -/// Using the [`TimeZone`](./trait.TimeZone.html) methods -/// on the Local struct is the preferred way to construct `DateTime<Local>` -/// instances. -/// -/// # Example -/// -/// ~~~~ -/// use chrono::{Local, DateTime, TimeZone}; -/// -/// let dt: DateTime<Local> = Local::now(); -/// let dt: DateTime<Local> = Local.timestamp(0, 0); -/// ~~~~ -#[derive(Copy, Clone, Debug)] -pub struct Local; - -impl Local { - /// Returns a `Date` which corresponds to the current date. - pub fn today() -> Date<Local> { - Local::now().date() - } - - /// Returns a `DateTime` which corresponds to the current date. - #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] - pub fn now() -> DateTime<Local> { - tm_to_datetime(Timespec::now().local()) - } - - /// Returns a `DateTime` which corresponds to the current date. - #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))] - pub fn now() -> DateTime<Local> { - use super::Utc; - let now: DateTime<Utc> = super::Utc::now(); - - // Workaround missing timezone logic in `time` crate - let offset = FixedOffset::west((js_sys::Date::new_0().get_timezone_offset() as i32) * 60); - DateTime::from_utc(now.naive_utc(), offset) - } -} - -impl TimeZone for Local { - type Offset = FixedOffset; - - fn from_offset(_offset: &FixedOffset) -> Local { - Local - } - - // they are easier to define in terms of the finished date and time unlike other offsets - fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<FixedOffset> { - self.from_local_date(local).map(|date| *date.offset()) - } - - fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<FixedOffset> { - self.from_local_datetime(local).map(|datetime| *datetime.offset()) - } - - fn offset_from_utc_date(&self, utc: &NaiveDate) -> FixedOffset { - *self.from_utc_date(utc).offset() - } - - fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> FixedOffset { - *self.from_utc_datetime(utc).offset() - } - - // override them for avoiding redundant works - fn from_local_date(&self, local: &NaiveDate) -> LocalResult<Date<Local>> { - // this sounds very strange, but required for keeping `TimeZone::ymd` sane. - // in the other words, we use the offset at the local midnight - // but keep the actual date unaltered (much like `FixedOffset`). - let midnight = self.from_local_datetime(&local.and_hms(0, 0, 0)); - midnight.map(|datetime| Date::from_utc(*local, *datetime.offset())) - } - - #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))] - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Local>> { - let mut local = local.clone(); - // Get the offset from the js runtime - let offset = FixedOffset::west((js_sys::Date::new_0().get_timezone_offset() as i32) * 60); - local -= ::Duration::seconds(offset.local_minus_utc() as i64); - LocalResult::Single(DateTime::from_utc(local, offset)) - } - - #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Local>> { - let timespec = datetime_to_timespec(local, true); - - // datetime_to_timespec completely ignores leap seconds, so we need to adjust for them - let mut tm = timespec.local(); - assert_eq!(tm.tm_nsec, 0); - tm.tm_nsec = local.nanosecond() as i32; - - LocalResult::Single(tm_to_datetime(tm)) - } - - fn from_utc_date(&self, utc: &NaiveDate) -> Date<Local> { - let midnight = self.from_utc_datetime(&utc.and_hms(0, 0, 0)); - Date::from_utc(*utc, *midnight.offset()) - } - - #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))] - fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Local> { - // Get the offset from the js runtime - let offset = FixedOffset::west((js_sys::Date::new_0().get_timezone_offset() as i32) * 60); - DateTime::from_utc(*utc, offset) - } - - #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] - fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Local> { - let timespec = datetime_to_timespec(utc, false); - - // datetime_to_timespec completely ignores leap seconds, so we need to adjust for them - let mut tm = timespec.local(); - assert_eq!(tm.tm_nsec, 0); - tm.tm_nsec = utc.nanosecond() as i32; - - tm_to_datetime(tm) - } -} - -#[cfg(test)] -mod tests { - use super::Local; - use offset::TimeZone; - use Datelike; - - #[test] - fn test_local_date_sanity_check() { - // issue #27 - assert_eq!(Local.ymd(2999, 12, 28).day(), 28); - } - - #[test] - fn test_leap_second() { - // issue #123 - let today = Local::today(); - - let dt = today.and_hms_milli(1, 2, 59, 1000); - let timestr = dt.time().to_string(); - // the OS API may or may not support the leap second, - // but there are only two sensible options. - assert!(timestr == "01:02:60" || timestr == "01:03:00", "unexpected timestr {:?}", timestr); - - let dt = today.and_hms_milli(1, 2, 3, 1234); - let timestr = dt.time().to_string(); - assert!( - timestr == "01:02:03.234" || timestr == "01:02:04.234", - "unexpected timestr {:?}", - timestr - ); - } -} diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs new file mode 100644 index 0000000..37e7889 --- /dev/null +++ b/src/offset/local/mod.rs @@ -0,0 +1,537 @@ +// This is a part of Chrono. +// See README.md and LICENSE.txt for details. + +//! The local (system) time zone. + +#[cfg(windows)] +use std::cmp::Ordering; + +#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] +use rkyv::{Archive, Deserialize, Serialize}; + +use super::fixed::FixedOffset; +use super::{LocalResult, TimeZone}; +use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime}; +#[allow(deprecated)] +use crate::Date; +use crate::{DateTime, Utc}; + +#[cfg(unix)] +#[path = "unix.rs"] +mod inner; + +#[cfg(windows)] +#[path = "windows.rs"] +mod inner; + +#[cfg(all(windows, feature = "clock"))] +#[allow(unreachable_pub)] +mod win_bindings; + +#[cfg(all( + not(unix), + not(windows), + not(all( + target_arch = "wasm32", + feature = "wasmbind", + not(any(target_os = "emscripten", target_os = "wasi")) + )) +))] +mod inner { + use crate::{FixedOffset, LocalResult, NaiveDateTime}; + + pub(super) fn offset_from_utc_datetime(_utc_time: &NaiveDateTime) -> LocalResult<FixedOffset> { + LocalResult::Single(FixedOffset::east_opt(0).unwrap()) + } + + pub(super) fn offset_from_local_datetime( + _local_time: &NaiveDateTime, + ) -> LocalResult<FixedOffset> { + LocalResult::Single(FixedOffset::east_opt(0).unwrap()) + } +} + +#[cfg(all( + target_arch = "wasm32", + feature = "wasmbind", + not(any(target_os = "emscripten", target_os = "wasi")) +))] +mod inner { + use crate::{Datelike, FixedOffset, LocalResult, NaiveDateTime, Timelike}; + + pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> { + let offset = js_sys::Date::from(utc.and_utc()).get_timezone_offset(); + LocalResult::Single(FixedOffset::west_opt((offset as i32) * 60).unwrap()) + } + + pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> { + let mut year = local.year(); + if year < 100 { + // The API in `js_sys` does not let us create a `Date` with negative years. + // And values for years from `0` to `99` map to the years `1900` to `1999`. + // Shift the value by a multiple of 400 years until it is `>= 100`. + let shift_cycles = (year - 100).div_euclid(400); + year -= shift_cycles * 400; + } + let js_date = js_sys::Date::new_with_year_month_day_hr_min_sec( + year as u32, + local.month0() as i32, + local.day() as i32, + local.hour() as i32, + local.minute() as i32, + local.second() as i32, + // ignore milliseconds, our representation of leap seconds may be problematic + ); + let offset = js_date.get_timezone_offset(); + // We always get a result, even if this time does not exist or is ambiguous. + LocalResult::Single(FixedOffset::west_opt((offset as i32) * 60).unwrap()) + } +} + +#[cfg(unix)] +mod tz_info; + +/// The local timescale. +/// +/// Using the [`TimeZone`](./trait.TimeZone.html) methods +/// on the Local struct is the preferred way to construct `DateTime<Local>` +/// instances. +/// +/// # Example +/// +/// ``` +/// use chrono::{Local, DateTime, TimeZone}; +/// +/// let dt1: DateTime<Local> = Local::now(); +/// let dt2: DateTime<Local> = Local.timestamp_opt(0, 0).unwrap(); +/// assert!(dt1 >= dt2); +/// ``` +#[derive(Copy, Clone, Debug)] +#[cfg_attr( + any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), + derive(Archive, Deserialize, Serialize), + archive(compare(PartialEq)), + archive_attr(derive(Clone, Copy, Debug)) +)] +#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct Local; + +impl Local { + /// Returns a `Date` which corresponds to the current date. + #[deprecated(since = "0.4.23", note = "use `Local::now()` instead")] + #[allow(deprecated)] + #[must_use] + pub fn today() -> Date<Local> { + Local::now().date() + } + + /// Returns a `DateTime<Local>` which corresponds to the current date, time and offset from + /// UTC. + /// + /// See also the similar [`Utc::now()`] which returns `DateTime<Utc>`, i.e. without the local + /// offset. + /// + /// # Example + /// + /// ``` + /// # #![allow(unused_variables)] + /// # use chrono::{DateTime, FixedOffset, Local}; + /// // Current local time + /// let now = Local::now(); + /// + /// // Current local date + /// let today = now.date_naive(); + /// + /// // Current local time, converted to `DateTime<FixedOffset>` + /// let now_fixed_offset = Local::now().fixed_offset(); + /// // or + /// let now_fixed_offset: DateTime<FixedOffset> = Local::now().into(); + /// + /// // Current time in some timezone (let's use +05:00) + /// // Note that it is usually more efficient to use `Utc::now` for this use case. + /// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap(); + /// let now_with_offset = Local::now().with_timezone(&offset); + /// ``` + pub fn now() -> DateTime<Local> { + Utc::now().with_timezone(&Local) + } +} + +impl TimeZone for Local { + type Offset = FixedOffset; + + fn from_offset(_offset: &FixedOffset) -> Local { + Local + } + + #[allow(deprecated)] + fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<FixedOffset> { + // Get the offset at local midnight. + self.offset_from_local_datetime(&local.and_time(NaiveTime::MIN)) + } + + fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<FixedOffset> { + inner::offset_from_local_datetime(local) + } + + #[allow(deprecated)] + fn offset_from_utc_date(&self, utc: &NaiveDate) -> FixedOffset { + // Get the offset at midnight. + self.offset_from_utc_datetime(&utc.and_time(NaiveTime::MIN)) + } + + fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> FixedOffset { + inner::offset_from_utc_datetime(utc).unwrap() + } +} + +#[cfg(windows)] +#[derive(Copy, Clone, Eq, PartialEq)] +struct Transition { + transition_utc: NaiveDateTime, + offset_before: FixedOffset, + offset_after: FixedOffset, +} + +#[cfg(windows)] +impl Transition { + fn new( + transition_local: NaiveDateTime, + offset_before: FixedOffset, + offset_after: FixedOffset, + ) -> Transition { + // It is no problem if the transition time in UTC falls a couple of hours inside the buffer + // space around the `NaiveDateTime` range (although it is very theoretical to have a + // transition at midnight around `NaiveDate::(MIN|MAX)`. + let transition_utc = transition_local.overflowing_sub_offset(offset_before); + Transition { transition_utc, offset_before, offset_after } + } +} + +#[cfg(windows)] +impl PartialOrd for Transition { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.transition_utc.cmp(&other.transition_utc)) + } +} + +#[cfg(windows)] +impl Ord for Transition { + fn cmp(&self, other: &Self) -> Ordering { + self.transition_utc.cmp(&other.transition_utc) + } +} + +// Calculate the time in UTC given a local time and transitions. +// `transitions` must be sorted. +#[cfg(windows)] +fn lookup_with_dst_transitions( + transitions: &[Transition], + dt: NaiveDateTime, +) -> LocalResult<FixedOffset> { + for t in transitions.iter() { + // A transition can result in the wall clock time going forward (creating a gap) or going + // backward (creating a fold). We are interested in the earliest and latest wall time of the + // transition, as this are the times between which `dt` does may not exist or is ambiguous. + // + // It is no problem if the transition times falls a couple of hours inside the buffer + // space around the `NaiveDateTime` range (although it is very theoretical to have a + // transition at midnight around `NaiveDate::(MIN|MAX)`. + let (offset_min, offset_max) = + match t.offset_after.local_minus_utc() > t.offset_before.local_minus_utc() { + true => (t.offset_before, t.offset_after), + false => (t.offset_after, t.offset_before), + }; + let wall_earliest = t.transition_utc.overflowing_add_offset(offset_min); + let wall_latest = t.transition_utc.overflowing_add_offset(offset_max); + + if dt < wall_earliest { + return LocalResult::Single(t.offset_before); + } else if dt <= wall_latest { + return match t.offset_after.local_minus_utc().cmp(&t.offset_before.local_minus_utc()) { + Ordering::Equal => LocalResult::Single(t.offset_before), + Ordering::Less => LocalResult::Ambiguous(t.offset_before, t.offset_after), + Ordering::Greater => { + if dt == wall_earliest { + LocalResult::Single(t.offset_before) + } else if dt == wall_latest { + LocalResult::Single(t.offset_after) + } else { + LocalResult::None + } + } + }; + } + } + LocalResult::Single(transitions.last().unwrap().offset_after) +} + +#[cfg(test)] +mod tests { + use super::Local; + #[cfg(windows)] + use crate::offset::local::{lookup_with_dst_transitions, Transition}; + use crate::offset::TimeZone; + use crate::{Datelike, TimeDelta, Utc}; + #[cfg(windows)] + use crate::{FixedOffset, LocalResult, NaiveDate, NaiveDateTime}; + + #[test] + fn verify_correct_offsets() { + let now = Local::now(); + let from_local = Local.from_local_datetime(&now.naive_local()).unwrap(); + let from_utc = Local.from_utc_datetime(&now.naive_utc()); + + assert_eq!(now.offset().local_minus_utc(), from_local.offset().local_minus_utc()); + assert_eq!(now.offset().local_minus_utc(), from_utc.offset().local_minus_utc()); + + assert_eq!(now, from_local); + assert_eq!(now, from_utc); + } + + #[test] + fn verify_correct_offsets_distant_past() { + let distant_past = Local::now() - TimeDelta::days(365 * 500); + let from_local = Local.from_local_datetime(&distant_past.naive_local()).unwrap(); + let from_utc = Local.from_utc_datetime(&distant_past.naive_utc()); + + assert_eq!(distant_past.offset().local_minus_utc(), from_local.offset().local_minus_utc()); + assert_eq!(distant_past.offset().local_minus_utc(), from_utc.offset().local_minus_utc()); + + assert_eq!(distant_past, from_local); + assert_eq!(distant_past, from_utc); + } + + #[test] + fn verify_correct_offsets_distant_future() { + let distant_future = Local::now() + TimeDelta::days(365 * 35000); + let from_local = Local.from_local_datetime(&distant_future.naive_local()).unwrap(); + let from_utc = Local.from_utc_datetime(&distant_future.naive_utc()); + + assert_eq!( + distant_future.offset().local_minus_utc(), + from_local.offset().local_minus_utc() + ); + assert_eq!(distant_future.offset().local_minus_utc(), from_utc.offset().local_minus_utc()); + + assert_eq!(distant_future, from_local); + assert_eq!(distant_future, from_utc); + } + + #[test] + fn test_local_date_sanity_check() { + // issue #27 + assert_eq!(Local.with_ymd_and_hms(2999, 12, 28, 0, 0, 0).unwrap().day(), 28); + } + + #[test] + fn test_leap_second() { + // issue #123 + let today = Utc::now().date_naive(); + + if let Some(dt) = today.and_hms_milli_opt(15, 2, 59, 1000) { + let timestr = dt.time().to_string(); + // the OS API may or may not support the leap second, + // but there are only two sensible options. + assert!( + timestr == "15:02:60" || timestr == "15:03:00", + "unexpected timestr {:?}", + timestr + ); + } + + if let Some(dt) = today.and_hms_milli_opt(15, 2, 3, 1234) { + let timestr = dt.time().to_string(); + assert!( + timestr == "15:02:03.234" || timestr == "15:02:04.234", + "unexpected timestr {:?}", + timestr + ); + } + } + + #[test] + #[cfg(windows)] + fn test_lookup_with_dst_transitions() { + let ymdhms = |y, m, d, h, n, s| { + NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap() + }; + + #[track_caller] + #[allow(clippy::too_many_arguments)] + fn compare_lookup( + transitions: &[Transition], + y: i32, + m: u32, + d: u32, + h: u32, + n: u32, + s: u32, + result: LocalResult<FixedOffset>, + ) { + let dt = NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap(); + assert_eq!(lookup_with_dst_transitions(transitions, dt), result); + } + + // dst transition before std transition + // dst offset > std offset + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst), + Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), dst, std), + ]; + compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, LocalResult::Single(dst)); + + compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 10, 29, 4, 0, 0, LocalResult::Single(std)); + + // std transition before dst transition + // dst offset > std offset + let std = FixedOffset::east_opt(-5 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(-4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 24, 3, 0, 0), dst, std), + Transition::new(ymdhms(2023, 10, 27, 2, 0, 0), std, dst), + ]; + compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 3, 24, 4, 0, 0, LocalResult::Single(std)); + + compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 27, 4, 0, 0, LocalResult::Single(dst)); + + // dst transition before std transition + // dst offset < std offset + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt((2 * 60 + 30) * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 26, 2, 30, 0), std, dst), + Transition::new(ymdhms(2023, 10, 29, 2, 0, 0), dst, std), + ]; + compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 3, 26, 2, 15, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst)); + + compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 29, 2, 15, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Single(std)); + + // std transition before dst transition + // dst offset < std offset + let std = FixedOffset::east_opt(-(4 * 60 + 30) * 60).unwrap(); + let dst = FixedOffset::east_opt(-5 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 24, 2, 0, 0), dst, std), + Transition::new(ymdhms(2023, 10, 27, 2, 30, 0), std, dst), + ]; + compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 24, 2, 15, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, LocalResult::Single(std)); + + compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 10, 27, 2, 15, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, LocalResult::Single(dst)); + + // offset stays the same + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, std), + Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), std, std), + ]; + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Single(std)); + + // single transition + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap(); + let transitions = [Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst)]; + compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, LocalResult::Single(dst)); + } + + #[test] + #[cfg(windows)] + fn test_lookup_with_dst_transitions_limits() { + // Transition beyond UTC year end doesn't panic in year of `NaiveDate::MAX` + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(NaiveDateTime::MAX.with_month(7).unwrap(), std, dst), + Transition::new(NaiveDateTime::MAX, dst, std), + ]; + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(3).unwrap()), + LocalResult::Single(std) + ); + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(8).unwrap()), + LocalResult::Single(dst) + ); + // Doesn't panic with `NaiveDateTime::MAX` as argument (which would be out of range when + // converted to UTC). + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX), + LocalResult::Ambiguous(dst, std) + ); + + // Transition before UTC year end doesn't panic in year of `NaiveDate::MIN` + let std = FixedOffset::west_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::west_opt(4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(NaiveDateTime::MIN, std, dst), + Transition::new(NaiveDateTime::MIN.with_month(6).unwrap(), dst, std), + ]; + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(3).unwrap()), + LocalResult::Single(dst) + ); + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(8).unwrap()), + LocalResult::Single(std) + ); + // Doesn't panic with `NaiveDateTime::MIN` as argument (which would be out of range when + // converted to UTC). + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN), + LocalResult::Ambiguous(std, dst) + ); + } + + #[test] + #[cfg(feature = "rkyv-validation")] + fn test_rkyv_validation() { + let local = Local; + // Local is a ZST and serializes to 0 bytes + let bytes = rkyv::to_bytes::<_, 0>(&local).unwrap(); + assert_eq!(bytes.len(), 0); + + // but is deserialized to an archived variant without a + // wrapping object + assert_eq!(rkyv::from_bytes::<Local>(&bytes).unwrap(), super::ArchivedLocal); + } +} diff --git a/src/offset/local/tz_info/mod.rs b/src/offset/local/tz_info/mod.rs new file mode 100644 index 0000000..780e15a --- /dev/null +++ b/src/offset/local/tz_info/mod.rs @@ -0,0 +1,116 @@ +#![deny(missing_docs)] +#![allow(dead_code)] +#![warn(unreachable_pub)] + +use std::num::ParseIntError; +use std::str::Utf8Error; +use std::time::SystemTimeError; +use std::{error, fmt, io}; + +mod timezone; +pub(crate) use timezone::TimeZone; + +mod parser; +mod rule; + +/// Unified error type for everything in the crate +#[derive(Debug)] +pub(crate) enum Error { + /// Date time error + DateTime(&'static str), + /// Local time type search error + FindLocalTimeType(&'static str), + /// Local time type error + LocalTimeType(&'static str), + /// Invalid slice for integer conversion + InvalidSlice(&'static str), + /// Invalid Tzif file + InvalidTzFile(&'static str), + /// Invalid TZ string + InvalidTzString(&'static str), + /// I/O error + Io(io::Error), + /// Out of range error + OutOfRange(&'static str), + /// Integer parsing error + ParseInt(ParseIntError), + /// Date time projection error + ProjectDateTime(&'static str), + /// System time error + SystemTime(SystemTimeError), + /// Time zone error + TimeZone(&'static str), + /// Transition rule error + TransitionRule(&'static str), + /// Unsupported Tzif file + UnsupportedTzFile(&'static str), + /// Unsupported TZ string + UnsupportedTzString(&'static str), + /// UTF-8 error + Utf8(Utf8Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Error::*; + match self { + DateTime(error) => write!(f, "invalid date time: {}", error), + FindLocalTimeType(error) => error.fmt(f), + LocalTimeType(error) => write!(f, "invalid local time type: {}", error), + InvalidSlice(error) => error.fmt(f), + InvalidTzString(error) => write!(f, "invalid TZ string: {}", error), + InvalidTzFile(error) => error.fmt(f), + Io(error) => error.fmt(f), + OutOfRange(error) => error.fmt(f), + ParseInt(error) => error.fmt(f), + ProjectDateTime(error) => error.fmt(f), + SystemTime(error) => error.fmt(f), + TransitionRule(error) => write!(f, "invalid transition rule: {}", error), + TimeZone(error) => write!(f, "invalid time zone: {}", error), + UnsupportedTzFile(error) => error.fmt(f), + UnsupportedTzString(error) => write!(f, "unsupported TZ string: {}", error), + Utf8(error) => error.fmt(f), + } + } +} + +impl error::Error for Error {} + +impl From<io::Error> for Error { + fn from(error: io::Error) -> Self { + Error::Io(error) + } +} + +impl From<ParseIntError> for Error { + fn from(error: ParseIntError) -> Self { + Error::ParseInt(error) + } +} + +impl From<SystemTimeError> for Error { + fn from(error: SystemTimeError) -> Self { + Error::SystemTime(error) + } +} + +impl From<Utf8Error> for Error { + fn from(error: Utf8Error) -> Self { + Error::Utf8(error) + } +} + +/// Number of hours in one day +const HOURS_PER_DAY: i64 = 24; +/// Number of seconds in one hour +const SECONDS_PER_HOUR: i64 = 3600; +/// Number of seconds in one day +const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * HOURS_PER_DAY; +/// Number of days in one week +const DAYS_PER_WEEK: i64 = 7; + +/// Month days in a normal year +const DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +/// Cumulated month days in a normal year +const CUMUL_DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] = + [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; diff --git a/src/offset/local/tz_info/parser.rs b/src/offset/local/tz_info/parser.rs new file mode 100644 index 0000000..47cc037 --- /dev/null +++ b/src/offset/local/tz_info/parser.rs @@ -0,0 +1,333 @@ +use std::io::{self, ErrorKind}; +use std::iter; +use std::num::ParseIntError; +use std::str::{self, FromStr}; + +use super::rule::TransitionRule; +use super::timezone::{LeapSecond, LocalTimeType, TimeZone, Transition}; +use super::Error; + +pub(super) fn parse(bytes: &[u8]) -> Result<TimeZone, Error> { + let mut cursor = Cursor::new(bytes); + let state = State::new(&mut cursor, true)?; + let (state, footer) = match state.header.version { + Version::V1 => match cursor.is_empty() { + true => (state, None), + false => { + return Err(Error::InvalidTzFile("remaining data after end of TZif v1 data block")) + } + }, + Version::V2 | Version::V3 => { + let state = State::new(&mut cursor, false)?; + (state, Some(cursor.remaining())) + } + }; + + let mut transitions = Vec::with_capacity(state.header.transition_count); + for (arr_time, &local_time_type_index) in + state.transition_times.chunks_exact(state.time_size).zip(state.transition_types) + { + let unix_leap_time = + state.parse_time(&arr_time[0..state.time_size], state.header.version)?; + let local_time_type_index = local_time_type_index as usize; + transitions.push(Transition::new(unix_leap_time, local_time_type_index)); + } + + let mut local_time_types = Vec::with_capacity(state.header.type_count); + for arr in state.local_time_types.chunks_exact(6) { + let ut_offset = read_be_i32(&arr[..4])?; + + let is_dst = match arr[4] { + 0 => false, + 1 => true, + _ => return Err(Error::InvalidTzFile("invalid DST indicator")), + }; + + let char_index = arr[5] as usize; + if char_index >= state.header.char_count { + return Err(Error::InvalidTzFile("invalid time zone name char index")); + } + + let position = match state.names[char_index..].iter().position(|&c| c == b'\0') { + Some(position) => position, + None => return Err(Error::InvalidTzFile("invalid time zone name char index")), + }; + + let name = &state.names[char_index..char_index + position]; + let name = if !name.is_empty() { Some(name) } else { None }; + local_time_types.push(LocalTimeType::new(ut_offset, is_dst, name)?); + } + + let mut leap_seconds = Vec::with_capacity(state.header.leap_count); + for arr in state.leap_seconds.chunks_exact(state.time_size + 4) { + let unix_leap_time = state.parse_time(&arr[0..state.time_size], state.header.version)?; + let correction = read_be_i32(&arr[state.time_size..state.time_size + 4])?; + leap_seconds.push(LeapSecond::new(unix_leap_time, correction)); + } + + let std_walls_iter = state.std_walls.iter().copied().chain(iter::repeat(0)); + let ut_locals_iter = state.ut_locals.iter().copied().chain(iter::repeat(0)); + if std_walls_iter.zip(ut_locals_iter).take(state.header.type_count).any(|pair| pair == (0, 1)) { + return Err(Error::InvalidTzFile( + "invalid couple of standard/wall and UT/local indicators", + )); + } + + let extra_rule = match footer { + Some(footer) => { + let footer = str::from_utf8(footer)?; + if !(footer.starts_with('\n') && footer.ends_with('\n')) { + return Err(Error::InvalidTzFile("invalid footer")); + } + + let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace()); + if tz_string.starts_with(':') || tz_string.contains('\0') { + return Err(Error::InvalidTzFile("invalid footer")); + } + + match tz_string.is_empty() { + true => None, + false => Some(TransitionRule::from_tz_string( + tz_string.as_bytes(), + state.header.version == Version::V3, + )?), + } + } + None => None, + }; + + TimeZone::new(transitions, local_time_types, leap_seconds, extra_rule) +} + +/// TZif data blocks +struct State<'a> { + header: Header, + /// Time size in bytes + time_size: usize, + /// Transition times data block + transition_times: &'a [u8], + /// Transition types data block + transition_types: &'a [u8], + /// Local time types data block + local_time_types: &'a [u8], + /// Time zone names data block + names: &'a [u8], + /// Leap seconds data block + leap_seconds: &'a [u8], + /// UT/local indicators data block + std_walls: &'a [u8], + /// Standard/wall indicators data block + ut_locals: &'a [u8], +} + +impl<'a> State<'a> { + /// Read TZif data blocks + fn new(cursor: &mut Cursor<'a>, first: bool) -> Result<Self, Error> { + let header = Header::new(cursor)?; + let time_size = match first { + true => 4, // We always parse V1 first + false => 8, + }; + + Ok(Self { + time_size, + transition_times: cursor.read_exact(header.transition_count * time_size)?, + transition_types: cursor.read_exact(header.transition_count)?, + local_time_types: cursor.read_exact(header.type_count * 6)?, + names: cursor.read_exact(header.char_count)?, + leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?, + std_walls: cursor.read_exact(header.std_wall_count)?, + ut_locals: cursor.read_exact(header.ut_local_count)?, + header, + }) + } + + /// Parse time values + fn parse_time(&self, arr: &[u8], version: Version) -> Result<i64, Error> { + match version { + Version::V1 => Ok(read_be_i32(&arr[..4])?.into()), + Version::V2 | Version::V3 => read_be_i64(arr), + } + } +} + +/// TZif header +#[derive(Debug)] +struct Header { + /// TZif version + version: Version, + /// Number of UT/local indicators + ut_local_count: usize, + /// Number of standard/wall indicators + std_wall_count: usize, + /// Number of leap-second records + leap_count: usize, + /// Number of transition times + transition_count: usize, + /// Number of local time type records + type_count: usize, + /// Number of time zone names bytes + char_count: usize, +} + +impl Header { + fn new(cursor: &mut Cursor) -> Result<Self, Error> { + let magic = cursor.read_exact(4)?; + if magic != *b"TZif" { + return Err(Error::InvalidTzFile("invalid magic number")); + } + + let version = match cursor.read_exact(1)? { + [0x00] => Version::V1, + [0x32] => Version::V2, + [0x33] => Version::V3, + _ => return Err(Error::UnsupportedTzFile("unsupported TZif version")), + }; + + cursor.read_exact(15)?; + let ut_local_count = cursor.read_be_u32()?; + let std_wall_count = cursor.read_be_u32()?; + let leap_count = cursor.read_be_u32()?; + let transition_count = cursor.read_be_u32()?; + let type_count = cursor.read_be_u32()?; + let char_count = cursor.read_be_u32()?; + + if !(type_count != 0 + && char_count != 0 + && (ut_local_count == 0 || ut_local_count == type_count) + && (std_wall_count == 0 || std_wall_count == type_count)) + { + return Err(Error::InvalidTzFile("invalid header")); + } + + Ok(Self { + version, + ut_local_count: ut_local_count as usize, + std_wall_count: std_wall_count as usize, + leap_count: leap_count as usize, + transition_count: transition_count as usize, + type_count: type_count as usize, + char_count: char_count as usize, + }) + } +} + +/// A `Cursor` contains a slice of a buffer and a read count. +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct Cursor<'a> { + /// Slice representing the remaining data to be read + remaining: &'a [u8], + /// Number of already read bytes + read_count: usize, +} + +impl<'a> Cursor<'a> { + /// Construct a new `Cursor` from remaining data + pub(crate) const fn new(remaining: &'a [u8]) -> Self { + Self { remaining, read_count: 0 } + } + + pub(crate) fn peek(&self) -> Option<&u8> { + self.remaining().first() + } + + /// Returns remaining data + pub(crate) const fn remaining(&self) -> &'a [u8] { + self.remaining + } + + /// Returns `true` if data is remaining + pub(crate) const fn is_empty(&self) -> bool { + self.remaining.is_empty() + } + + pub(crate) fn read_be_u32(&mut self) -> Result<u32, Error> { + let mut buf = [0; 4]; + buf.copy_from_slice(self.read_exact(4)?); + Ok(u32::from_be_bytes(buf)) + } + + /// Read exactly `count` bytes, reducing remaining data and incrementing read count + pub(crate) fn read_exact(&mut self, count: usize) -> Result<&'a [u8], io::Error> { + match (self.remaining.get(..count), self.remaining.get(count..)) { + (Some(result), Some(remaining)) => { + self.remaining = remaining; + self.read_count += count; + Ok(result) + } + _ => Err(io::Error::from(ErrorKind::UnexpectedEof)), + } + } + + /// Read bytes and compare them to the provided tag + pub(crate) fn read_tag(&mut self, tag: &[u8]) -> Result<(), io::Error> { + if self.read_exact(tag.len())? == tag { + Ok(()) + } else { + Err(io::Error::from(ErrorKind::InvalidData)) + } + } + + /// Read bytes if the remaining data is prefixed by the provided tag + pub(crate) fn read_optional_tag(&mut self, tag: &[u8]) -> Result<bool, io::Error> { + if self.remaining.starts_with(tag) { + self.read_exact(tag.len())?; + Ok(true) + } else { + Ok(false) + } + } + + /// Read bytes as long as the provided predicate is true + pub(crate) fn read_while<F: Fn(&u8) -> bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> { + match self.remaining.iter().position(|x| !f(x)) { + None => self.read_exact(self.remaining.len()), + Some(position) => self.read_exact(position), + } + } + + // Parse an integer out of the ASCII digits + pub(crate) fn read_int<T: FromStr<Err = ParseIntError>>(&mut self) -> Result<T, Error> { + let bytes = self.read_while(u8::is_ascii_digit)?; + Ok(str::from_utf8(bytes)?.parse()?) + } + + /// Read bytes until the provided predicate is true + pub(crate) fn read_until<F: Fn(&u8) -> bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> { + match self.remaining.iter().position(f) { + None => self.read_exact(self.remaining.len()), + Some(position) => self.read_exact(position), + } + } +} + +pub(crate) fn read_be_i32(bytes: &[u8]) -> Result<i32, Error> { + if bytes.len() != 4 { + return Err(Error::InvalidSlice("too short for i32")); + } + + let mut buf = [0; 4]; + buf.copy_from_slice(bytes); + Ok(i32::from_be_bytes(buf)) +} + +pub(crate) fn read_be_i64(bytes: &[u8]) -> Result<i64, Error> { + if bytes.len() != 8 { + return Err(Error::InvalidSlice("too short for i64")); + } + + let mut buf = [0; 8]; + buf.copy_from_slice(bytes); + Ok(i64::from_be_bytes(buf)) +} + +/// TZif version +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Version { + /// Version 1 + V1, + /// Version 2 + V2, + /// Version 3 + V3, +} diff --git a/src/offset/local/tz_info/rule.rs b/src/offset/local/tz_info/rule.rs new file mode 100644 index 0000000..369e317 --- /dev/null +++ b/src/offset/local/tz_info/rule.rs @@ -0,0 +1,1045 @@ +use std::cmp::Ordering; + +use super::parser::Cursor; +use super::timezone::{LocalTimeType, SECONDS_PER_WEEK}; +use super::{ + Error, CUMUL_DAY_IN_MONTHS_NORMAL_YEAR, DAYS_PER_WEEK, DAY_IN_MONTHS_NORMAL_YEAR, + SECONDS_PER_DAY, +}; + +/// Transition rule +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) enum TransitionRule { + /// Fixed local time type + Fixed(LocalTimeType), + /// Alternate local time types + Alternate(AlternateTime), +} + +impl TransitionRule { + /// Parse a POSIX TZ string containing a time zone description, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). + /// + /// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used. + /// + pub(super) fn from_tz_string( + tz_string: &[u8], + use_string_extensions: bool, + ) -> Result<Self, Error> { + let mut cursor = Cursor::new(tz_string); + + let std_time_zone = Some(parse_name(&mut cursor)?); + let std_offset = parse_offset(&mut cursor)?; + + if cursor.is_empty() { + return Ok(LocalTimeType::new(-std_offset, false, std_time_zone)?.into()); + } + + let dst_time_zone = Some(parse_name(&mut cursor)?); + + let dst_offset = match cursor.peek() { + Some(&b',') => std_offset - 3600, + Some(_) => parse_offset(&mut cursor)?, + None => { + return Err(Error::UnsupportedTzString("DST start and end rules must be provided")) + } + }; + + if cursor.is_empty() { + return Err(Error::UnsupportedTzString("DST start and end rules must be provided")); + } + + cursor.read_tag(b",")?; + let (dst_start, dst_start_time) = RuleDay::parse(&mut cursor, use_string_extensions)?; + + cursor.read_tag(b",")?; + let (dst_end, dst_end_time) = RuleDay::parse(&mut cursor, use_string_extensions)?; + + if !cursor.is_empty() { + return Err(Error::InvalidTzString("remaining data after parsing TZ string")); + } + + Ok(AlternateTime::new( + LocalTimeType::new(-std_offset, false, std_time_zone)?, + LocalTimeType::new(-dst_offset, true, dst_time_zone)?, + dst_start, + dst_start_time, + dst_end, + dst_end_time, + )? + .into()) + } + + /// Find the local time type associated to the transition rule at the specified Unix time in seconds + pub(super) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> { + match self { + TransitionRule::Fixed(local_time_type) => Ok(local_time_type), + TransitionRule::Alternate(alternate_time) => { + alternate_time.find_local_time_type(unix_time) + } + } + } + + /// Find the local time type associated to the transition rule at the specified Unix time in seconds + pub(super) fn find_local_time_type_from_local( + &self, + local_time: i64, + year: i32, + ) -> Result<crate::LocalResult<LocalTimeType>, Error> { + match self { + TransitionRule::Fixed(local_time_type) => { + Ok(crate::LocalResult::Single(*local_time_type)) + } + TransitionRule::Alternate(alternate_time) => { + alternate_time.find_local_time_type_from_local(local_time, year) + } + } + } +} + +impl From<LocalTimeType> for TransitionRule { + fn from(inner: LocalTimeType) -> Self { + TransitionRule::Fixed(inner) + } +} + +impl From<AlternateTime> for TransitionRule { + fn from(inner: AlternateTime) -> Self { + TransitionRule::Alternate(inner) + } +} + +/// Transition rule representing alternate local time types +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) struct AlternateTime { + /// Local time type for standard time + pub(super) std: LocalTimeType, + /// Local time type for Daylight Saving Time + pub(super) dst: LocalTimeType, + /// Start day of Daylight Saving Time + dst_start: RuleDay, + /// Local start day time of Daylight Saving Time, in seconds + dst_start_time: i32, + /// End day of Daylight Saving Time + dst_end: RuleDay, + /// Local end day time of Daylight Saving Time, in seconds + dst_end_time: i32, +} + +impl AlternateTime { + /// Construct a transition rule representing alternate local time types + const fn new( + std: LocalTimeType, + dst: LocalTimeType, + dst_start: RuleDay, + dst_start_time: i32, + dst_end: RuleDay, + dst_end_time: i32, + ) -> Result<Self, Error> { + // Overflow is not possible + if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK + && (dst_end_time as i64).abs() < SECONDS_PER_WEEK) + { + return Err(Error::TransitionRule("invalid DST start or end time")); + } + + Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time }) + } + + /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds + fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> { + // Overflow is not possible + let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64; + let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64; + + let current_year = match UtcDateTime::from_timespec(unix_time) { + Ok(dt) => dt.year, + Err(error) => return Err(error), + }; + + // Check if the current year is valid for the following computations + if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) { + return Err(Error::OutOfRange("out of range date time")); + } + + let current_year_dst_start_unix_time = + self.dst_start.unix_time(current_year, dst_start_time_in_utc); + let current_year_dst_end_unix_time = + self.dst_end.unix_time(current_year, dst_end_time_in_utc); + + // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range + let is_dst = + match Ord::cmp(¤t_year_dst_start_unix_time, ¤t_year_dst_end_unix_time) { + Ordering::Less | Ordering::Equal => { + if unix_time < current_year_dst_start_unix_time { + let previous_year_dst_end_unix_time = + self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc); + if unix_time < previous_year_dst_end_unix_time { + let previous_year_dst_start_unix_time = + self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc); + previous_year_dst_start_unix_time <= unix_time + } else { + false + } + } else if unix_time < current_year_dst_end_unix_time { + true + } else { + let next_year_dst_start_unix_time = + self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc); + if next_year_dst_start_unix_time <= unix_time { + let next_year_dst_end_unix_time = + self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc); + unix_time < next_year_dst_end_unix_time + } else { + false + } + } + } + Ordering::Greater => { + if unix_time < current_year_dst_end_unix_time { + let previous_year_dst_start_unix_time = + self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc); + if unix_time < previous_year_dst_start_unix_time { + let previous_year_dst_end_unix_time = + self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc); + unix_time < previous_year_dst_end_unix_time + } else { + true + } + } else if unix_time < current_year_dst_start_unix_time { + false + } else { + let next_year_dst_end_unix_time = + self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc); + if next_year_dst_end_unix_time <= unix_time { + let next_year_dst_start_unix_time = + self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc); + next_year_dst_start_unix_time <= unix_time + } else { + true + } + } + } + }; + + if is_dst { + Ok(&self.dst) + } else { + Ok(&self.std) + } + } + + fn find_local_time_type_from_local( + &self, + local_time: i64, + current_year: i32, + ) -> Result<crate::LocalResult<LocalTimeType>, Error> { + // Check if the current year is valid for the following computations + if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) { + return Err(Error::OutOfRange("out of range date time")); + } + + let dst_start_transition_start = + self.dst_start.unix_time(current_year, 0) + i64::from(self.dst_start_time); + let dst_start_transition_end = self.dst_start.unix_time(current_year, 0) + + i64::from(self.dst_start_time) + + i64::from(self.dst.ut_offset) + - i64::from(self.std.ut_offset); + + let dst_end_transition_start = + self.dst_end.unix_time(current_year, 0) + i64::from(self.dst_end_time); + let dst_end_transition_end = self.dst_end.unix_time(current_year, 0) + + i64::from(self.dst_end_time) + + i64::from(self.std.ut_offset) + - i64::from(self.dst.ut_offset); + + match self.std.ut_offset.cmp(&self.dst.ut_offset) { + Ordering::Equal => Ok(crate::LocalResult::Single(self.std)), + Ordering::Less => { + if self.dst_start.transition_date(current_year).0 + < self.dst_end.transition_date(current_year).0 + { + // northern hemisphere + // For the DST END transition, the `start` happens at a later timestamp than the `end`. + if local_time <= dst_start_transition_start { + Ok(crate::LocalResult::Single(self.std)) + } else if local_time > dst_start_transition_start + && local_time < dst_start_transition_end + { + Ok(crate::LocalResult::None) + } else if local_time >= dst_start_transition_end + && local_time < dst_end_transition_end + { + Ok(crate::LocalResult::Single(self.dst)) + } else if local_time >= dst_end_transition_end + && local_time <= dst_end_transition_start + { + Ok(crate::LocalResult::Ambiguous(self.std, self.dst)) + } else { + Ok(crate::LocalResult::Single(self.std)) + } + } else { + // southern hemisphere regular DST + // For the DST END transition, the `start` happens at a later timestamp than the `end`. + if local_time < dst_end_transition_end { + Ok(crate::LocalResult::Single(self.dst)) + } else if local_time >= dst_end_transition_end + && local_time <= dst_end_transition_start + { + Ok(crate::LocalResult::Ambiguous(self.std, self.dst)) + } else if local_time > dst_end_transition_end + && local_time < dst_start_transition_start + { + Ok(crate::LocalResult::Single(self.std)) + } else if local_time >= dst_start_transition_start + && local_time < dst_start_transition_end + { + Ok(crate::LocalResult::None) + } else { + Ok(crate::LocalResult::Single(self.dst)) + } + } + } + Ordering::Greater => { + if self.dst_start.transition_date(current_year).0 + < self.dst_end.transition_date(current_year).0 + { + // southern hemisphere reverse DST + // For the DST END transition, the `start` happens at a later timestamp than the `end`. + if local_time < dst_start_transition_end { + Ok(crate::LocalResult::Single(self.std)) + } else if local_time >= dst_start_transition_end + && local_time <= dst_start_transition_start + { + Ok(crate::LocalResult::Ambiguous(self.dst, self.std)) + } else if local_time > dst_start_transition_start + && local_time < dst_end_transition_start + { + Ok(crate::LocalResult::Single(self.dst)) + } else if local_time >= dst_end_transition_start + && local_time < dst_end_transition_end + { + Ok(crate::LocalResult::None) + } else { + Ok(crate::LocalResult::Single(self.std)) + } + } else { + // northern hemisphere reverse DST + // For the DST END transition, the `start` happens at a later timestamp than the `end`. + if local_time <= dst_end_transition_start { + Ok(crate::LocalResult::Single(self.dst)) + } else if local_time > dst_end_transition_start + && local_time < dst_end_transition_end + { + Ok(crate::LocalResult::None) + } else if local_time >= dst_end_transition_end + && local_time < dst_start_transition_end + { + Ok(crate::LocalResult::Single(self.std)) + } else if local_time >= dst_start_transition_end + && local_time <= dst_start_transition_start + { + Ok(crate::LocalResult::Ambiguous(self.dst, self.std)) + } else { + Ok(crate::LocalResult::Single(self.dst)) + } + } + } + } + } +} + +/// Parse time zone name +fn parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error> { + match cursor.peek() { + Some(b'<') => {} + _ => return Ok(cursor.read_while(u8::is_ascii_alphabetic)?), + } + + cursor.read_exact(1)?; + let unquoted = cursor.read_until(|&x| x == b'>')?; + cursor.read_exact(1)?; + Ok(unquoted) +} + +/// Parse time zone offset +fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> { + let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; + + if !(0..=24).contains(&hour) { + return Err(Error::InvalidTzString("invalid offset hour")); + } + if !(0..=59).contains(&minute) { + return Err(Error::InvalidTzString("invalid offset minute")); + } + if !(0..=59).contains(&second) { + return Err(Error::InvalidTzString("invalid offset second")); + } + + Ok(sign * (hour * 3600 + minute * 60 + second)) +} + +/// Parse transition rule time +fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error> { + let (hour, minute, second) = parse_hhmmss(cursor)?; + + if !(0..=24).contains(&hour) { + return Err(Error::InvalidTzString("invalid day time hour")); + } + if !(0..=59).contains(&minute) { + return Err(Error::InvalidTzString("invalid day time minute")); + } + if !(0..=59).contains(&second) { + return Err(Error::InvalidTzString("invalid day time second")); + } + + Ok(hour * 3600 + minute * 60 + second) +} + +/// Parse transition rule time with TZ string extensions +fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error> { + let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; + + if !(-167..=167).contains(&hour) { + return Err(Error::InvalidTzString("invalid day time hour")); + } + if !(0..=59).contains(&minute) { + return Err(Error::InvalidTzString("invalid day time minute")); + } + if !(0..=59).contains(&second) { + return Err(Error::InvalidTzString("invalid day time second")); + } + + Ok(sign * (hour * 3600 + minute * 60 + second)) +} + +/// Parse hours, minutes and seconds +fn parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), Error> { + let hour = cursor.read_int()?; + + let mut minute = 0; + let mut second = 0; + + if cursor.read_optional_tag(b":")? { + minute = cursor.read_int()?; + + if cursor.read_optional_tag(b":")? { + second = cursor.read_int()?; + } + } + + Ok((hour, minute, second)) +} + +/// Parse signed hours, minutes and seconds +fn parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), Error> { + let mut sign = 1; + if let Some(&c) = cursor.peek() { + if c == b'+' || c == b'-' { + cursor.read_exact(1)?; + if c == b'-' { + sign = -1; + } + } + } + + let (hour, minute, second) = parse_hhmmss(cursor)?; + Ok((sign, hour, minute, second)) +} + +/// Transition rule day +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum RuleDay { + /// Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable + Julian1WithoutLeap(u16), + /// Zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account + Julian0WithLeap(u16), + /// Day represented by a month, a month week and a week day + MonthWeekday { + /// Month in `[1, 12]` + month: u8, + /// Week of the month in `[1, 5]`, with `5` representing the last week of the month + week: u8, + /// Day of the week in `[0, 6]` from Sunday + week_day: u8, + }, +} + +impl RuleDay { + /// Parse transition rule + fn parse(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(Self, i32), Error> { + let date = match cursor.peek() { + Some(b'M') => { + cursor.read_exact(1)?; + let month = cursor.read_int()?; + cursor.read_tag(b".")?; + let week = cursor.read_int()?; + cursor.read_tag(b".")?; + let week_day = cursor.read_int()?; + RuleDay::month_weekday(month, week, week_day)? + } + Some(b'J') => { + cursor.read_exact(1)?; + RuleDay::julian_1(cursor.read_int()?)? + } + _ => RuleDay::julian_0(cursor.read_int()?)?, + }; + + Ok(( + date, + match (cursor.read_optional_tag(b"/")?, use_string_extensions) { + (false, _) => 2 * 3600, + (true, true) => parse_rule_time_extended(cursor)?, + (true, false) => parse_rule_time(cursor)?, + }, + )) + } + + /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable + fn julian_1(julian_day_1: u16) -> Result<Self, Error> { + if !(1..=365).contains(&julian_day_1) { + return Err(Error::TransitionRule("invalid rule day julian day")); + } + + Ok(RuleDay::Julian1WithoutLeap(julian_day_1)) + } + + /// Construct a transition rule day represented by a zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account + const fn julian_0(julian_day_0: u16) -> Result<Self, Error> { + if julian_day_0 > 365 { + return Err(Error::TransitionRule("invalid rule day julian day")); + } + + Ok(RuleDay::Julian0WithLeap(julian_day_0)) + } + + /// Construct a transition rule day represented by a month, a month week and a week day + fn month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error> { + if !(1..=12).contains(&month) { + return Err(Error::TransitionRule("invalid rule day month")); + } + + if !(1..=5).contains(&week) { + return Err(Error::TransitionRule("invalid rule day week")); + } + + if week_day > 6 { + return Err(Error::TransitionRule("invalid rule day week day")); + } + + Ok(RuleDay::MonthWeekday { month, week, week_day }) + } + + /// Get the transition date for the provided year + /// + /// ## Outputs + /// + /// * `month`: Month in `[1, 12]` + /// * `month_day`: Day of the month in `[1, 31]` + fn transition_date(&self, year: i32) -> (usize, i64) { + match *self { + RuleDay::Julian1WithoutLeap(year_day) => { + let year_day = year_day as i64; + + let month = match CUMUL_DAY_IN_MONTHS_NORMAL_YEAR.binary_search(&(year_day - 1)) { + Ok(x) => x + 1, + Err(x) => x, + }; + + let month_day = year_day - CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1]; + + (month, month_day) + } + RuleDay::Julian0WithLeap(year_day) => { + let leap = is_leap_year(year) as i64; + + let cumul_day_in_months = [ + 0, + 31, + 59 + leap, + 90 + leap, + 120 + leap, + 151 + leap, + 181 + leap, + 212 + leap, + 243 + leap, + 273 + leap, + 304 + leap, + 334 + leap, + ]; + + let year_day = year_day as i64; + + let month = match cumul_day_in_months.binary_search(&year_day) { + Ok(x) => x + 1, + Err(x) => x, + }; + + let month_day = 1 + year_day - cumul_day_in_months[month - 1]; + + (month, month_day) + } + RuleDay::MonthWeekday { month: rule_month, week, week_day } => { + let leap = is_leap_year(year) as i64; + + let month = rule_month as usize; + + let mut day_in_month = DAY_IN_MONTHS_NORMAL_YEAR[month - 1]; + if month == 2 { + day_in_month += leap; + } + + let week_day_of_first_month_day = + (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK); + let first_week_day_occurence_in_month = + 1 + (week_day as i64 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK); + + let mut month_day = + first_week_day_occurence_in_month + (week as i64 - 1) * DAYS_PER_WEEK; + if month_day > day_in_month { + month_day -= DAYS_PER_WEEK + } + + (month, month_day) + } + } + } + + /// Returns the UTC Unix time in seconds associated to the transition date for the provided year + fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 { + let (month, month_day) = self.transition_date(year); + days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc + } +} + +/// UTC date time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) struct UtcDateTime { + /// Year + pub(crate) year: i32, + /// Month in `[1, 12]` + pub(crate) month: u8, + /// Day of the month in `[1, 31]` + pub(crate) month_day: u8, + /// Hours since midnight in `[0, 23]` + pub(crate) hour: u8, + /// Minutes in `[0, 59]` + pub(crate) minute: u8, + /// Seconds in `[0, 60]`, with a possible leap second + pub(crate) second: u8, +} + +impl UtcDateTime { + /// Construct a UTC date time from a Unix time in seconds and nanoseconds + pub(crate) fn from_timespec(unix_time: i64) -> Result<Self, Error> { + let seconds = match unix_time.checked_sub(UNIX_OFFSET_SECS) { + Some(seconds) => seconds, + None => return Err(Error::OutOfRange("out of range operation")), + }; + + let mut remaining_days = seconds / SECONDS_PER_DAY; + let mut remaining_seconds = seconds % SECONDS_PER_DAY; + if remaining_seconds < 0 { + remaining_seconds += SECONDS_PER_DAY; + remaining_days -= 1; + } + + let mut cycles_400_years = remaining_days / DAYS_PER_400_YEARS; + remaining_days %= DAYS_PER_400_YEARS; + if remaining_days < 0 { + remaining_days += DAYS_PER_400_YEARS; + cycles_400_years -= 1; + } + + let cycles_100_years = Ord::min(remaining_days / DAYS_PER_100_YEARS, 3); + remaining_days -= cycles_100_years * DAYS_PER_100_YEARS; + + let cycles_4_years = Ord::min(remaining_days / DAYS_PER_4_YEARS, 24); + remaining_days -= cycles_4_years * DAYS_PER_4_YEARS; + + let remaining_years = Ord::min(remaining_days / DAYS_PER_NORMAL_YEAR, 3); + remaining_days -= remaining_years * DAYS_PER_NORMAL_YEAR; + + let mut year = OFFSET_YEAR + + remaining_years + + cycles_4_years * 4 + + cycles_100_years * 100 + + cycles_400_years * 400; + + let mut month = 0; + while month < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH.len() { + let days = DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month]; + if remaining_days < days { + break; + } + remaining_days -= days; + month += 1; + } + month += 2; + + if month >= MONTHS_PER_YEAR as usize { + month -= MONTHS_PER_YEAR as usize; + year += 1; + } + month += 1; + + let month_day = 1 + remaining_days; + + let hour = remaining_seconds / SECONDS_PER_HOUR; + let minute = (remaining_seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; + let second = remaining_seconds % SECONDS_PER_MINUTE; + + let year = match year >= i32::min_value() as i64 && year <= i32::max_value() as i64 { + true => year as i32, + false => return Err(Error::OutOfRange("i64 is out of range for i32")), + }; + + Ok(Self { + year, + month: month as u8, + month_day: month_day as u8, + hour: hour as u8, + minute: minute as u8, + second: second as u8, + }) + } +} + +/// Number of nanoseconds in one second +const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000; +/// Number of seconds in one minute +const SECONDS_PER_MINUTE: i64 = 60; +/// Number of seconds in one hour +const SECONDS_PER_HOUR: i64 = 3600; +/// Number of minutes in one hour +const MINUTES_PER_HOUR: i64 = 60; +/// Number of months in one year +const MONTHS_PER_YEAR: i64 = 12; +/// Number of days in a normal year +const DAYS_PER_NORMAL_YEAR: i64 = 365; +/// Number of days in 4 years (including 1 leap year) +const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1; +/// Number of days in 100 years (including 24 leap years) +const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24; +/// Number of days in 400 years (including 97 leap years) +const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97; +/// Unix time at `2000-03-01T00:00:00Z` (Wednesday) +const UNIX_OFFSET_SECS: i64 = 951868800; +/// Offset year +const OFFSET_YEAR: i64 = 2000; +/// Month days in a leap year from March +const DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH: [i64; 12] = + [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29]; + +/// Compute the number of days since Unix epoch (`1970-01-01T00:00:00Z`). +/// +/// ## Inputs +/// +/// * `year`: Year +/// * `month`: Month in `[1, 12]` +/// * `month_day`: Day of the month in `[1, 31]` +pub(crate) const fn days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64 { + let is_leap_year = is_leap_year(year); + + let year = year as i64; + + let mut result = (year - 1970) * 365; + + if year >= 1970 { + result += (year - 1968) / 4; + result -= (year - 1900) / 100; + result += (year - 1600) / 400; + + if is_leap_year && month < 3 { + result -= 1; + } + } else { + result += (year - 1972) / 4; + result -= (year - 2000) / 100; + result += (year - 2000) / 400; + + if is_leap_year && month >= 3 { + result += 1; + } + } + + result += CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1] + month_day - 1; + + result +} + +/// Check if a year is a leap year +pub(crate) const fn is_leap_year(year: i32) -> bool { + year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) +} + +#[cfg(test)] +mod tests { + use super::super::timezone::Transition; + use super::super::{Error, TimeZone}; + use super::{AlternateTime, LocalTimeType, RuleDay, TransitionRule}; + + #[test] + fn test_quoted() -> Result<(), Error> { + let transition_rule = TransitionRule::from_tz_string(b"<-03>+3<+03>-3,J1,J365", false)?; + assert_eq!( + transition_rule, + AlternateTime::new( + LocalTimeType::new(-10800, false, Some(b"-03"))?, + LocalTimeType::new(10800, true, Some(b"+03"))?, + RuleDay::julian_1(1)?, + 7200, + RuleDay::julian_1(365)?, + 7200, + )? + .into() + ); + Ok(()) + } + + #[test] + fn test_full() -> Result<(), Error> { + let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00"; + let transition_rule = TransitionRule::from_tz_string(tz_string, false)?; + assert_eq!( + transition_rule, + AlternateTime::new( + LocalTimeType::new(43200, false, Some(b"NZST"))?, + LocalTimeType::new(46800, true, Some(b"NZDT"))?, + RuleDay::month_weekday(10, 1, 0)?, + 7200, + RuleDay::month_weekday(3, 3, 0)?, + 7200, + )? + .into() + ); + Ok(()) + } + + #[test] + fn test_negative_dst() -> Result<(), Error> { + let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1"; + let transition_rule = TransitionRule::from_tz_string(tz_string, false)?; + assert_eq!( + transition_rule, + AlternateTime::new( + LocalTimeType::new(3600, false, Some(b"IST"))?, + LocalTimeType::new(0, true, Some(b"GMT"))?, + RuleDay::month_weekday(10, 5, 0)?, + 7200, + RuleDay::month_weekday(3, 5, 0)?, + 3600, + )? + .into() + ); + Ok(()) + } + + #[test] + fn test_negative_hour() -> Result<(), Error> { + let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"; + assert!(TransitionRule::from_tz_string(tz_string, false).is_err()); + + assert_eq!( + TransitionRule::from_tz_string(tz_string, true)?, + AlternateTime::new( + LocalTimeType::new(-10800, false, Some(b"-03"))?, + LocalTimeType::new(-7200, true, Some(b"-02"))?, + RuleDay::month_weekday(3, 5, 0)?, + -7200, + RuleDay::month_weekday(10, 5, 0)?, + -3600, + )? + .into() + ); + Ok(()) + } + + #[test] + fn test_all_year_dst() -> Result<(), Error> { + let tz_string = b"EST5EDT,0/0,J365/25"; + assert!(TransitionRule::from_tz_string(tz_string, false).is_err()); + + assert_eq!( + TransitionRule::from_tz_string(tz_string, true)?, + AlternateTime::new( + LocalTimeType::new(-18000, false, Some(b"EST"))?, + LocalTimeType::new(-14400, true, Some(b"EDT"))?, + RuleDay::julian_0(0)?, + 0, + RuleDay::julian_1(365)?, + 90000, + )? + .into() + ); + Ok(()) + } + + #[test] + fn test_v3_file() -> Result<(), Error> { + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\x1c\x20\0\0IST\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\0\0\0\0\x7f\xe8\x17\x80\0\0\0\x1c\x20\0\0IST\0\x01\x01\x0aIST-2IDT,M3.4.4/26,M10.5.0\x0a"; + + let time_zone = TimeZone::from_tz_data(bytes)?; + + let time_zone_result = TimeZone::new( + vec![Transition::new(2145916800, 0)], + vec![LocalTimeType::new(7200, false, Some(b"IST"))?], + Vec::new(), + Some(TransitionRule::from(AlternateTime::new( + LocalTimeType::new(7200, false, Some(b"IST"))?, + LocalTimeType::new(10800, true, Some(b"IDT"))?, + RuleDay::month_weekday(3, 4, 4)?, + 93600, + RuleDay::month_weekday(10, 5, 0)?, + 7200, + )?)), + )?; + + assert_eq!(time_zone, time_zone_result); + + Ok(()) + } + + #[test] + fn test_rule_day() -> Result<(), Error> { + let rule_day_j1 = RuleDay::julian_1(60)?; + assert_eq!(rule_day_j1.transition_date(2000), (3, 1)); + assert_eq!(rule_day_j1.transition_date(2001), (3, 1)); + assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000); + + let rule_day_j0 = RuleDay::julian_0(59)?; + assert_eq!(rule_day_j0.transition_date(2000), (2, 29)); + assert_eq!(rule_day_j0.transition_date(2001), (3, 1)); + assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600); + + let rule_day_mwd = RuleDay::month_weekday(2, 5, 2)?; + assert_eq!(rule_day_mwd.transition_date(2000), (2, 29)); + assert_eq!(rule_day_mwd.transition_date(2001), (2, 27)); + assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600); + assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200); + + Ok(()) + } + + #[test] + fn test_transition_rule() -> Result<(), Error> { + let transition_rule_fixed = TransitionRule::from(LocalTimeType::new(-36000, false, None)?); + assert_eq!(transition_rule_fixed.find_local_time_type(0)?.offset(), -36000); + + let transition_rule_dst = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(43200, false, Some(b"NZST"))?, + LocalTimeType::new(46800, true, Some(b"NZDT"))?, + RuleDay::month_weekday(10, 1, 0)?, + 7200, + RuleDay::month_weekday(3, 3, 0)?, + 7200, + )?); + + assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.offset(), 46800); + assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.offset(), 43200); + assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.offset(), 43200); + assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.offset(), 46800); + + let transition_rule_negative_dst = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(3600, false, Some(b"IST"))?, + LocalTimeType::new(0, true, Some(b"GMT"))?, + RuleDay::month_weekday(10, 5, 0)?, + 7200, + RuleDay::month_weekday(3, 5, 0)?, + 3600, + )?); + + assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.offset(), 0); + assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.offset(), 3600); + assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.offset(), 3600); + assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.offset(), 0); + + let transition_rule_negative_time_1 = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(0, false, None)?, + LocalTimeType::new(0, true, None)?, + RuleDay::julian_0(100)?, + 0, + RuleDay::julian_0(101)?, + -86500, + )?); + + assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst()); + assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst()); + assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst()); + assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst()); + + let transition_rule_negative_time_2 = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(-10800, false, Some(b"-03"))?, + LocalTimeType::new(-7200, true, Some(b"-02"))?, + RuleDay::month_weekday(3, 5, 0)?, + -7200, + RuleDay::month_weekday(10, 5, 0)?, + -3600, + )?); + + assert_eq!( + transition_rule_negative_time_2.find_local_time_type(954032399)?.offset(), + -10800 + ); + assert_eq!( + transition_rule_negative_time_2.find_local_time_type(954032400)?.offset(), + -7200 + ); + assert_eq!( + transition_rule_negative_time_2.find_local_time_type(972781199)?.offset(), + -7200 + ); + assert_eq!( + transition_rule_negative_time_2.find_local_time_type(972781200)?.offset(), + -10800 + ); + + let transition_rule_all_year_dst = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(-18000, false, Some(b"EST"))?, + LocalTimeType::new(-14400, true, Some(b"EDT"))?, + RuleDay::julian_0(0)?, + 0, + RuleDay::julian_1(365)?, + 90000, + )?); + + assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.offset(), -14400); + assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.offset(), -14400); + + Ok(()) + } + + #[test] + fn test_transition_rule_overflow() -> Result<(), Error> { + let transition_rule_1 = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(-1, false, None)?, + LocalTimeType::new(-1, true, None)?, + RuleDay::julian_1(365)?, + 0, + RuleDay::julian_1(1)?, + 0, + )?); + + let transition_rule_2 = TransitionRule::from(AlternateTime::new( + LocalTimeType::new(1, false, None)?, + LocalTimeType::new(1, true, None)?, + RuleDay::julian_1(365)?, + 0, + RuleDay::julian_1(1)?, + 0, + )?); + + let min_unix_time = -67768100567971200; + let max_unix_time = 67767976233532799; + + assert!(matches!( + transition_rule_1.find_local_time_type(min_unix_time), + Err(Error::OutOfRange(_)) + )); + assert!(matches!( + transition_rule_2.find_local_time_type(max_unix_time), + Err(Error::OutOfRange(_)) + )); + + Ok(()) + } +} diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs new file mode 100644 index 0000000..02b6f34 --- /dev/null +++ b/src/offset/local/tz_info/timezone.rs @@ -0,0 +1,950 @@ +//! Types related to a time zone. + +use std::fs::{self, File}; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; +use std::{cmp::Ordering, fmt, str}; + +use super::rule::{AlternateTime, TransitionRule}; +use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY}; + +/// Time zone +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct TimeZone { + /// List of transitions + transitions: Vec<Transition>, + /// List of local time types (cannot be empty) + local_time_types: Vec<LocalTimeType>, + /// List of leap seconds + leap_seconds: Vec<LeapSecond>, + /// Extra transition rule applicable after the last transition + extra_rule: Option<TransitionRule>, +} + +impl TimeZone { + /// Returns local time zone. + /// + /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. + /// + pub(crate) fn local(env_tz: Option<&str>) -> Result<Self, Error> { + match env_tz { + Some(tz) => Self::from_posix_tz(tz), + None => Self::from_posix_tz("localtime"), + } + } + + /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). + fn from_posix_tz(tz_string: &str) -> Result<Self, Error> { + if tz_string.is_empty() { + return Err(Error::InvalidTzString("empty TZ string")); + } + + if tz_string == "localtime" { + return Self::from_tz_data(&fs::read("/etc/localtime")?); + } + + // attributes are not allowed on if blocks in Rust 1.38 + #[cfg(target_os = "android")] + { + if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) { + return Self::from_tz_data(&bytes); + } + } + + let mut chars = tz_string.chars(); + if chars.next() == Some(':') { + return Self::from_file(&mut find_tz_file(chars.as_str())?); + } + + if let Ok(mut file) = find_tz_file(tz_string) { + return Self::from_file(&mut file); + } + + // TZ string extensions are not allowed + let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace()); + let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?; + Self::new( + vec![], + match rule { + TransitionRule::Fixed(local_time_type) => vec![local_time_type], + TransitionRule::Alternate(AlternateTime { std, dst, .. }) => vec![std, dst], + }, + vec![], + Some(rule), + ) + } + + /// Construct a time zone + pub(super) fn new( + transitions: Vec<Transition>, + local_time_types: Vec<LocalTimeType>, + leap_seconds: Vec<LeapSecond>, + extra_rule: Option<TransitionRule>, + ) -> Result<Self, Error> { + let new = Self { transitions, local_time_types, leap_seconds, extra_rule }; + new.as_ref().validate()?; + Ok(new) + } + + /// Construct a time zone from the contents of a time zone file + fn from_file(file: &mut File) -> Result<Self, Error> { + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes)?; + Self::from_tz_data(&bytes) + } + + /// Construct a time zone from the contents of a time zone file + /// + /// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536). + pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> { + parser::parse(bytes) + } + + /// Construct a time zone with the specified UTC offset in seconds + fn fixed(ut_offset: i32) -> Result<Self, Error> { + Ok(Self { + transitions: Vec::new(), + local_time_types: vec![LocalTimeType::with_offset(ut_offset)?], + leap_seconds: Vec::new(), + extra_rule: None, + }) + } + + /// Construct the time zone associated to UTC + pub(crate) fn utc() -> Self { + Self { + transitions: Vec::new(), + local_time_types: vec![LocalTimeType::UTC], + leap_seconds: Vec::new(), + extra_rule: None, + } + } + + /// Find the local time type associated to the time zone at the specified Unix time in seconds + pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> { + self.as_ref().find_local_time_type(unix_time) + } + + // should we pass NaiveDateTime all the way through to this fn? + pub(crate) fn find_local_time_type_from_local( + &self, + local_time: i64, + year: i32, + ) -> Result<crate::LocalResult<LocalTimeType>, Error> { + self.as_ref().find_local_time_type_from_local(local_time, year) + } + + /// Returns a reference to the time zone + fn as_ref(&self) -> TimeZoneRef { + TimeZoneRef { + transitions: &self.transitions, + local_time_types: &self.local_time_types, + leap_seconds: &self.leap_seconds, + extra_rule: &self.extra_rule, + } + } +} + +/// Reference to a time zone +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) struct TimeZoneRef<'a> { + /// List of transitions + transitions: &'a [Transition], + /// List of local time types (cannot be empty) + local_time_types: &'a [LocalTimeType], + /// List of leap seconds + leap_seconds: &'a [LeapSecond], + /// Extra transition rule applicable after the last transition + extra_rule: &'a Option<TransitionRule>, +} + +impl<'a> TimeZoneRef<'a> { + /// Find the local time type associated to the time zone at the specified Unix time in seconds + pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, Error> { + let extra_rule = match self.transitions.last() { + None => match self.extra_rule { + Some(extra_rule) => extra_rule, + None => return Ok(&self.local_time_types[0]), + }, + Some(last_transition) => { + let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) { + Ok(unix_leap_time) => unix_leap_time, + Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)), + Err(err) => return Err(err), + }; + + if unix_leap_time >= last_transition.unix_leap_time { + match self.extra_rule { + Some(extra_rule) => extra_rule, + None => { + // RFC 8536 3.2: + // "Local time for timestamps on or after the last transition is + // specified by the TZ string in the footer (Section 3.3) if present + // and nonempty; otherwise, it is unspecified." + // + // Older versions of macOS (1.12 and before?) have TZif file with a + // missing TZ string, and use the offset given by the last transition. + return Ok( + &self.local_time_types[last_transition.local_time_type_index] + ); + } + } + } else { + let index = match self + .transitions + .binary_search_by_key(&unix_leap_time, Transition::unix_leap_time) + { + Ok(x) => x + 1, + Err(x) => x, + }; + + let local_time_type_index = if index > 0 { + self.transitions[index - 1].local_time_type_index + } else { + 0 + }; + return Ok(&self.local_time_types[local_time_type_index]); + } + } + }; + + match extra_rule.find_local_time_type(unix_time) { + Ok(local_time_type) => Ok(local_time_type), + Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)), + err => err, + } + } + + pub(crate) fn find_local_time_type_from_local( + &self, + local_time: i64, + year: i32, + ) -> Result<crate::LocalResult<LocalTimeType>, Error> { + // #TODO: this is wrong as we need 'local_time_to_local_leap_time ? + // but ... does the local time even include leap seconds ?? + // let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) { + // Ok(unix_leap_time) => unix_leap_time, + // Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)), + // Err(err) => return Err(err), + // }; + let local_leap_time = local_time; + + // if we have at least one transition, + // we must check _all_ of them, incase of any Overlapping (LocalResult::Ambiguous) or Skipping (LocalResult::None) transitions + let offset_after_last = if !self.transitions.is_empty() { + let mut prev = self.local_time_types[0]; + + for transition in self.transitions { + let after_ltt = self.local_time_types[transition.local_time_type_index]; + + // the end and start here refers to where the time starts prior to the transition + // and where it ends up after. not the temporal relationship. + let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset); + let transition_start = transition.unix_leap_time + i64::from(prev.ut_offset); + + match transition_start.cmp(&transition_end) { + Ordering::Greater => { + // bakwards transition, eg from DST to regular + // this means a given local time could have one of two possible offsets + if local_leap_time < transition_end { + return Ok(crate::LocalResult::Single(prev)); + } else if local_leap_time >= transition_end + && local_leap_time <= transition_start + { + if prev.ut_offset < after_ltt.ut_offset { + return Ok(crate::LocalResult::Ambiguous(prev, after_ltt)); + } else { + return Ok(crate::LocalResult::Ambiguous(after_ltt, prev)); + } + } + } + Ordering::Equal => { + // should this ever happen? presumably we have to handle it anyway. + if local_leap_time < transition_start { + return Ok(crate::LocalResult::Single(prev)); + } else if local_leap_time == transition_end { + if prev.ut_offset < after_ltt.ut_offset { + return Ok(crate::LocalResult::Ambiguous(prev, after_ltt)); + } else { + return Ok(crate::LocalResult::Ambiguous(after_ltt, prev)); + } + } + } + Ordering::Less => { + // forwards transition, eg from regular to DST + // this means that times that are skipped are invalid local times + if local_leap_time <= transition_start { + return Ok(crate::LocalResult::Single(prev)); + } else if local_leap_time < transition_end { + return Ok(crate::LocalResult::None); + } else if local_leap_time == transition_end { + return Ok(crate::LocalResult::Single(after_ltt)); + } + } + } + + // try the next transition, we are fully after this one + prev = after_ltt; + } + + prev + } else { + self.local_time_types[0] + }; + + if let Some(extra_rule) = self.extra_rule { + match extra_rule.find_local_time_type_from_local(local_time, year) { + Ok(local_time_type) => Ok(local_time_type), + Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)), + err => err, + } + } else { + Ok(crate::LocalResult::Single(offset_after_last)) + } + } + + /// Check time zone inputs + fn validate(&self) -> Result<(), Error> { + // Check local time types + let local_time_types_size = self.local_time_types.len(); + if local_time_types_size == 0 { + return Err(Error::TimeZone("list of local time types must not be empty")); + } + + // Check transitions + let mut i_transition = 0; + while i_transition < self.transitions.len() { + if self.transitions[i_transition].local_time_type_index >= local_time_types_size { + return Err(Error::TimeZone("invalid local time type index")); + } + + if i_transition + 1 < self.transitions.len() + && self.transitions[i_transition].unix_leap_time + >= self.transitions[i_transition + 1].unix_leap_time + { + return Err(Error::TimeZone("invalid transition")); + } + + i_transition += 1; + } + + // Check leap seconds + if !(self.leap_seconds.is_empty() + || self.leap_seconds[0].unix_leap_time >= 0 + && self.leap_seconds[0].correction.saturating_abs() == 1) + { + return Err(Error::TimeZone("invalid leap second")); + } + + let min_interval = SECONDS_PER_28_DAYS - 1; + + let mut i_leap_second = 0; + while i_leap_second < self.leap_seconds.len() { + if i_leap_second + 1 < self.leap_seconds.len() { + let x0 = &self.leap_seconds[i_leap_second]; + let x1 = &self.leap_seconds[i_leap_second + 1]; + + let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time); + let abs_diff_correction = + x1.correction.saturating_sub(x0.correction).saturating_abs(); + + if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) { + return Err(Error::TimeZone("invalid leap second")); + } + } + i_leap_second += 1; + } + + // Check extra rule + let (extra_rule, last_transition) = match (&self.extra_rule, self.transitions.last()) { + (Some(rule), Some(trans)) => (rule, trans), + _ => return Ok(()), + }; + + let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index]; + let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) { + Ok(unix_time) => unix_time, + Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)), + Err(err) => return Err(err), + }; + + let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) { + Ok(rule_local_time_type) => rule_local_time_type, + Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)), + Err(err) => return Err(err), + }; + + let check = last_local_time_type.ut_offset == rule_local_time_type.ut_offset + && last_local_time_type.is_dst == rule_local_time_type.is_dst + && match (&last_local_time_type.name, &rule_local_time_type.name) { + (Some(x), Some(y)) => x.equal(y), + (None, None) => true, + _ => false, + }; + + if !check { + return Err(Error::TimeZone( + "extra transition rule is inconsistent with the last transition", + )); + } + + Ok(()) + } + + /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone + const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, Error> { + let mut unix_leap_time = unix_time; + + let mut i = 0; + while i < self.leap_seconds.len() { + let leap_second = &self.leap_seconds[i]; + + if unix_leap_time < leap_second.unix_leap_time { + break; + } + + unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) { + Some(unix_leap_time) => unix_leap_time, + None => return Err(Error::OutOfRange("out of range operation")), + }; + + i += 1; + } + + Ok(unix_leap_time) + } + + /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone + fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, Error> { + if unix_leap_time == i64::min_value() { + return Err(Error::OutOfRange("out of range operation")); + } + + let index = match self + .leap_seconds + .binary_search_by_key(&(unix_leap_time - 1), LeapSecond::unix_leap_time) + { + Ok(x) => x + 1, + Err(x) => x, + }; + + let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 }; + + match unix_leap_time.checked_sub(correction as i64) { + Some(unix_time) => Ok(unix_time), + None => Err(Error::OutOfRange("out of range operation")), + } + } + + /// The UTC time zone + const UTC: TimeZoneRef<'static> = TimeZoneRef { + transitions: &[], + local_time_types: &[LocalTimeType::UTC], + leap_seconds: &[], + extra_rule: &None, + }; +} + +/// Transition of a TZif file +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) struct Transition { + /// Unix leap time + unix_leap_time: i64, + /// Index specifying the local time type of the transition + local_time_type_index: usize, +} + +impl Transition { + /// Construct a TZif file transition + pub(super) const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self { + Self { unix_leap_time, local_time_type_index } + } + + /// Returns Unix leap time + const fn unix_leap_time(&self) -> i64 { + self.unix_leap_time + } +} + +/// Leap second of a TZif file +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) struct LeapSecond { + /// Unix leap time + unix_leap_time: i64, + /// Leap second correction + correction: i32, +} + +impl LeapSecond { + /// Construct a TZif file leap second + pub(super) const fn new(unix_leap_time: i64, correction: i32) -> Self { + Self { unix_leap_time, correction } + } + + /// Returns Unix leap time + const fn unix_leap_time(&self) -> i64 { + self.unix_leap_time + } +} + +/// ASCII-encoded fixed-capacity string, used for storing time zone names +#[derive(Copy, Clone, Eq, PartialEq)] +struct TimeZoneName { + /// Length-prefixed string buffer + bytes: [u8; 8], +} + +impl TimeZoneName { + /// Construct a time zone name + /// + /// man tzfile(5): + /// Time zone designations should consist of at least three (3) and no more than six (6) ASCII + /// characters from the set of alphanumerics, “-”, and “+”. This is for compatibility with + /// POSIX requirements for time zone abbreviations. + fn new(input: &[u8]) -> Result<Self, Error> { + let len = input.len(); + + if !(3..=7).contains(&len) { + return Err(Error::LocalTimeType( + "time zone name must have between 3 and 7 characters", + )); + } + + let mut bytes = [0; 8]; + bytes[0] = input.len() as u8; + + let mut i = 0; + while i < len { + let b = input[i]; + match b { + b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-' => {} + _ => return Err(Error::LocalTimeType("invalid characters in time zone name")), + } + + bytes[i + 1] = b; + i += 1; + } + + Ok(Self { bytes }) + } + + /// Returns time zone name as a byte slice + fn as_bytes(&self) -> &[u8] { + match self.bytes[0] { + 3 => &self.bytes[1..4], + 4 => &self.bytes[1..5], + 5 => &self.bytes[1..6], + 6 => &self.bytes[1..7], + 7 => &self.bytes[1..8], + _ => unreachable!(), + } + } + + /// Check if two time zone names are equal + fn equal(&self, other: &Self) -> bool { + self.bytes == other.bytes + } +} + +impl AsRef<str> for TimeZoneName { + fn as_ref(&self) -> &str { + // SAFETY: ASCII is valid UTF-8 + unsafe { str::from_utf8_unchecked(self.as_bytes()) } + } +} + +impl fmt::Debug for TimeZoneName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.as_ref().fmt(f) + } +} + +/// Local time type associated to a time zone +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) struct LocalTimeType { + /// Offset from UTC in seconds + pub(super) ut_offset: i32, + /// Daylight Saving Time indicator + is_dst: bool, + /// Time zone name + name: Option<TimeZoneName>, +} + +impl LocalTimeType { + /// Construct a local time type + pub(super) fn new(ut_offset: i32, is_dst: bool, name: Option<&[u8]>) -> Result<Self, Error> { + if ut_offset == i32::min_value() { + return Err(Error::LocalTimeType("invalid UTC offset")); + } + + let name = match name { + Some(name) => TimeZoneName::new(name)?, + None => return Ok(Self { ut_offset, is_dst, name: None }), + }; + + Ok(Self { ut_offset, is_dst, name: Some(name) }) + } + + /// Construct a local time type with the specified UTC offset in seconds + pub(super) const fn with_offset(ut_offset: i32) -> Result<Self, Error> { + if ut_offset == i32::min_value() { + return Err(Error::LocalTimeType("invalid UTC offset")); + } + + Ok(Self { ut_offset, is_dst: false, name: None }) + } + + /// Returns offset from UTC in seconds + pub(crate) const fn offset(&self) -> i32 { + self.ut_offset + } + + /// Returns daylight saving time indicator + pub(super) const fn is_dst(&self) -> bool { + self.is_dst + } + + pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None }; +} + +/// Open the TZif file corresponding to a TZ string +fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> { + // Don't check system timezone directories on non-UNIX platforms + #[cfg(not(unix))] + return Ok(File::open(path)?); + + #[cfg(unix)] + { + let path = path.as_ref(); + if path.is_absolute() { + return Ok(File::open(path)?); + } + + for folder in &ZONE_INFO_DIRECTORIES { + if let Ok(file) = File::open(PathBuf::from(folder).join(path)) { + return Ok(file); + } + } + + Err(Error::Io(io::ErrorKind::NotFound.into())) + } +} + +// Possible system timezone directories +#[cfg(unix)] +const ZONE_INFO_DIRECTORIES: [&str; 4] = + ["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"]; + +/// Number of seconds in one week +pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK; +/// Number of seconds in 28 days +const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28; + +#[cfg(test)] +mod tests { + use super::super::Error; + use super::{LeapSecond, LocalTimeType, TimeZone, TimeZoneName, Transition, TransitionRule}; + + #[test] + fn test_no_dst() -> Result<(), Error> { + let tz_string = b"HST10"; + let transition_rule = TransitionRule::from_tz_string(tz_string, false)?; + assert_eq!(transition_rule, LocalTimeType::new(-36000, false, Some(b"HST"))?.into()); + Ok(()) + } + + #[test] + fn test_error() -> Result<(), Error> { + assert!(matches!( + TransitionRule::from_tz_string(b"IST-1GMT0", false), + Err(Error::UnsupportedTzString(_)) + )); + assert!(matches!( + TransitionRule::from_tz_string(b"EET-2EEST", false), + Err(Error::UnsupportedTzString(_)) + )); + + Ok(()) + } + + #[test] + fn test_v1_file_with_leap_seconds() -> Result<(), Error> { + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0"; + + let time_zone = TimeZone::from_tz_data(bytes)?; + + let time_zone_result = TimeZone::new( + Vec::new(), + vec![LocalTimeType::new(0, false, Some(b"UTC"))?], + vec![ + LeapSecond::new(78796800, 1), + LeapSecond::new(94694401, 2), + LeapSecond::new(126230402, 3), + LeapSecond::new(157766403, 4), + LeapSecond::new(189302404, 5), + LeapSecond::new(220924805, 6), + LeapSecond::new(252460806, 7), + LeapSecond::new(283996807, 8), + LeapSecond::new(315532808, 9), + LeapSecond::new(362793609, 10), + LeapSecond::new(394329610, 11), + LeapSecond::new(425865611, 12), + LeapSecond::new(489024012, 13), + LeapSecond::new(567993613, 14), + LeapSecond::new(631152014, 15), + LeapSecond::new(662688015, 16), + LeapSecond::new(709948816, 17), + LeapSecond::new(741484817, 18), + LeapSecond::new(773020818, 19), + LeapSecond::new(820454419, 20), + LeapSecond::new(867715220, 21), + LeapSecond::new(915148821, 22), + LeapSecond::new(1136073622, 23), + LeapSecond::new(1230768023, 24), + LeapSecond::new(1341100824, 25), + LeapSecond::new(1435708825, 26), + LeapSecond::new(1483228826, 27), + ], + None, + )?; + + assert_eq!(time_zone, time_zone_result); + + Ok(()) + } + + #[test] + fn test_v2_file() -> Result<(), Error> { + let bytes = b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10\x0a"; + + let time_zone = TimeZone::from_tz_data(bytes)?; + + let time_zone_result = TimeZone::new( + vec![ + Transition::new(-2334101314, 1), + Transition::new(-1157283000, 2), + Transition::new(-1155436200, 1), + Transition::new(-880198200, 3), + Transition::new(-769395600, 4), + Transition::new(-765376200, 1), + Transition::new(-712150200, 5), + ], + vec![ + LocalTimeType::new(-37886, false, Some(b"LMT"))?, + LocalTimeType::new(-37800, false, Some(b"HST"))?, + LocalTimeType::new(-34200, true, Some(b"HDT"))?, + LocalTimeType::new(-34200, true, Some(b"HWT"))?, + LocalTimeType::new(-34200, true, Some(b"HPT"))?, + LocalTimeType::new(-36000, false, Some(b"HST"))?, + ], + Vec::new(), + Some(TransitionRule::from(LocalTimeType::new(-36000, false, Some(b"HST"))?)), + )?; + + assert_eq!(time_zone, time_zone_result); + + assert_eq!( + *time_zone.find_local_time_type(-1156939200)?, + LocalTimeType::new(-34200, true, Some(b"HDT"))? + ); + assert_eq!( + *time_zone.find_local_time_type(1546300800)?, + LocalTimeType::new(-36000, false, Some(b"HST"))? + ); + + Ok(()) + } + + #[test] + fn test_no_tz_string() -> Result<(), Error> { + // Guayaquil from macOS 10.11 + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\x02\0\0\0\0\0\0\0\x01\0\0\0\x02\0\0\0\x08\xb6\xa4B\x18\x01\xff\xff\xb6h\0\0\xff\xff\xb9\xb0\0\x04QMT\0ECT\0\0\0\0\0"; + + let time_zone = TimeZone::from_tz_data(bytes)?; + dbg!(&time_zone); + + let time_zone_result = TimeZone::new( + vec![Transition::new(-1230749160, 1)], + vec![ + LocalTimeType::new(-18840, false, Some(b"QMT"))?, + LocalTimeType::new(-18000, false, Some(b"ECT"))?, + ], + Vec::new(), + None, + )?; + + assert_eq!(time_zone, time_zone_result); + + assert_eq!( + *time_zone.find_local_time_type(-1500000000)?, + LocalTimeType::new(-18840, false, Some(b"QMT"))? + ); + assert_eq!( + *time_zone.find_local_time_type(0)?, + LocalTimeType::new(-18000, false, Some(b"ECT"))? + ); + + Ok(()) + } + + #[test] + fn test_tz_ascii_str() -> Result<(), Error> { + assert!(matches!(TimeZoneName::new(b""), Err(Error::LocalTimeType(_)))); + assert!(matches!(TimeZoneName::new(b"A"), Err(Error::LocalTimeType(_)))); + assert!(matches!(TimeZoneName::new(b"AB"), Err(Error::LocalTimeType(_)))); + assert_eq!(TimeZoneName::new(b"CET")?.as_bytes(), b"CET"); + assert_eq!(TimeZoneName::new(b"CHADT")?.as_bytes(), b"CHADT"); + assert_eq!(TimeZoneName::new(b"abcdefg")?.as_bytes(), b"abcdefg"); + assert_eq!(TimeZoneName::new(b"UTC+02")?.as_bytes(), b"UTC+02"); + assert_eq!(TimeZoneName::new(b"-1230")?.as_bytes(), b"-1230"); + assert!(matches!(TimeZoneName::new("−0330".as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212) + assert!(matches!(TimeZoneName::new(b"\x00123"), Err(Error::LocalTimeType(_)))); + assert!(matches!(TimeZoneName::new(b"12345678"), Err(Error::LocalTimeType(_)))); + assert!(matches!(TimeZoneName::new(b"GMT\0\0\0"), Err(Error::LocalTimeType(_)))); + + Ok(()) + } + + #[test] + fn test_time_zone() -> Result<(), Error> { + let utc = LocalTimeType::UTC; + let cet = LocalTimeType::with_offset(3600)?; + + let utc_local_time_types = vec![utc]; + let fixed_extra_rule = TransitionRule::from(cet); + + let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?; + let time_zone_2 = + TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?; + let time_zone_3 = + TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?; + let time_zone_4 = TimeZone::new( + vec![Transition::new(i32::min_value().into(), 0), Transition::new(0, 1)], + vec![utc, cet], + Vec::new(), + Some(fixed_extra_rule), + )?; + + assert_eq!(*time_zone_1.find_local_time_type(0)?, utc); + assert_eq!(*time_zone_2.find_local_time_type(0)?, cet); + + assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc); + assert_eq!(*time_zone_3.find_local_time_type(0)?, utc); + + assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc); + assert_eq!(*time_zone_4.find_local_time_type(0)?, cet); + + let time_zone_err = TimeZone::new( + vec![Transition::new(0, 0)], + utc_local_time_types, + vec![], + Some(fixed_extra_rule), + ); + assert!(time_zone_err.is_err()); + + Ok(()) + } + + #[test] + fn test_time_zone_from_posix_tz() -> Result<(), Error> { + #[cfg(unix)] + { + // if the TZ var is set, this essentially _overrides_ the + // time set by the localtime symlink + // so just ensure that ::local() acts as expected + // in this case + if let Ok(tz) = std::env::var("TZ") { + let time_zone_local = TimeZone::local(Some(tz.as_str()))?; + let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?; + assert_eq!(time_zone_local, time_zone_local_1); + } + + // `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have + // a time zone database, like for example some docker containers. + // In that case skip the test. + if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC") { + assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); + } + } + + assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err()); + assert!(TimeZone::from_posix_tz("").is_err()); + + Ok(()) + } + + #[test] + fn test_leap_seconds() -> Result<(), Error> { + let time_zone = TimeZone::new( + Vec::new(), + vec![LocalTimeType::new(0, false, Some(b"UTC"))?], + vec![ + LeapSecond::new(78796800, 1), + LeapSecond::new(94694401, 2), + LeapSecond::new(126230402, 3), + LeapSecond::new(157766403, 4), + LeapSecond::new(189302404, 5), + LeapSecond::new(220924805, 6), + LeapSecond::new(252460806, 7), + LeapSecond::new(283996807, 8), + LeapSecond::new(315532808, 9), + LeapSecond::new(362793609, 10), + LeapSecond::new(394329610, 11), + LeapSecond::new(425865611, 12), + LeapSecond::new(489024012, 13), + LeapSecond::new(567993613, 14), + LeapSecond::new(631152014, 15), + LeapSecond::new(662688015, 16), + LeapSecond::new(709948816, 17), + LeapSecond::new(741484817, 18), + LeapSecond::new(773020818, 19), + LeapSecond::new(820454419, 20), + LeapSecond::new(867715220, 21), + LeapSecond::new(915148821, 22), + LeapSecond::new(1136073622, 23), + LeapSecond::new(1230768023, 24), + LeapSecond::new(1341100824, 25), + LeapSecond::new(1435708825, 26), + LeapSecond::new(1483228826, 27), + ], + None, + )?; + + let time_zone_ref = time_zone.as_ref(); + + assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599))); + assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600))); + assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600))); + assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601))); + + assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621))); + assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623))); + assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624))); + + Ok(()) + } + + #[test] + fn test_leap_seconds_overflow() -> Result<(), Error> { + let time_zone_err = TimeZone::new( + vec![Transition::new(i64::min_value(), 0)], + vec![LocalTimeType::UTC], + vec![LeapSecond::new(0, 1)], + Some(TransitionRule::from(LocalTimeType::UTC)), + ); + assert!(time_zone_err.is_err()); + + let time_zone = TimeZone::new( + vec![Transition::new(i64::max_value(), 0)], + vec![LocalTimeType::UTC], + vec![LeapSecond::new(0, 1)], + None, + )?; + assert!(matches!( + time_zone.find_local_time_type(i64::max_value()), + Err(Error::FindLocalTimeType(_)) + )); + + Ok(()) + } +} diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs new file mode 100644 index 0000000..ce96a6e --- /dev/null +++ b/src/offset/local/unix.rs @@ -0,0 +1,171 @@ +// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime}; + +use super::tz_info::TimeZone; +use super::{FixedOffset, NaiveDateTime}; +use crate::{Datelike, LocalResult}; + +pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> { + offset(utc, false) +} + +pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> { + offset(local, true) +} + +fn offset(d: &NaiveDateTime, local: bool) -> LocalResult<FixedOffset> { + TZ_INFO.with(|maybe_cache| { + maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local) + }) +} + +// we have to store the `Cache` in an option as it can't +// be initalized in a static context. +thread_local! { + static TZ_INFO: RefCell<Option<Cache>> = Default::default(); +} + +enum Source { + LocalTime { mtime: SystemTime }, + Environment { hash: u64 }, +} + +impl Source { + fn new(env_tz: Option<&str>) -> Source { + match env_tz { + Some(tz) => { + let mut hasher = hash_map::DefaultHasher::new(); + hasher.write(tz.as_bytes()); + let hash = hasher.finish(); + Source::Environment { hash } + } + None => match fs::symlink_metadata("/etc/localtime") { + Ok(data) => Source::LocalTime { + // we have to pick a sensible default when the mtime fails + // by picking SystemTime::now() we raise the probability of + // the cache being invalidated if/when the mtime starts working + mtime: data.modified().unwrap_or_else(|_| SystemTime::now()), + }, + Err(_) => { + // as above, now() should be a better default than some constant + // TODO: see if we can improve caching in the case where the fallback is a valid timezone + Source::LocalTime { mtime: SystemTime::now() } + } + }, + } + } +} + +struct Cache { + zone: TimeZone, + source: Source, + last_checked: SystemTime, +} + +#[cfg(target_os = "aix")] +const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo"; + +#[cfg(not(any(target_os = "android", target_os = "aix")))] +const TZDB_LOCATION: &str = "/usr/share/zoneinfo"; + +fn fallback_timezone() -> Option<TimeZone> { + let tz_name = iana_time_zone::get_timezone().ok()?; + #[cfg(not(target_os = "android"))] + let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).ok()?; + #[cfg(target_os = "android")] + let bytes = android_tzdata::find_tz_data(&tz_name).ok()?; + TimeZone::from_tz_data(&bytes).ok() +} + +impl Default for Cache { + fn default() -> Cache { + // default to UTC if no local timezone can be found + let env_tz = env::var("TZ").ok(); + let env_ref = env_tz.as_deref(); + Cache { + last_checked: SystemTime::now(), + source: Source::new(env_ref), + zone: current_zone(env_ref), + } + } +} + +fn current_zone(var: Option<&str>) -> TimeZone { + TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc) +} + +impl Cache { + fn offset(&mut self, d: NaiveDateTime, local: bool) -> LocalResult<FixedOffset> { + let now = SystemTime::now(); + + match now.duration_since(self.last_checked) { + // If the cache has been around for less than a second then we reuse it + // unconditionally. This is a reasonable tradeoff because the timezone + // generally won't be changing _that_ often, but if the time zone does + // change, it will reflect sufficiently quickly from an application + // user's perspective. + Ok(d) if d.as_secs() < 1 => (), + Ok(_) | Err(_) => { + let env_tz = env::var("TZ").ok(); + let env_ref = env_tz.as_deref(); + let new_source = Source::new(env_ref); + + let out_of_date = match (&self.source, &new_source) { + // change from env to file or file to env, must recreate the zone + (Source::Environment { .. }, Source::LocalTime { .. }) + | (Source::LocalTime { .. }, Source::Environment { .. }) => true, + // stay as file, but mtime has changed + (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime }) + if old_mtime != mtime => + { + true + } + // stay as env, but hash of variable has changed + (Source::Environment { hash: old_hash }, Source::Environment { hash }) + if old_hash != hash => + { + true + } + // cache can be reused + _ => false, + }; + + if out_of_date { + self.zone = current_zone(env_ref); + } + + self.last_checked = now; + self.source = new_source; + } + } + + if !local { + let offset = self + .zone + .find_local_time_type(d.timestamp()) + .expect("unable to select local time type") + .offset(); + + return match FixedOffset::east_opt(offset) { + Some(offset) => LocalResult::Single(offset), + None => LocalResult::None, + }; + } + + // we pass through the year as the year of a local point in time must either be valid in that locale, or + // the entire time was skipped in which case we will return LocalResult::None anyway. + self.zone + .find_local_time_type_from_local(d.timestamp(), d.year()) + .expect("unable to select local time type") + .map(|o| FixedOffset::east_opt(o.offset()).unwrap()) + } +} diff --git a/src/offset/local/win_bindings.rs b/src/offset/local/win_bindings.rs new file mode 100644 index 0000000..7574fb3 --- /dev/null +++ b/src/offset/local/win_bindings.rs @@ -0,0 +1,71 @@ +// Bindings generated by `windows-bindgen` 0.52.0 + +#![allow(non_snake_case, non_upper_case_globals, non_camel_case_types, dead_code, clippy::all)] +::windows_targets::link!("kernel32.dll" "system" fn GetTimeZoneInformationForYear(wyear : u16, pdtzi : *const DYNAMIC_TIME_ZONE_INFORMATION, ptzi : *mut TIME_ZONE_INFORMATION) -> BOOL); +::windows_targets::link!("kernel32.dll" "system" fn SystemTimeToFileTime(lpsystemtime : *const SYSTEMTIME, lpfiletime : *mut FILETIME) -> BOOL); +::windows_targets::link!("kernel32.dll" "system" fn SystemTimeToTzSpecificLocalTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lpuniversaltime : *const SYSTEMTIME, lplocaltime : *mut SYSTEMTIME) -> BOOL); +::windows_targets::link!("kernel32.dll" "system" fn TzSpecificLocalTimeToSystemTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lplocaltime : *const SYSTEMTIME, lpuniversaltime : *mut SYSTEMTIME) -> BOOL); +pub type BOOL = i32; +pub type BOOLEAN = u8; +#[repr(C)] +pub struct DYNAMIC_TIME_ZONE_INFORMATION { + pub Bias: i32, + pub StandardName: [u16; 32], + pub StandardDate: SYSTEMTIME, + pub StandardBias: i32, + pub DaylightName: [u16; 32], + pub DaylightDate: SYSTEMTIME, + pub DaylightBias: i32, + pub TimeZoneKeyName: [u16; 128], + pub DynamicDaylightTimeDisabled: BOOLEAN, +} +impl ::core::marker::Copy for DYNAMIC_TIME_ZONE_INFORMATION {} +impl ::core::clone::Clone for DYNAMIC_TIME_ZONE_INFORMATION { + fn clone(&self) -> Self { + *self + } +} +#[repr(C)] +pub struct FILETIME { + pub dwLowDateTime: u32, + pub dwHighDateTime: u32, +} +impl ::core::marker::Copy for FILETIME {} +impl ::core::clone::Clone for FILETIME { + fn clone(&self) -> Self { + *self + } +} +#[repr(C)] +pub struct SYSTEMTIME { + pub wYear: u16, + pub wMonth: u16, + pub wDayOfWeek: u16, + pub wDay: u16, + pub wHour: u16, + pub wMinute: u16, + pub wSecond: u16, + pub wMilliseconds: u16, +} +impl ::core::marker::Copy for SYSTEMTIME {} +impl ::core::clone::Clone for SYSTEMTIME { + fn clone(&self) -> Self { + *self + } +} +#[repr(C)] +pub struct TIME_ZONE_INFORMATION { + pub Bias: i32, + pub StandardName: [u16; 32], + pub StandardDate: SYSTEMTIME, + pub StandardBias: i32, + pub DaylightName: [u16; 32], + pub DaylightDate: SYSTEMTIME, + pub DaylightBias: i32, +} +impl ::core::marker::Copy for TIME_ZONE_INFORMATION {} +impl ::core::clone::Clone for TIME_ZONE_INFORMATION { + fn clone(&self) -> Self { + *self + } +} diff --git a/src/offset/local/win_bindings.txt b/src/offset/local/win_bindings.txt new file mode 100644 index 0000000..7fb3e2f --- /dev/null +++ b/src/offset/local/win_bindings.txt @@ -0,0 +1,7 @@ +--out src/offset/local/win_bindings.rs +--config flatten sys +--filter + Windows.Win32.System.Time.GetTimeZoneInformationForYear + Windows.Win32.System.Time.SystemTimeToFileTime + Windows.Win32.System.Time.SystemTimeToTzSpecificLocalTime + Windows.Win32.System.Time.TzSpecificLocalTimeToSystemTime diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs new file mode 100644 index 0000000..cee09ec --- /dev/null +++ b/src/offset/local/windows.rs @@ -0,0 +1,262 @@ +// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::cmp::Ordering; +use std::convert::TryFrom; +use std::mem::MaybeUninit; +use std::ptr; + +use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION}; + +use crate::offset::local::{lookup_with_dst_transitions, Transition}; +use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday}; + +// We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates +// as Chrono. Also it really isn't that difficult to work out the correct offset from the provided +// DST rules. +// +// This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC +// falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is +// very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`. +pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> { + // Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be + // using the rules for the year of the corresponding local time. But this matches what + // `SystemTimeToTzSpecificLocalTime` is documented to do. + let tz_info = match TzInfo::for_year(utc.year()) { + Some(tz_info) => tz_info, + None => return LocalResult::None, + }; + let offset = match (tz_info.std_transition, tz_info.dst_transition) { + (Some(std_transition), Some(dst_transition)) => { + let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset); + let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset); + if dst_transition_utc < std_transition_utc { + match utc >= &dst_transition_utc && utc < &std_transition_utc { + true => tz_info.dst_offset, + false => tz_info.std_offset, + } + } else { + match utc >= &std_transition_utc && utc < &dst_transition_utc { + true => tz_info.std_offset, + false => tz_info.dst_offset, + } + } + } + (Some(std_transition), None) => { + let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset); + match utc < &std_transition_utc { + true => tz_info.dst_offset, + false => tz_info.std_offset, + } + } + (None, Some(dst_transition)) => { + let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset); + match utc < &dst_transition_utc { + true => tz_info.std_offset, + false => tz_info.dst_offset, + } + } + (None, None) => tz_info.std_offset, + }; + LocalResult::Single(offset) +} + +// We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle +// ambiguous cases (during a DST transition). Instead we get the timezone information for the +// current year and compute it ourselves, like we do on Unix. +pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> { + let tz_info = match TzInfo::for_year(local.year()) { + Some(tz_info) => tz_info, + None => return LocalResult::None, + }; + // Create a sorted slice of transitions and use `lookup_with_dst_transitions`. + match (tz_info.std_transition, tz_info.dst_transition) { + (Some(std_transition), Some(dst_transition)) => { + let std_transition = + Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset); + let dst_transition = + Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset); + let transitions = match std_transition.cmp(&dst_transition) { + Ordering::Less => [std_transition, dst_transition], + Ordering::Greater => [dst_transition, std_transition], + Ordering::Equal => { + // This doesn't make sense. Let's just return the standard offset. + return LocalResult::Single(tz_info.std_offset); + } + }; + lookup_with_dst_transitions(&transitions, *local) + } + (Some(std_transition), None) => { + let transitions = + [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)]; + lookup_with_dst_transitions(&transitions, *local) + } + (None, Some(dst_transition)) => { + let transitions = + [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)]; + lookup_with_dst_transitions(&transitions, *local) + } + (None, None) => return LocalResult::Single(tz_info.std_offset), + } +} + +// The basis for Windows timezone and DST support has been in place since Windows 2000. It does not +// allow for complex rules like the IANA timezone database: +// - A timezone has the same base offset the whole year. +// - There seem to be either zero or two DST transitions (but we support having just one). +// - As of Vista(?) only years from 2004 until a few years into the future are supported. +// - All other years get the base settings, which seem to be that of the current year. +// +// These details don't matter much, we just work with the offsets and transition dates Windows +// returns through `GetTimeZoneInformationForYear` for a particular year. +struct TzInfo { + // Offset from UTC during standard time. + std_offset: FixedOffset, + // Offset from UTC during daylight saving time. + dst_offset: FixedOffset, + // Transition from standard time to daylight saving time, given in local standard time. + std_transition: Option<NaiveDateTime>, + // Transition from daylight saving time to standard time, given in local daylight saving time. + dst_transition: Option<NaiveDateTime>, +} + +impl TzInfo { + fn for_year(year: i32) -> Option<TzInfo> { + // The API limits years to 1601..=30827. + // Working with timezones and daylight saving time this far into the past or future makes + // little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated + // for years beyond. + let ref_year = year.clamp(1601, 30827) as u16; + let tz_info = unsafe { + let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit(); + if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 { + return None; + } + tz_info.assume_init() + }; + Some(TzInfo { + std_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.StandardBias) * 60)?, + dst_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.DaylightBias) * 60)?, + std_transition: system_time_from_naive_date_time(tz_info.StandardDate, year), + dst_transition: system_time_from_naive_date_time(tz_info.DaylightDate, year), + }) + } +} + +fn system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option<NaiveDateTime> { + if st.wYear == 0 && st.wMonth == 0 { + return None; // No DST transitions for this year in this timezone. + } + let time = NaiveTime::from_hms_milli_opt( + st.wHour as u32, + st.wMinute as u32, + st.wSecond as u32, + st.wMilliseconds as u32, + )?; + // In Chrono's Weekday, Monday is 0 whereas in SYSTEMTIME Monday is 1 and Sunday is 0. + // Therefore we move back one day after converting the u16 value to a Weekday. + let day_of_week = Weekday::try_from(u8::try_from(st.wDayOfWeek).ok()?).ok()?.pred(); + if st.wYear != 0 { + return NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32) + .map(|d| d.and_time(time)); + } + let date = if let Some(date) = + NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, st.wDay as u8) + { + date + } else if st.wDay == 5 { + NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, 4)? + } else { + return None; + }; + Some(date.and_time(time)) +} + +#[cfg(test)] +mod tests { + use crate::offset::local::win_bindings::{ + SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, FILETIME, SYSTEMTIME, + }; + use crate::{DateTime, Duration, FixedOffset, Local, NaiveDate, NaiveDateTime}; + use crate::{Datelike, TimeZone, Timelike}; + use std::mem::MaybeUninit; + use std::ptr; + + #[test] + fn verify_against_tz_specific_local_time_to_system_time() { + // The implementation in Windows itself is the source of truth on how to work with the OS + // timezone information. This test compares for every hour over a period of 125 years our + // implementation to `TzSpecificLocalTimeToSystemTime`. + // + // This uses parts of a previous Windows `Local` implementation in chrono. + fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> { + let st = system_time_from_naive_date_time(dt); + let utc_time = local_to_utc_time(&st); + let utc_secs = system_time_as_unix_seconds(&utc_time); + let local_secs = system_time_as_unix_seconds(&st); + let offset = (local_secs - utc_secs) as i32; + let offset = FixedOffset::east_opt(offset).unwrap(); + DateTime::from_naive_utc_and_offset(*dt - offset, offset) + } + fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME { + SYSTEMTIME { + // Valid values: 1601-30827 + wYear: dt.year() as u16, + // Valid values:1-12 + wMonth: dt.month() as u16, + // Valid values: 0-6, starting Sunday. + // NOTE: enum returns 1-7, starting Monday, so we are + // off here, but this is not currently used in local. + wDayOfWeek: dt.weekday() as u16, + // Valid values: 1-31 + wDay: dt.day() as u16, + // Valid values: 0-23 + wHour: dt.hour() as u16, + // Valid values: 0-59 + wMinute: dt.minute() as u16, + // Valid values: 0-59 + wSecond: dt.second() as u16, + // Valid values: 0-999 + wMilliseconds: 0, + } + } + fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME { + let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit(); + unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) }; + // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can + // assume the value is initialized. + unsafe { sys_time.assume_init() } + } + const HECTONANOSECS_IN_SEC: i64 = 10_000_000; + const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC; + fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 { + let mut init = MaybeUninit::<FILETIME>::uninit(); + unsafe { + SystemTimeToFileTime(st, init.as_mut_ptr()); + } + // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is + // initalized. + let filetime = unsafe { init.assume_init() }; + let bit_shift = + ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64); + (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC + } + + let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap(); + + while date.year() < 2078 { + // Windows doesn't handle non-existing dates, it just treats it as valid. + if let Some(our_result) = Local.from_local_datetime(&date).earliest() { + assert_eq!(from_local_time(&date), our_result); + } + date += Duration::hours(1); + } + } +} diff --git a/src/offset/mod.rs b/src/offset/mod.rs index 0da6bfb..11928b4 100644 --- a/src/offset/mod.rs +++ b/src/offset/mod.rs @@ -20,10 +20,22 @@ use core::fmt; -use format::{parse, ParseResult, Parsed, StrftimeItems}; -use naive::{NaiveDate, NaiveDateTime, NaiveTime}; -use Weekday; -use {Date, DateTime}; +use crate::format::{parse, ParseResult, Parsed, StrftimeItems}; +use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime}; +use crate::Weekday; +#[allow(deprecated)] +use crate::{Date, DateTime}; + +pub(crate) mod fixed; +pub use self::fixed::FixedOffset; + +#[cfg(feature = "clock")] +pub(crate) mod local; +#[cfg(feature = "clock")] +pub use self::local::Local; + +pub(crate) mod utc; +pub use self::utc::Utc; /// The conversion result from the local time to the timezone-aware datetime types. #[derive(Clone, PartialEq, Debug, Copy, Eq, Hash)] @@ -40,6 +52,7 @@ pub enum LocalResult<T> { impl<T> LocalResult<T> { /// Returns `Some` only when the conversion result is unique, or `None` otherwise. + #[must_use] pub fn single(self) -> Option<T> { match self { LocalResult::Single(t) => Some(t), @@ -48,6 +61,7 @@ impl<T> LocalResult<T> { } /// Returns `Some` for the earliest possible conversion result, or `None` if none. + #[must_use] pub fn earliest(self) -> Option<T> { match self { LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => Some(t), @@ -56,6 +70,7 @@ impl<T> LocalResult<T> { } /// Returns `Some` for the latest possible conversion result, or `None` if none. + #[must_use] pub fn latest(self) -> Option<T> { match self { LocalResult::Single(t) | LocalResult::Ambiguous(_, t) => Some(t), @@ -64,6 +79,7 @@ impl<T> LocalResult<T> { } /// Maps a `LocalResult<T>` into `LocalResult<U>` with given function. + #[must_use] pub fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> LocalResult<U> { match self { LocalResult::None => LocalResult::None, @@ -73,12 +89,14 @@ impl<T> LocalResult<T> { } } +#[allow(deprecated)] impl<Tz: TimeZone> LocalResult<Date<Tz>> { /// Makes a new `DateTime` from the current date and given `NaiveTime`. /// The offset in the current date is preserved. /// /// Propagates any error. Ambiguous result would be discarded. #[inline] + #[must_use] pub fn and_time(self, time: NaiveTime) -> LocalResult<DateTime<Tz>> { match self { LocalResult::Single(d) => { @@ -93,6 +111,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> { /// /// Propagates any error. Ambiguous result would be discarded. #[inline] + #[must_use] pub fn and_hms_opt(self, hour: u32, min: u32, sec: u32) -> LocalResult<DateTime<Tz>> { match self { LocalResult::Single(d) => { @@ -108,6 +127,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> { /// /// Propagates any error. Ambiguous result would be discarded. #[inline] + #[must_use] pub fn and_hms_milli_opt( self, hour: u32, @@ -129,6 +149,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> { /// /// Propagates any error. Ambiguous result would be discarded. #[inline] + #[must_use] pub fn and_hms_micro_opt( self, hour: u32, @@ -150,6 +171,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> { /// /// Propagates any error. Ambiguous result would be discarded. #[inline] + #[must_use] pub fn and_hms_nano_opt( self, hour: u32, @@ -168,6 +190,8 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> { impl<T: fmt::Debug> LocalResult<T> { /// Returns the single unique conversion result, or panics accordingly. + #[must_use] + #[track_caller] pub fn unwrap(self) -> T { match self { LocalResult::None => panic!("No such local time"), @@ -187,14 +211,34 @@ pub trait Offset: Sized + Clone + fmt::Debug { /// The time zone. /// -/// The methods here are the primarily constructors for [`Date`](../struct.Date.html) and -/// [`DateTime`](../struct.DateTime.html) types. +/// The methods here are the primary constructors for the [`DateTime`] type. pub trait TimeZone: Sized + Clone { /// An associated offset type. /// This type is used to store the actual offset in date and time types. /// The original `TimeZone` value can be recovered via `TimeZone::from_offset`. type Offset: Offset; + /// Make a new `DateTime` from year, month, day, time components and current time zone. + /// + /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. + /// + /// Returns `LocalResult::None` on invalid input data. + fn with_ymd_and_hms( + &self, + year: i32, + month: u32, + day: u32, + hour: u32, + min: u32, + sec: u32, + ) -> LocalResult<DateTime<Self>> { + match NaiveDate::from_ymd_opt(year, month, day).and_then(|d| d.and_hms_opt(hour, min, sec)) + { + Some(dt) => self.from_local_datetime(&dt), + None => LocalResult::None, + } + } + /// Makes a new `Date` from year, month, day and the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// @@ -202,14 +246,8 @@ pub trait TimeZone: Sized + Clone { /// but it will propagate to the `DateTime` values constructed via this date. /// /// Panics on the out-of-range date, invalid month and/or day. - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{Utc, TimeZone}; - /// - /// assert_eq!(Utc.ymd(2015, 5, 15).to_string(), "2015-05-15UTC"); - /// ~~~~ + #[deprecated(since = "0.4.23", note = "use `with_ymd_and_hms()` instead")] + #[allow(deprecated)] fn ymd(&self, year: i32, month: u32, day: u32) -> Date<Self> { self.ymd_opt(year, month, day).unwrap() } @@ -221,15 +259,8 @@ pub trait TimeZone: Sized + Clone { /// but it will propagate to the `DateTime` values constructed via this date. /// /// Returns `None` on the out-of-range date, invalid month and/or day. - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{Utc, LocalResult, TimeZone}; - /// - /// assert_eq!(Utc.ymd_opt(2015, 5, 15).unwrap().to_string(), "2015-05-15UTC"); - /// assert_eq!(Utc.ymd_opt(2000, 0, 0), LocalResult::None); - /// ~~~~ + #[deprecated(since = "0.4.23", note = "use `with_ymd_and_hms()` instead")] + #[allow(deprecated)] fn ymd_opt(&self, year: i32, month: u32, day: u32) -> LocalResult<Date<Self>> { match NaiveDate::from_ymd_opt(year, month, day) { Some(d) => self.from_local_date(&d), @@ -244,14 +275,11 @@ pub trait TimeZone: Sized + Clone { /// but it will propagate to the `DateTime` values constructed via this date. /// /// Panics on the out-of-range date and/or invalid DOY. - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{Utc, TimeZone}; - /// - /// assert_eq!(Utc.yo(2015, 135).to_string(), "2015-05-15UTC"); - /// ~~~~ + #[deprecated( + since = "0.4.23", + note = "use `from_local_datetime()` with a `NaiveDateTime` instead" + )] + #[allow(deprecated)] fn yo(&self, year: i32, ordinal: u32) -> Date<Self> { self.yo_opt(year, ordinal).unwrap() } @@ -263,6 +291,11 @@ pub trait TimeZone: Sized + Clone { /// but it will propagate to the `DateTime` values constructed via this date. /// /// Returns `None` on the out-of-range date and/or invalid DOY. + #[deprecated( + since = "0.4.23", + note = "use `from_local_datetime()` with a `NaiveDateTime` instead" + )] + #[allow(deprecated)] fn yo_opt(&self, year: i32, ordinal: u32) -> LocalResult<Date<Self>> { match NaiveDate::from_yo_opt(year, ordinal) { Some(d) => self.from_local_date(&d), @@ -279,14 +312,11 @@ pub trait TimeZone: Sized + Clone { /// but it will propagate to the `DateTime` values constructed via this date. /// /// Panics on the out-of-range date and/or invalid week number. - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{Utc, Weekday, TimeZone}; - /// - /// assert_eq!(Utc.isoywd(2015, 20, Weekday::Fri).to_string(), "2015-05-15UTC"); - /// ~~~~ + #[deprecated( + since = "0.4.23", + note = "use `from_local_datetime()` with a `NaiveDateTime` instead" + )] + #[allow(deprecated)] fn isoywd(&self, year: i32, week: u32, weekday: Weekday) -> Date<Self> { self.isoywd_opt(year, week, weekday).unwrap() } @@ -300,6 +330,11 @@ pub trait TimeZone: Sized + Clone { /// but it will propagate to the `DateTime` values constructed via this date. /// /// Returns `None` on the out-of-range date and/or invalid week number. + #[deprecated( + since = "0.4.23", + note = "use `from_local_datetime()` with a `NaiveDateTime` instead" + )] + #[allow(deprecated)] fn isoywd_opt(&self, year: i32, week: u32, weekday: Weekday) -> LocalResult<Date<Self>> { match NaiveDate::from_isoywd_opt(year, week, weekday) { Some(d) => self.from_local_date(&d), @@ -311,16 +346,15 @@ pub trait TimeZone: Sized + Clone { /// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp") /// and the number of nanoseconds since the last whole non-leap second. /// - /// Panics on the out-of-range number of seconds and/or invalid nanosecond, - /// for a non-panicking version see [`timestamp_opt`](#method.timestamp_opt). - /// - /// # Example + /// The nanosecond part can exceed 1,000,000,000 in order to represent a + /// [leap second](crate::NaiveTime#leap-second-handling), but only when `secs % 60 == 59`. + /// (The true "UNIX timestamp" cannot represent a leap second unambiguously.) /// - /// ~~~~ - /// use chrono::{Utc, TimeZone}; + /// # Panics /// - /// assert_eq!(Utc.timestamp(1431648000, 0).to_string(), "2015-05-15 00:00:00 UTC"); - /// ~~~~ + /// Panics on the out-of-range number of seconds and/or invalid nanosecond, + /// for a non-panicking version see [`timestamp_opt`](#method.timestamp_opt). + #[deprecated(since = "0.4.23", note = "use `timestamp_opt()` instead")] fn timestamp(&self, secs: i64, nsecs: u32) -> DateTime<Self> { self.timestamp_opt(secs, nsecs).unwrap() } @@ -329,8 +363,22 @@ pub trait TimeZone: Sized + Clone { /// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp") /// and the number of nanoseconds since the last whole non-leap second. /// + /// The nanosecond part can exceed 1,000,000,000 in order to represent a + /// [leap second](crate::NaiveTime#leap-second-handling), but only when `secs % 60 == 59`. + /// (The true "UNIX timestamp" cannot represent a leap second unambiguously.) + /// + /// # Errors + /// /// Returns `LocalResult::None` on out-of-range number of seconds and/or /// invalid nanosecond, otherwise always returns `LocalResult::Single`. + /// + /// # Example + /// + /// ``` + /// use chrono::{Utc, TimeZone}; + /// + /// assert_eq!(Utc.timestamp_opt(1431648000, 0).unwrap().to_string(), "2015-05-15 00:00:00 UTC"); + /// ``` fn timestamp_opt(&self, secs: i64, nsecs: u32) -> LocalResult<DateTime<Self>> { match NaiveDateTime::from_timestamp_opt(secs, nsecs) { Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)), @@ -343,14 +391,7 @@ pub trait TimeZone: Sized + Clone { /// /// Panics on out-of-range number of milliseconds for a non-panicking /// version see [`timestamp_millis_opt`](#method.timestamp_millis_opt). - /// - /// # Example - /// - /// ~~~~ - /// use chrono::{Utc, TimeZone}; - /// - /// assert_eq!(Utc.timestamp_millis(1431648000).timestamp(), 1431648); - /// ~~~~ + #[deprecated(since = "0.4.23", note = "use `timestamp_millis_opt()` instead")] fn timestamp_millis(&self, millis: i64) -> DateTime<Self> { self.timestamp_millis_opt(millis).unwrap() } @@ -365,35 +406,32 @@ pub trait TimeZone: Sized + Clone { /// /// # Example /// - /// ~~~~ + /// ``` /// use chrono::{Utc, TimeZone, LocalResult}; /// match Utc.timestamp_millis_opt(1431648000) { /// LocalResult::Single(dt) => assert_eq!(dt.timestamp(), 1431648), /// _ => panic!("Incorrect timestamp_millis"), /// }; - /// ~~~~ + /// ``` fn timestamp_millis_opt(&self, millis: i64) -> LocalResult<DateTime<Self>> { - let (mut secs, mut millis) = (millis / 1000, millis % 1000); - if millis < 0 { - secs -= 1; - millis += 1000; + match NaiveDateTime::from_timestamp_millis(millis) { + Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)), + None => LocalResult::None, } - self.timestamp_opt(secs, millis as u32 * 1_000_000) } /// Makes a new `DateTime` from the number of non-leap nanoseconds /// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp"). /// - /// Unlike [`timestamp_millis`](#method.timestamp_millis), this never - /// panics. + /// Unlike [`timestamp_millis_opt`](#method.timestamp_millis_opt), this never fails. /// /// # Example /// - /// ~~~~ + /// ``` /// use chrono::{Utc, TimeZone}; /// /// assert_eq!(Utc.timestamp_nanos(1431648000000000).timestamp(), 1431648); - /// ~~~~ + /// ``` fn timestamp_nanos(&self, nanos: i64) -> DateTime<Self> { let (mut secs, mut nanos) = (nanos / 1_000_000_000, nanos % 1_000_000_000); if nanos < 0 { @@ -403,16 +441,42 @@ pub trait TimeZone: Sized + Clone { self.timestamp_opt(secs, nanos as u32).unwrap() } - /// Parses a string with the specified format string and - /// returns a `DateTime` with the current offset. - /// See the [`format::strftime` module](../format/strftime/index.html) - /// on the supported escape sequences. + /// Makes a new `DateTime` from the number of non-leap microseconds + /// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp"). + /// + /// # Example + /// + /// ``` + /// use chrono::{Utc, TimeZone}; + /// + /// assert_eq!(Utc.timestamp_micros(1431648000000).unwrap().timestamp(), 1431648); + /// ``` + fn timestamp_micros(&self, micros: i64) -> LocalResult<DateTime<Self>> { + match NaiveDateTime::from_timestamp_micros(micros) { + Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)), + None => LocalResult::None, + } + } + + /// Parses a string with the specified format string and returns a + /// `DateTime` with the current offset. + /// + /// See the [`crate::format::strftime`] module on the + /// supported escape sequences. + /// + /// If the to-be-parsed string includes an offset, it *must* match the + /// offset of the TimeZone, otherwise an error will be returned. /// - /// If the format does not include offsets, the current offset is assumed; - /// otherwise the input should have a matching UTC offset. + /// See also [`DateTime::parse_from_str`] which gives a [`DateTime`] with + /// parsed [`FixedOffset`]. /// - /// See also `DateTime::parse_from_str` which gives a local `DateTime` - /// with parsed `FixedOffset`. + /// See also [`NaiveDateTime::parse_from_str`] which gives a [`NaiveDateTime`] without + /// an offset, but can be converted to a [`DateTime`] with [`NaiveDateTime::and_utc`] or + /// [`NaiveDateTime::and_local_timezone`]. + #[deprecated( + since = "0.4.29", + note = "use `DateTime::parse_from_str` or `NaiveDateTime::parse_from_str` with `and_utc()` or `and_local_timezone()` instead" + )] fn datetime_from_str(&self, s: &str, fmt: &str) -> ParseResult<DateTime<Self>> { let mut parsed = Parsed::new(); parse(&mut parsed, s, StrftimeItems::new(fmt))?; @@ -429,6 +493,9 @@ pub trait TimeZone: Sized + Clone { fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset>; /// Converts the local `NaiveDate` to the timezone-aware `Date` if possible. + #[allow(clippy::wrong_self_convention)] + #[deprecated(since = "0.4.23", note = "use `from_local_datetime()` instead")] + #[allow(deprecated)] fn from_local_date(&self, local: &NaiveDate) -> LocalResult<Date<Self>> { self.offset_from_local_date(local).map(|offset| { // since FixedOffset is within +/- 1 day, the date is never affected @@ -437,9 +504,26 @@ pub trait TimeZone: Sized + Clone { } /// Converts the local `NaiveDateTime` to the timezone-aware `DateTime` if possible. + #[allow(clippy::wrong_self_convention)] fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Self>> { - self.offset_from_local_datetime(local) - .map(|offset| DateTime::from_utc(*local - offset.fix(), offset)) + // Return `LocalResult::None` when the offset pushes a value out of range, instead of + // panicking. + match self.offset_from_local_datetime(local) { + LocalResult::None => LocalResult::None, + LocalResult::Single(offset) => match local.checked_sub_offset(offset.fix()) { + Some(dt) => LocalResult::Single(DateTime::from_naive_utc_and_offset(dt, offset)), + None => LocalResult::None, + }, + LocalResult::Ambiguous(o1, o2) => { + match (local.checked_sub_offset(o1.fix()), local.checked_sub_offset(o2.fix())) { + (Some(d1), Some(d2)) => LocalResult::Ambiguous( + DateTime::from_naive_utc_and_offset(d1, o1), + DateTime::from_naive_utc_and_offset(d2, o2), + ), + _ => LocalResult::None, + } + } + } } /// Creates the offset for given UTC `NaiveDate`. This cannot fail. @@ -450,48 +534,68 @@ pub trait TimeZone: Sized + Clone { /// Converts the UTC `NaiveDate` to the local time. /// The UTC is continuous and thus this cannot fail (but can give the duplicate local time). + #[allow(clippy::wrong_self_convention)] + #[deprecated(since = "0.4.23", note = "use `from_utc_datetime()` instead")] + #[allow(deprecated)] fn from_utc_date(&self, utc: &NaiveDate) -> Date<Self> { Date::from_utc(*utc, self.offset_from_utc_date(utc)) } /// Converts the UTC `NaiveDateTime` to the local time. /// The UTC is continuous and thus this cannot fail (but can give the duplicate local time). + #[allow(clippy::wrong_self_convention)] fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Self> { - DateTime::from_utc(*utc, self.offset_from_utc_datetime(utc)) + DateTime::from_naive_utc_and_offset(*utc, self.offset_from_utc_datetime(utc)) } } -mod fixed; -#[cfg(feature = "clock")] -mod local; -mod utc; - -pub use self::fixed::FixedOffset; -#[cfg(feature = "clock")] -pub use self::local::Local; -pub use self::utc::Utc; - #[cfg(test)] mod tests { use super::*; #[test] + fn test_fixed_offset_min_max_dates() { + for offset_hour in -23..=23 { + dbg!(offset_hour); + let offset = FixedOffset::east_opt(offset_hour * 60 * 60).unwrap(); + + let local_max = offset.from_utc_datetime(&NaiveDateTime::MAX); + assert_eq!(local_max.naive_utc(), NaiveDateTime::MAX); + let local_min = offset.from_utc_datetime(&NaiveDateTime::MIN); + assert_eq!(local_min.naive_utc(), NaiveDateTime::MIN); + + let local_max = offset.from_local_datetime(&NaiveDateTime::MAX); + if offset_hour >= 0 { + assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX); + } else { + assert_eq!(local_max, LocalResult::None); + } + let local_min = offset.from_local_datetime(&NaiveDateTime::MIN); + if offset_hour <= 0 { + assert_eq!(local_min.unwrap().naive_local(), NaiveDateTime::MIN); + } else { + assert_eq!(local_min, LocalResult::None); + } + } + } + + #[test] fn test_negative_millis() { - let dt = Utc.timestamp_millis(-1000); + let dt = Utc.timestamp_millis_opt(-1000).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC"); - let dt = Utc.timestamp_millis(-7000); + let dt = Utc.timestamp_millis_opt(-7000).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:53 UTC"); - let dt = Utc.timestamp_millis(-7001); + let dt = Utc.timestamp_millis_opt(-7001).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:52.999 UTC"); - let dt = Utc.timestamp_millis(-7003); + let dt = Utc.timestamp_millis_opt(-7003).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:52.997 UTC"); - let dt = Utc.timestamp_millis(-999); + let dt = Utc.timestamp_millis_opt(-999).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:59.001 UTC"); - let dt = Utc.timestamp_millis(-1); + let dt = Utc.timestamp_millis_opt(-1).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999 UTC"); - let dt = Utc.timestamp_millis(-60000); + let dt = Utc.timestamp_millis_opt(-60000).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC"); - let dt = Utc.timestamp_millis(-3600000); + let dt = Utc.timestamp_millis_opt(-3600000).unwrap(); assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC"); for (millis, expected) in &[ @@ -528,4 +632,18 @@ mod tests { Utc.timestamp_nanos(i64::default()); Utc.timestamp_nanos(i64::min_value()); } + + #[test] + fn test_negative_micros() { + let dt = Utc.timestamp_micros(-1_000_000).unwrap(); + assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC"); + let dt = Utc.timestamp_micros(-999_999).unwrap(); + assert_eq!(dt.to_string(), "1969-12-31 23:59:59.000001 UTC"); + let dt = Utc.timestamp_micros(-1).unwrap(); + assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999999 UTC"); + let dt = Utc.timestamp_micros(-60_000_000).unwrap(); + assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC"); + let dt = Utc.timestamp_micros(-3_600_000_000).unwrap(); + assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC"); + } } diff --git a/src/offset/utc.rs b/src/offset/utc.rs index aec6667..29b832a 100644 --- a/src/offset/utc.rs +++ b/src/offset/utc.rs @@ -4,16 +4,24 @@ //! The UTC (Coordinated Universal Time) time zone. use core::fmt; - -use super::{FixedOffset, LocalResult, Offset, TimeZone}; -use naive::{NaiveDate, NaiveDateTime}; #[cfg(all( - feature = "clock", - not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")) + feature = "now", + not(all( + target_arch = "wasm32", + feature = "wasmbind", + not(any(target_os = "emscripten", target_os = "wasi")) + )) ))] use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(feature = "clock")] -use {Date, DateTime}; + +#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] +use rkyv::{Archive, Deserialize, Serialize}; + +use super::{FixedOffset, LocalResult, Offset, TimeZone}; +use crate::naive::{NaiveDate, NaiveDateTime}; +#[cfg(feature = "now")] +#[allow(deprecated)] +use crate::{Date, DateTime}; /// The UTC time zone. This is the most efficient time zone when you don't need the local time. /// It is also used as an offset (which is also a dummy type). @@ -24,35 +32,81 @@ use {Date, DateTime}; /// /// # Example /// -/// ~~~~ -/// use chrono::{DateTime, TimeZone, NaiveDateTime, Utc}; +/// ``` +/// use chrono::{TimeZone, NaiveDateTime, Utc}; /// -/// let dt = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc); +/// let dt = Utc.from_utc_datetime(&NaiveDateTime::from_timestamp_opt(61, 0).unwrap()); /// -/// assert_eq!(Utc.timestamp(61, 0), dt); -/// assert_eq!(Utc.ymd(1970, 1, 1).and_hms(0, 1, 1), dt); -/// ~~~~ -#[derive(Copy, Clone, PartialEq, Eq)] +/// assert_eq!(Utc.timestamp_opt(61, 0).unwrap(), dt); +/// assert_eq!(Utc.with_ymd_and_hms(1970, 1, 1, 0, 1, 1).unwrap(), dt); +/// ``` +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), + derive(Archive, Deserialize, Serialize), + archive(compare(PartialEq)), + archive_attr(derive(Clone, Copy, PartialEq, Eq, Debug, Hash)) +)] +#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] +#[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))] pub struct Utc; -#[cfg(feature = "clock")] +#[cfg(feature = "now")] impl Utc { /// Returns a `Date` which corresponds to the current date. + #[deprecated( + since = "0.4.23", + note = "use `Utc::now()` instead, potentially with `.date_naive()`" + )] + #[allow(deprecated)] + #[must_use] pub fn today() -> Date<Utc> { Utc::now().date() } - /// Returns a `DateTime` which corresponds to the current date. - #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] + /// Returns a `DateTime<Utc>` which corresponds to the current date and time in UTC. + /// + /// See also the similar [`Local::now()`] which returns `DateTime<Local>`, i.e. the local date + /// and time including offset from UTC. + /// + /// [`Local::now()`]: crate::Local::now + /// + /// # Example + /// + /// ``` + /// # #![allow(unused_variables)] + /// # use chrono::{FixedOffset, Utc}; + /// // Current time in UTC + /// let now_utc = Utc::now(); + /// + /// // Current date in UTC + /// let today_utc = now_utc.date_naive(); + /// + /// // Current time in some timezone (let's use +05:00) + /// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap(); + /// let now_with_offset = Utc::now().with_timezone(&offset); + /// ``` + #[cfg(not(all( + target_arch = "wasm32", + feature = "wasmbind", + not(any(target_os = "emscripten", target_os = "wasi")) + )))] + #[must_use] pub fn now() -> DateTime<Utc> { let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch"); - let naive = NaiveDateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos() as u32); - DateTime::from_utc(naive, Utc) + let naive = + NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()).unwrap(); + Utc.from_utc_datetime(&naive) } - /// Returns a `DateTime` which corresponds to the current date. - #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))] + /// Returns a `DateTime` which corresponds to the current date and time. + #[cfg(all( + target_arch = "wasm32", + feature = "wasmbind", + not(any(target_os = "emscripten", target_os = "wasi")) + ))] + #[must_use] pub fn now() -> DateTime<Utc> { let now = js_sys::Date::new_0(); DateTime::<Utc>::from(now) @@ -83,7 +137,7 @@ impl TimeZone for Utc { impl Offset for Utc { fn fix(&self) -> FixedOffset { - FixedOffset::east(0) + FixedOffset::east_opt(0).unwrap() } } |