From 50eb7e363df2da182839f5c0e3ff2168d10ee7d3 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Fri, 13 May 2022 21:37:31 +1000 Subject: [PATCH] handle localtime ambiguity --- src/offset/local/mod.rs | 6 +- src/offset/local/stub.rs | 8 +- src/offset/local/tz_info/rule.rs | 135 +++++++++++++++++++++++++++ src/offset/local/tz_info/timezone.rs | 128 +++++++++++++++++++++++-- src/offset/local/unix.rs | 54 ++++++++--- src/offset/local/windows.rs | 7 +- 6 files changed, 305 insertions(+), 33 deletions(-) diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs index 8468c21859..cae9278bb4 100644 --- a/src/offset/local/mod.rs +++ b/src/offset/local/mod.rs @@ -112,7 +112,7 @@ impl TimeZone for Local { #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { - LocalResult::Single(inner::naive_to_local(local, true)) + inner::naive_to_local(local, true) } fn from_utc_date(&self, utc: &NaiveDate) -> Date { @@ -129,7 +129,9 @@ impl TimeZone for Local { #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime { - inner::naive_to_local(utc, false) + // this is OK to unwrap as getting local time from a UTC + // timestamp is never ambiguous + inner::naive_to_local(utc, false).unwrap() } } diff --git a/src/offset/local/stub.rs b/src/offset/local/stub.rs index 9b8950f11e..d4d76c5a9b 100644 --- a/src/offset/local/stub.rs +++ b/src/offset/local/stub.rs @@ -11,7 +11,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use super::{FixedOffset, Local}; -use crate::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use crate::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; pub(super) fn now() -> DateTime { tm_to_datetime(Timespec::now().local()) @@ -19,7 +19,7 @@ pub(super) fn now() -> DateTime { /// Converts a local `NaiveDateTime` to the `time::Timespec`. #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))] -pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime { +pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult> { let tm = Tm { tm_sec: d.second() as i32, tm_min: d.minute() as i32, @@ -49,7 +49,7 @@ pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime assert_eq!(tm.tm_nsec, 0); tm.tm_nsec = d.nanosecond() as i32; - tm_to_datetime(tm) + LocalResult::Single(tm_to_datetime(tm)) } /// Converts a `time::Tm` struct into the timezone-aware `DateTime`. @@ -116,7 +116,7 @@ impl Timespec { /// day, and so on), also called a broken-down time value. // FIXME: use c_int instead of i32? #[repr(C)] -struct Tm { +pub(super) struct Tm { /// Seconds after the minute - [0, 60] tm_sec: i32, diff --git a/src/offset/local/tz_info/rule.rs b/src/offset/local/tz_info/rule.rs index 98feb7c3aa..7e9ed7e98d 100644 --- a/src/offset/local/tz_info/rule.rs +++ b/src/offset/local/tz_info/rule.rs @@ -78,6 +78,22 @@ impl TransitionRule { } } } + + /// 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, 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 for TransitionRule { @@ -211,6 +227,125 @@ impl AlternateTime { Ok(&self.std) } } + + fn find_local_time_type_from_local( + &self, + local_time: i64, + current_year: i32, + ) -> Result, 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 diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index 3266b05b73..6401fdec33 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -3,7 +3,7 @@ use std::fs::{self, File}; use std::io::{self, Read}; use std::path::{Path, PathBuf}; -use std::{fmt, str}; +use std::{cmp::Ordering, fmt, str}; use super::rule::{AlternateTime, TransitionRule}; use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY}; @@ -27,7 +27,11 @@ impl TimeZone { /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. /// pub(crate) fn local() -> Result { - Self::from_posix_tz("localtime") + if let Ok(tz) = std::env::var("TZ") { + Self::from_posix_tz(&tz) + } else { + 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). @@ -114,6 +118,15 @@ impl TimeZone { 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, 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 { @@ -188,6 +201,91 @@ impl<'a> TimeZoneRef<'a> { } } + pub(crate) fn find_local_time_type_from_local( + &self, + local_time: i64, + year: i32, + ) -> Result, 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 + if !self.transitions.is_empty() { + let mut prev = Some(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.unwrap().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.unwrap())); + } else if local_leap_time >= transition_end + && local_leap_time <= transition_start + { + if prev.unwrap().ut_offset < after_ltt.ut_offset { + return Ok(crate::LocalResult::Ambiguous(prev.unwrap(), after_ltt)); + } else { + return Ok(crate::LocalResult::Ambiguous(after_ltt, prev.unwrap())); + } + } + } + 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.unwrap())); + } else if local_leap_time == transition_end { + if prev.unwrap().ut_offset < after_ltt.ut_offset { + return Ok(crate::LocalResult::Ambiguous(prev.unwrap(), after_ltt)); + } else { + return Ok(crate::LocalResult::Ambiguous(after_ltt, prev.unwrap())); + } + } + } + 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.unwrap())); + } 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 = Some(after_ltt); + } + }; + + 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(self.local_time_types[0])) + } + } + /// Check time zone inputs fn validate(&self) -> Result<(), Error> { // Check local time types @@ -710,14 +808,24 @@ mod tests { fn test_time_zone_from_posix_tz() -> Result<(), Error> { #[cfg(unix)] { - let time_zone_local = TimeZone::local()?; - let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?; - let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?; - let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?; - - assert_eq!(time_zone_local, time_zone_local_1); - assert_eq!(time_zone_local, time_zone_local_2); - assert_eq!(time_zone_local, time_zone_local_3); + // 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()?; + let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?; + assert_eq!(time_zone_local, time_zone_local_1); + } else { + let time_zone_local = TimeZone::local()?; + let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?; + let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?; + let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?; + + assert_eq!(time_zone_local, time_zone_local_1); + assert_eq!(time_zone_local, time_zone_local_2); + assert_eq!(time_zone_local, time_zone_local_3); + } let time_zone_utc = TimeZone::from_posix_tz("UTC")?; assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 0cd5406a44..959b8e0267 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -12,23 +12,31 @@ use std::sync::Once; use super::tz_info::TimeZone; use super::{DateTime, FixedOffset, Local, NaiveDateTime}; -use crate::Utc; +use crate::{Datelike, LocalResult, Utc}; pub(super) fn now() -> DateTime { - let now = Utc::now(); - DateTime::from_utc(now.naive_utc(), offset(now.timestamp())) + let now = Utc::now().naive_utc(); + DateTime::from_utc(now, offset(now, false).unwrap()) } -pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime { - let offset = match local { - true => offset(d.timestamp()), - false => FixedOffset::east(0), - }; - - DateTime::from_utc(*d - offset, offset) +pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult> { + if local { + match offset(*d, true) { + LocalResult::None => LocalResult::None, + LocalResult::Ambiguous(early, late) => LocalResult::Ambiguous( + DateTime::from_utc(*d - early, early), + DateTime::from_utc(*d - late, late), + ), + LocalResult::Single(offset) => { + LocalResult::Single(DateTime::from_utc(*d - offset, offset)) + } + } + } else { + LocalResult::Single(DateTime::from_utc(*d, offset(*d, false).unwrap())) + } } -fn offset(unix: i64) -> FixedOffset { +fn offset(d: NaiveDateTime, local: bool) -> LocalResult { let info = unsafe { INIT.call_once(|| { INFO = Some(TimeZone::local().expect("unable to parse localtime info")); @@ -36,9 +44,27 @@ fn offset(unix: i64) -> FixedOffset { INFO.as_ref().unwrap() }; - FixedOffset::east( - info.find_local_time_type(unix).expect("unable to select local time type").offset(), - ) + if local { + // 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 anywa. + match info + .find_local_time_type_from_local(d.timestamp(), d.year()) + .expect("unable to select local time type") + { + LocalResult::None => LocalResult::None, + LocalResult::Ambiguous(early, late) => LocalResult::Ambiguous( + FixedOffset::east(early.offset()), + FixedOffset::east(late.offset()), + ), + LocalResult::Single(tt) => LocalResult::Single(FixedOffset::east(tt.offset())), + } + } else { + LocalResult::Single(FixedOffset::east( + info.find_local_time_type(d.timestamp()) + .expect("unable to select local time type") + .offset(), + )) + } } static mut INFO: Option = None; diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs index 29e75cf048..b4c0ee675f 100644 --- a/src/offset/local/windows.rs +++ b/src/offset/local/windows.rs @@ -17,14 +17,14 @@ use winapi::um::minwinbase::SYSTEMTIME; use winapi::um::timezoneapi::*; use super::{FixedOffset, Local}; -use crate::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use crate::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; pub(super) fn now() -> DateTime { tm_to_datetime(Timespec::now().local()) } /// Converts a local `NaiveDateTime` to the `time::Timespec`. -pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime { +pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult> { let tm = Tm { tm_sec: d.second() as i32, tm_min: d.minute() as i32, @@ -54,7 +54,8 @@ pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime assert_eq!(tm.tm_nsec, 0); tm.tm_nsec = d.nanosecond() as i32; - tm_to_datetime(tm) + // #TODO - there should be ambiguous cases, investigate? + LocalResult::Single(tm_to_datetime(tm)) } /// Converts a `time::Tm` struct into the timezone-aware `DateTime`.