From b2d04bf0d49032a3d35e7cfb21dd85f1bb490c10 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Wed, 3 May 2023 17:15:26 +0200 Subject: [PATCH] Fix panic in default impl of `TimeZone::from_local_datetime` --- src/naive/datetime/tests.rs | 24 ++++++++++++++++++-- src/offset/mod.rs | 44 +++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/naive/datetime/tests.rs b/src/naive/datetime/tests.rs index 4558bf7f42..067438ce71 100644 --- a/src/naive/datetime/tests.rs +++ b/src/naive/datetime/tests.rs @@ -1,7 +1,6 @@ use super::NaiveDateTime; use crate::duration::Duration as OldDuration; -use crate::NaiveDate; -use crate::{Datelike, FixedOffset, Utc}; +use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, Utc}; #[test] fn test_datetime_from_timestamp_millis() { @@ -490,3 +489,24 @@ fn test_checked_sub_offset() { assert_eq!(dt.checked_add_offset(positive_offset), Some(dt + positive_offset)); assert_eq!(dt.checked_sub_offset(positive_offset), Some(dt - positive_offset)); } + +#[test] +fn test_and_timezone_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 = NaiveDateTime::MAX.and_local_timezone(offset); + if offset_hour >= 0 { + assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX); + } else { + assert_eq!(local_max, LocalResult::None); + } + let local_min = NaiveDateTime::MIN.and_local_timezone(offset); + if offset_hour <= 0 { + assert_eq!(local_min.unwrap().naive_local(), NaiveDateTime::MIN); + } else { + assert_eq!(local_min, LocalResult::None); + } + } +} diff --git a/src/offset/mod.rs b/src/offset/mod.rs index c3154ee894..f524a06db0 100644 --- a/src/offset/mod.rs +++ b/src/offset/mod.rs @@ -500,8 +500,22 @@ 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_naive_utc_and_offset(*local - offset.fix(), offset)) + 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. @@ -531,6 +545,32 @@ pub trait TimeZone: Sized + Clone { 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();