mirror of
https://github.com/chronotope/chrono.git
synced 2025-10-01 15:03:14 +00:00
handle localtime ambiguity
This commit is contained in:
parent
2caab3493e
commit
50eb7e363d
@ -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> {
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Local> {
|
||||
tm_to_datetime(Timespec::now().local())
|
||||
@ -19,7 +19,7 @@ pub(super) fn now() -> DateTime<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,
|
||||
@ -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`.
|
||||
@ -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,
|
||||
|
||||
|
@ -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 {
|
||||
@ -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
|
||||
|
@ -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, 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).
|
||||
@ -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 {
|
||||
@ -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
|
||||
@ -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")?;
|
||||
// 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);
|
||||
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);
|
||||
|
@ -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<Local> {
|
||||
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<Local> {
|
||||
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<DateTime<Local>> {
|
||||
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<FixedOffset> {
|
||||
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<TimeZone> = None;
|
||||
|
@ -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<Local> {
|
||||
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<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,
|
||||
@ -54,7 +54,8 @@ 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)
|
||||
// #TODO - there should be ambiguous cases, investigate?
|
||||
LocalResult::Single(tm_to_datetime(tm))
|
||||
}
|
||||
|
||||
/// Converts a `time::Tm` struct into the timezone-aware `DateTime`.
|
||||
|
Loading…
x
Reference in New Issue
Block a user