From 5edced6de28cfa418703c0f6b2a52a78aa319f57 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 26 Sep 2023 12:36:55 +0200 Subject: [PATCH 1/4] checked_add_months --- src/datetime/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 24a344d9ab..f82d7dafe7 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -382,7 +382,7 @@ impl DateTime { /// daylight saving time transition. #[must_use] pub fn checked_add_months(self, rhs: Months) -> Option> { - self.naive_local() + self.overflowing_naive_local() .checked_add_months(rhs)? .and_local_timezone(Tz::from_offset(&self.offset)) .single() @@ -415,7 +415,7 @@ impl DateTime { /// daylight saving time transition. #[must_use] pub fn checked_sub_months(self, rhs: Months) -> Option> { - self.naive_local() + self.overflowing_naive_local() .checked_sub_months(rhs)? .and_local_timezone(Tz::from_offset(&self.offset)) .single() From 44d6eee89177ed0e28386e7fc6600c2082086e60 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 26 Sep 2023 12:36:59 +0200 Subject: [PATCH 2/4] Make `DateTime::checked_*_days` always return `Some` when valid --- src/datetime/mod.rs | 10 ++++++---- src/naive/date.rs | 7 +++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index f82d7dafe7..7d17048e41 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -431,8 +431,9 @@ impl DateTime { /// daylight saving time transition. #[must_use] pub fn checked_add_days(self, days: Days) -> Option { - self.naive_local() - .checked_add_days(days)? + self.overflowing_naive_local() + .checked_add_days(days) + .filter(|d| d.date() <= NaiveDate::AFTER_MAX)? .and_local_timezone(TimeZone::from_offset(&self.offset)) .single() } @@ -447,8 +448,9 @@ impl DateTime { /// daylight saving time transition. #[must_use] pub fn checked_sub_days(self, days: Days) -> Option { - self.naive_local() - .checked_sub_days(days)? + self.overflowing_naive_local() + .checked_sub_days(days) + .filter(|d| d.date() >= NaiveDate::BEFORE_MIN)? .and_local_timezone(TimeZone::from_offset(&self.offset)) .single() } diff --git a/src/naive/date.rs b/src/naive/date.rs index 379e667a56..f8a539605f 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -784,10 +784,13 @@ impl NaiveDate { /// Add a duration of `i32` days to the date. pub(crate) const fn add_days(self, days: i32) -> Option { - // fast path if the result is within the same year + // Fast path if the result is within the same year. + // Also `DateTime::checked_(add|sub)_days` relies on this path, because if the value remains + // within the year it doesn't do a check if the year is in range. + // That is useful when working with values near `DateTime::MIN` or `DateTime::MAX`. const ORDINAL_MASK: i32 = 0b1_1111_1111_0000; if let Some(ordinal) = ((self.ymdf & ORDINAL_MASK) >> 4).checked_add(days) { - if ordinal > 0 && ordinal <= 365 { + if ordinal > 0 && ordinal <= (365 + self.leap_year() as i32) { let year_and_flags = self.ymdf & !ORDINAL_MASK; return Some(NaiveDate { ymdf: year_and_flags | (ordinal << 4) }); } From b3f843e00809332b8e12dd2b9f452e375e201bb2 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 23 Sep 2023 15:49:55 +0200 Subject: [PATCH 3/4] Make `DateTime::with_year` always return `Some` when valid --- src/datetime/mod.rs | 4 +++- src/naive/date.rs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 7d17048e41..33ba426f32 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -974,7 +974,9 @@ impl Datelike for DateTime { /// - The local time at the resulting date does not exist or is ambiguous, for example during a /// daylight saving time transition. fn with_year(&self, year: i32) -> Option> { - map_local(self, |datetime| datetime.with_year(year)) + map_local(self, |dt| { + dt.date().overflowing_with_year(year).map(|d| NaiveDateTime::new(d, dt.time())) + }) } /// Makes a new `DateTime` with the month number (starting from 1) changed. diff --git a/src/naive/date.rs b/src/naive/date.rs index f8a539605f..04f74258e0 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -1458,6 +1458,19 @@ impl NaiveDate { self.of().weekday() } + // Similar to `Datelike::with_year()`, but allows creating a `NaiveDate` beyond `MIN` or `MAX`. + // Only used by `DateTime::with_year`. + pub(crate) fn overflowing_with_year(&self, year: i32) -> Option { + // we need to operate with `mdf` since we should keep the month and day number as is + let mdf = self.mdf(); + + // adjust the flags as needed + let flags = YearFlags::from_year(year); + let mdf = mdf.with_flags(flags); + + mdf.to_of().map(|of| NaiveDate { ymdf: (year << 13) | (of.inner() as DateImpl) }) + } + /// The minimum possible `NaiveDate` (January 1, 262144 BCE). pub const MIN: NaiveDate = NaiveDate { ymdf: (MIN_YEAR << 13) | (1 << 4) | 0o12 /*D*/ }; /// The maximum possible `NaiveDate` (December 31, 262142 CE). From ffee5079c9fc969a460e8937893a3f62d98634f6 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 26 Sep 2023 12:48:36 +0200 Subject: [PATCH 4/4] other tests --- src/datetime/tests.rs | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 9e41a51096..aad0bd72e2 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1437,6 +1437,65 @@ fn test_min_max_setters() { assert_eq!(beyond_max.with_nanosecond(beyond_max.nanosecond()), Some(beyond_max)); } +#[test] +fn test_min_max_complex() { + let offset_min = FixedOffset::west_opt(2 * 60 * 60).unwrap(); + let beyond_min = offset_min.from_utc_datetime(&NaiveDateTime::MIN); + let offset_max = FixedOffset::east_opt(2 * 60 * 60).unwrap(); + let beyond_max = offset_max.from_utc_datetime(&NaiveDateTime::MAX); + let max_time = NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(); + + assert_eq!(beyond_min.checked_add_days(Days::new(0)), Some(beyond_min)); + assert_eq!( + beyond_min.checked_add_days(Days::new(1)), + Some(offset_min.from_utc_datetime(&(NaiveDate::MIN + Days(1)).and_time(NaiveTime::MIN))) + ); + assert_eq!(beyond_min.checked_sub_days(Days::new(0)), Some(beyond_min)); + assert_eq!(beyond_min.checked_sub_days(Days::new(1)), None); + assert_eq!(beyond_min.checked_add_months(Months::new(0)), Some(beyond_min)); + assert_eq!( + beyond_min.checked_add_months(Months::new(1)), + Some(offset_min.from_utc_datetime(&(NaiveDate::MIN + Months(1)).and_time(NaiveTime::MIN))) + ); + assert_eq!(beyond_min.checked_sub_months(Months::new(0)), Some(beyond_min)); + assert_eq!(beyond_min.checked_sub_months(Months::new(1)), None); + assert_eq!(beyond_min.with_year(beyond_min.year()), Some(beyond_min)); + let res = NaiveDate::MIN.with_year(2021).unwrap().and_time(NaiveTime::MIN) + offset_min; + assert_eq!(beyond_min.with_year(2020), offset_min.from_local_datetime(&res).single()); + assert_eq!( + offset_min + .from_utc_datetime( + &NaiveDate::from_ymd_opt(2023, 1, 1).unwrap().and_time(NaiveTime::MIN) + ) + .with_year(NaiveDate::MIN.year() - 1), + Some(beyond_min) + ); + + assert_eq!(beyond_max.checked_add_days(Days::new(0)), Some(beyond_max)); + assert_eq!(beyond_max.checked_add_days(Days::new(1)), None); + assert_eq!(beyond_max.checked_sub_days(Days::new(0)), Some(beyond_max)); + assert_eq!( + beyond_max.checked_sub_days(Days::new(1)), + Some(offset_max.from_utc_datetime(&(NaiveDate::MAX - Days(1)).and_time(max_time))) + ); + assert_eq!(beyond_max.checked_add_months(Months::new(0)), Some(beyond_max)); + assert_eq!(beyond_max.checked_add_months(Months::new(1)), None); + assert_eq!(beyond_max.checked_sub_months(Months::new(0)), Some(beyond_max)); + assert_eq!( + beyond_max.checked_sub_months(Months::new(1)), + Some(offset_max.from_utc_datetime(&(NaiveDate::MAX - Months(1)).and_time(max_time))) + ); + assert_eq!(beyond_max.with_year(beyond_max.year()), Some(beyond_max)); + let res = NaiveDate::MAX.with_year(2019).unwrap().and_time(max_time) + offset_max; + assert_eq!(beyond_max.with_year(2020), offset_max.from_local_datetime(&res).single()); + assert_eq!( + offset_max + .from_utc_datetime(&NaiveDate::from_ymd_opt(2023, 12, 31).unwrap().and_time(max_time)) + .with_year(NaiveDate::MAX.year() + 1), + Some(beyond_max) + ); +} + #[test] #[should_panic] fn test_local_beyond_min_datetime() {