Skip to content

Commit

Permalink
handle localtime ambiguity
Browse files Browse the repository at this point in the history
  • Loading branch information
esheppa authored and djc committed Jun 9, 2022
1 parent 4b0a782 commit b3ff13d
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 33 deletions.
6 changes: 4 additions & 2 deletions src/offset/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DateTime<Local>> {
LocalResult::Single(inner::naive_to_local(local, true))
inner::naive_to_local(local, true)
}

fn from_utc_date(&self, utc: &NaiveDate) -> Date<Local> {
Expand All @@ -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<Local> {
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()
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/offset/local/stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
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<Local> {
tm_to_datetime(Timespec::now().local())
}

/// 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<Local> {
pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
let tm = Tm {
tm_sec: d.second() as i32,
tm_min: d.minute() as i32,
Expand Down Expand Up @@ -49,7 +49,7 @@ pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime<Local>
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`.
Expand Down Expand Up @@ -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,

Expand Down
135 changes: 135 additions & 0 deletions src/offset/local/tz_info/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 {
Expand Down Expand Up @@ -211,6 +227,125 @@ impl AlternateTime {
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
Expand Down
128 changes: 118 additions & 10 deletions src/offset/local/tz_info/timezone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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, Error> {
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).
Expand Down Expand Up @@ -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<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 {
Expand Down Expand Up @@ -188,6 +201,91 @@ impl<'a> TimeZoneRef<'a> {
}
}

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
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
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit b3ff13d

Please sign in to comment.