diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs index deef38c6d3..358dae3515 100644 --- a/src/offset/local/mod.rs +++ b/src/offset/local/mod.rs @@ -291,7 +291,7 @@ mod tests { use super::Local; use crate::offset::local::TzInfo; use crate::offset::TimeZone; - use crate::{Datelike, Duration, FixedOffset, LocalResult, NaiveDate, Utc}; + use crate::{Datelike, Duration, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, Utc}; #[test] fn verify_correct_offsets() { @@ -342,6 +342,21 @@ mod tests { assert_eq!(Local.with_ymd_and_hms(2999, 12, 28, 0, 0, 0).unwrap().day(), 28); } + #[test] + fn test_local_min_max_dates() { + let local_max = Local.from_utc_datetime(&NaiveDateTime::MAX); + assert_eq!(local_max.naive_utc(), NaiveDateTime::MAX); + let local_min = Local.from_utc_datetime(&NaiveDateTime::MIN); + assert_eq!(local_min.naive_utc(), NaiveDateTime::MIN); + + if let Some(local_max) = Local.from_local_datetime(&NaiveDateTime::MAX).single() { + assert_eq!(local_max.naive_local(), NaiveDateTime::MAX); + } + if let Some(local_min) = Local.from_local_datetime(&NaiveDateTime::MIN).single() { + assert_eq!(local_min.naive_local(), NaiveDateTime::MIN); + } + } + #[test] fn test_leap_second() { // issue #123 diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 98a3eae3bc..3b955d1f69 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -12,6 +12,7 @@ use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::Sys use super::tz_info::TimeZone; use super::{DateTime, Local, NaiveDateTime}; +use crate::offset::local_time_min_offset; use crate::{Datelike, LocalResult}; pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult> { @@ -152,9 +153,10 @@ impl Cache { // 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 + let local_result_offset = self + .zone .find_local_offset_from_local(d.timestamp(), d.year()) - .expect("unable to select local time type") - .map(|offset| DateTime::from_utc(d - offset, offset)) + .expect("unable to select local time type"); + local_time_min_offset(local_result_offset, &d).unwrap_or(LocalResult::None) } } diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs index 76aad168b1..2338a43611 100644 --- a/src/offset/local/windows.rs +++ b/src/offset/local/windows.rs @@ -16,7 +16,10 @@ use winapi::um::minwinbase::SYSTEMTIME; use winapi::um::timezoneapi::{GetTimeZoneInformationForYear, TIME_ZONE_INFORMATION}; use super::{FixedOffset, Local, TzInfo}; -use crate::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday}; +use crate::offset::local_time_min_offset; +use crate::{ + DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday, +}; pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult> { let tz_info = match TzInfo::get_current_for_year(d.year()) { @@ -95,7 +98,7 @@ impl TzInfo { (Some(_), Some(_)) => self.lookup_with_dst_transitions(local_time), _ => LocalResult::Single(self.std_offset), }; - local_result_offset.map(|offset| DateTime::from_utc(*local_time - offset, offset)) + local_time_min_offset(local_result_offset, local_time).unwrap_or(LocalResult::None) } } diff --git a/src/offset/mod.rs b/src/offset/mod.rs index 5267ae4ce2..f7a955c222 100644 --- a/src/offset/mod.rs +++ b/src/offset/mod.rs @@ -24,7 +24,7 @@ use crate::format::{parse, ParseResult, Parsed, StrftimeItems}; use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime}; use crate::Weekday; #[allow(deprecated)] -use crate::{Date, DateTime}; +use crate::{Date, DateTime, Duration, Timelike}; mod fixed; pub use self::fixed::FixedOffset; @@ -472,8 +472,8 @@ 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> { - self.offset_from_local_datetime(local) - .map(|offset| DateTime::from_utc(*local - offset.fix(), offset)) + let local_result_offset = self.offset_from_local_datetime(local); + local_time_min_offset(local_result_offset, local).unwrap_or(LocalResult::None) } /// Creates the offset for given UTC `NaiveDate`. This cannot fail. @@ -499,10 +499,65 @@ pub trait TimeZone: Sized + Clone { } } +/// Helper function to map `LocalResult` to `LocalResult>`. +/// Returns `None` on out-of-range. +#[allow(dead_code)] +pub(crate) fn local_time_min_offset( + local_result: LocalResult, + local_time: &NaiveDateTime, +) -> Option>> { + // We temporarily extract and later add back the nanosecond part because + // `DateTime::checked_sub_signed()` normalizes leap_seconds, which we want to preserve. + fn try_apply_offset( + local_time: &NaiveDateTime, + offset: Tz::Offset, + ) -> Option> { + let nanos = local_time.nanosecond(); + let local_time = local_time.with_nanosecond(0).unwrap(); + let o = Duration::seconds(offset.fix().local_minus_utc() as i64); + let utc_time = local_time.checked_sub_signed(o)?.with_nanosecond(nanos).unwrap(); + Some(DateTime::from_utc(utc_time, offset)) + } + Some(match local_result { + LocalResult::None => LocalResult::None, + LocalResult::Single(offset) => LocalResult::Single(try_apply_offset(local_time, offset)?), + LocalResult::Ambiguous(o1, o2) => LocalResult::Ambiguous( + try_apply_offset(local_time, o1)?, + try_apply_offset(local_time, o2)?, + ), + }) +} + #[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_opt(-1000).unwrap();