diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 24a344d9ab..33ba426f32 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() @@ -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() } @@ -972,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/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() { diff --git a/src/naive/date.rs b/src/naive/date.rs index 379e667a56..04f74258e0 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) }); } @@ -1455,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).