From adc3c48e48c94f5cdd56e69795b008911eea957c Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Thu, 18 May 2023 09:13:03 +0200 Subject: [PATCH 1/6] Adjust MIN_YEAR and MAX_YEAR --- src/naive/date.rs | 27 +++++++++++++++------------ src/naive/datetime/mod.rs | 10 +++++----- src/naive/internals.rs | 11 +++++++++-- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/naive/date.rs b/src/naive/date.rs index 9677b0c439..6a99a2438c 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -1457,10 +1457,10 @@ impl NaiveDate { self.of().weekday() } - /// The minimum possible `NaiveDate` (January 1, 262145 BCE). - pub const MIN: NaiveDate = NaiveDate { ymdf: (MIN_YEAR << 13) | (1 << 4) | 0o07 /*FE*/ }; - /// The maximum possible `NaiveDate` (December 31, 262143 CE). - pub const MAX: NaiveDate = NaiveDate { ymdf: (MAX_YEAR << 13) | (365 << 4) | 0o17 /*F*/ }; + /// 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). + pub const MAX: NaiveDate = NaiveDate { ymdf: (MAX_YEAR << 13) | (365 << 4) | 0o16 /*G*/ }; } impl Datelike for NaiveDate { @@ -2277,8 +2277,8 @@ where to_string(&NaiveDate::from_ymd_opt(-1, 12, 31).unwrap()).ok(), Some(r#""-0001-12-31""#.into()) ); - assert_eq!(to_string(&NaiveDate::MIN).ok(), Some(r#""-262144-01-01""#.into())); - assert_eq!(to_string(&NaiveDate::MAX).ok(), Some(r#""+262143-12-31""#.into())); + assert_eq!(to_string(&NaiveDate::MIN).ok(), Some(r#""-262143-01-01""#.into())); + assert_eq!(to_string(&NaiveDate::MAX).ok(), Some(r#""+262142-12-31""#.into())); } #[cfg(all(test, any(feature = "rustc-serialize", feature = "serde")))] @@ -2301,8 +2301,8 @@ where from_str(r#""-0001-12-31""#).ok(), Some(NaiveDate::from_ymd_opt(-1, 12, 31).unwrap()) ); - assert_eq!(from_str(r#""-262144-01-01""#).ok(), Some(NaiveDate::MIN)); - assert_eq!(from_str(r#""+262143-12-31""#).ok(), Some(NaiveDate::MAX)); + assert_eq!(from_str(r#""-262143-01-01""#).ok(), Some(NaiveDate::MIN)); + assert_eq!(from_str(r#""+262142-12-31""#).ok(), Some(NaiveDate::MAX)); // bad formats assert!(from_str(r#""""#).is_err()); @@ -3155,9 +3155,12 @@ mod tests { #[test] fn test_day_iterator_limit() { - assert_eq!(NaiveDate::from_ymd_opt(262143, 12, 29).unwrap().iter_days().take(4).count(), 2); assert_eq!( - NaiveDate::from_ymd_opt(-262144, 1, 3).unwrap().iter_days().rev().take(4).count(), + NaiveDate::from_ymd_opt(MAX_YEAR, 12, 29).unwrap().iter_days().take(4).count(), + 2 + ); + assert_eq!( + NaiveDate::from_ymd_opt(MIN_YEAR, 1, 3).unwrap().iter_days().rev().take(4).count(), 2 ); } @@ -3165,11 +3168,11 @@ mod tests { #[test] fn test_week_iterator_limit() { assert_eq!( - NaiveDate::from_ymd_opt(262143, 12, 12).unwrap().iter_weeks().take(4).count(), + NaiveDate::from_ymd_opt(MAX_YEAR, 12, 12).unwrap().iter_weeks().take(4).count(), 2 ); assert_eq!( - NaiveDate::from_ymd_opt(-262144, 1, 15).unwrap().iter_weeks().rev().take(4).count(), + NaiveDate::from_ymd_opt(MIN_YEAR, 1, 15).unwrap().iter_weeks().rev().take(4).count(), 2 ); } diff --git a/src/naive/datetime/mod.rs b/src/naive/datetime/mod.rs index af399d8a67..535be49f89 100644 --- a/src/naive/datetime/mod.rs +++ b/src/naive/datetime/mod.rs @@ -2123,11 +2123,11 @@ where ); assert_eq!( to_string(&NaiveDate::MIN.and_hms_opt(0, 0, 0).unwrap()).ok(), - Some(r#""-262144-01-01T00:00:00""#.into()) + Some(r#""-262143-01-01T00:00:00""#.into()) ); assert_eq!( to_string(&NaiveDate::MAX.and_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap()).ok(), - Some(r#""+262143-12-31T23:59:60.999999999""#.into()) + Some(r#""+262142-12-31T23:59:60.999999999""#.into()) ); } @@ -2166,15 +2166,15 @@ where Some(NaiveDate::from_ymd_opt(-1, 12, 31).unwrap().and_hms_nano_opt(23, 59, 59, 7).unwrap()) ); assert_eq!( - from_str(r#""-262144-01-01T00:00:00""#).ok(), + from_str(r#""-262143-01-01T00:00:00""#).ok(), Some(NaiveDate::MIN.and_hms_opt(0, 0, 0).unwrap()) ); assert_eq!( - from_str(r#""+262143-12-31T23:59:60.999999999""#).ok(), + from_str(r#""+262142-12-31T23:59:60.999999999""#).ok(), Some(NaiveDate::MAX.and_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap()) ); assert_eq!( - from_str(r#""+262143-12-31T23:59:60.9999999999997""#).ok(), // excess digits are ignored + from_str(r#""+262142-12-31T23:59:60.9999999999997""#).ok(), // excess digits are ignored Some(NaiveDate::MAX.and_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap()) ); diff --git a/src/naive/internals.rs b/src/naive/internals.rs index 910419f387..c6d7536a0c 100644 --- a/src/naive/internals.rs +++ b/src/naive/internals.rs @@ -21,8 +21,15 @@ use core::fmt; /// The internal date representation: `year << 13 | Of` pub(super) type DateImpl = i32; -pub(super) const MAX_YEAR: DateImpl = i32::MAX >> 13; -pub(super) const MIN_YEAR: DateImpl = i32::MIN >> 13; +/// MAX_YEAR is one year less than the type is capable of representing. Internally we may sometimes +/// use the headroom, notably to handle cases where the offset of a `DateTime` constructed with +/// `NaiveDate::MAX` pushes it beyond the valid, representable range. +pub(super) const MAX_YEAR: DateImpl = (i32::MAX >> 13) - 1; + +/// MIN_YEAR is one year more than the type is capable of representing. Internally we may sometimes +/// use the headroom, notably to handle cases where the offset of a `DateTime` constructed with +/// `NaiveDate::MIN` pushes it beyond the valid, representable range. +pub(super) const MIN_YEAR: DateImpl = (i32::MIN >> 13) + 1; /// The year flags (aka the dominical letter). /// From 982496573de597d84fdac5207a8f89acb39d3b4a Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 23 Sep 2023 14:24:48 +0200 Subject: [PATCH 2/6] Add `NaiveDate::{BEFORE_MIN, AFTER_MAX}` --- src/naive/date.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/naive/date.rs b/src/naive/date.rs index 6a99a2438c..1c57b89d62 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -1461,6 +1461,13 @@ impl NaiveDate { pub const MIN: NaiveDate = NaiveDate { ymdf: (MIN_YEAR << 13) | (1 << 4) | 0o12 /*D*/ }; /// The maximum possible `NaiveDate` (December 31, 262142 CE). pub const MAX: NaiveDate = NaiveDate { ymdf: (MAX_YEAR << 13) | (365 << 4) | 0o16 /*G*/ }; + + /// One day before the minimum possible `NaiveDate` (December 31, 262145 BCE). + pub(crate) const BEFORE_MIN: NaiveDate = + NaiveDate { ymdf: ((MIN_YEAR - 1) << 13) | (366 << 4) | 0o07 /*FE*/ }; + /// One day after the maximum possible `NaiveDate` (January 1, 262143 CE). + pub(crate) const AFTER_MAX: NaiveDate = + NaiveDate { ymdf: ((MAX_YEAR + 1) << 13) | (1 << 4) | 0o17 /*F*/ }; } impl Datelike for NaiveDate { @@ -2444,6 +2451,7 @@ mod serde { mod tests { use super::{Days, Months, NaiveDate, MAX_YEAR, MIN_YEAR}; use crate::duration::Duration; + use crate::naive::internals::YearFlags; use crate::{Datelike, Weekday}; // as it is hard to verify year flags in `NaiveDate::MIN` and `NaiveDate::MAX`, @@ -2473,6 +2481,14 @@ mod tests { "The entire `NaiveDate` range somehow exceeds 2^{} seconds", MAX_BITS ); + + const BEFORE_MIN: NaiveDate = NaiveDate::BEFORE_MIN; + assert_eq!(BEFORE_MIN.of().flags(), YearFlags::from_year(BEFORE_MIN.year())); + assert_eq!((BEFORE_MIN.month(), BEFORE_MIN.day()), (12, 31)); + + const AFTER_MAX: NaiveDate = NaiveDate::AFTER_MAX; + assert_eq!(AFTER_MAX.of().flags(), YearFlags::from_year(AFTER_MAX.year())); + assert_eq!((AFTER_MAX.month(), AFTER_MAX.day()), (1, 1)); } #[test] From 9ac2119c642c1f9c839968d2b7972a12f4a7307e Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 26 Sep 2023 12:20:35 +0200 Subject: [PATCH 3/6] Tests and nicer panic message for `DateTime::naive_local` --- src/datetime/mod.rs | 4 +++- src/datetime/tests.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 3efd303817..1d3d809320 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -505,7 +505,9 @@ impl DateTime { #[inline] #[must_use] pub fn naive_local(&self) -> NaiveDateTime { - self.datetime + self.offset.fix() + self.datetime + .checked_add_offset(self.offset.fix()) + .expect("Local time out of range for `NaiveDateTime`") } /// Retrieve the elapsed years from now to the given [`DateTime`]. diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 3872e14b36..6230667b24 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1331,6 +1331,20 @@ fn test_datetime_sub_assign() { assert_eq!(datetime_sub, datetime - OldDuration::minutes(90)); } +#[test] +#[should_panic] +fn test_local_beyond_min_datetime() { + let min = FixedOffset::west_opt(2 * 60 * 60).unwrap().from_utc_datetime(&NaiveDateTime::MIN); + let _ = min.naive_local(); +} + +#[test] +#[should_panic] +fn test_local_beyond_max_datetime() { + let max = FixedOffset::east_opt(2 * 60 * 60).unwrap().from_utc_datetime(&NaiveDateTime::MAX); + let _ = max.naive_local(); +} + #[test] #[cfg(feature = "clock")] fn test_datetime_sub_assign_local() { From 73ac05157392ba6078ed20aa1213548755208cda Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Thu, 4 May 2023 17:03:30 +0200 Subject: [PATCH 4/6] Add `overflowing_naive_local` and `NaiveDateTime::overflowing_add_offset` --- src/datetime/mod.rs | 11 +++++++++++ src/naive/datetime/mod.rs | 16 ++++++++++++++++ src/naive/datetime/tests.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 1d3d809320..ec31028870 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -510,6 +510,17 @@ impl DateTime { .expect("Local time out of range for `NaiveDateTime`") } + /// Returns the naive local datetime. + /// + /// This makes use of the buffer space outside of the representable range of values of + /// `NaiveDateTime`. The result can be used as intermediate value, but should never be exposed + /// outside chrono. + #[inline] + #[must_use] + pub(crate) fn overflowing_naive_local(&self) -> NaiveDateTime { + self.datetime.overflowing_add_offset(self.offset.fix()) + } + /// Retrieve the elapsed years from now to the given [`DateTime`]. /// /// # Errors diff --git a/src/naive/datetime/mod.rs b/src/naive/datetime/mod.rs index 535be49f89..64f52d8d19 100644 --- a/src/naive/datetime/mod.rs +++ b/src/naive/datetime/mod.rs @@ -738,6 +738,22 @@ impl NaiveDateTime { Some(NaiveDateTime { date, time }) } + /// Adds given `FixedOffset` to the current datetime. + /// The resulting value may be outside the valid range of [`NaiveDateTime`]. + /// + /// This can be useful for intermediate values, but the resulting out-of-range `NaiveDate` + /// should not be exposed to library users. + #[must_use] + pub(crate) fn overflowing_add_offset(self, rhs: FixedOffset) -> NaiveDateTime { + let (time, days) = self.time.overflowing_add_offset(rhs); + let date = match days { + -1 => self.date.pred_opt().unwrap_or(NaiveDate::BEFORE_MIN), + 1 => self.date.succ_opt().unwrap_or(NaiveDate::AFTER_MAX), + _ => self.date, + }; + NaiveDateTime { date, time } + } + /// Subtracts given `Duration` from the current date and time. /// /// As a part of Chrono's [leap second handling](./struct.NaiveTime.html#leap-second-handling), diff --git a/src/naive/datetime/tests.rs b/src/naive/datetime/tests.rs index 067438ce71..97604db74f 100644 --- a/src/naive/datetime/tests.rs +++ b/src/naive/datetime/tests.rs @@ -490,6 +490,32 @@ fn test_checked_sub_offset() { assert_eq!(dt.checked_sub_offset(positive_offset), Some(dt - positive_offset)); } +#[test] +fn test_overflowing_add_offset() { + let ymdhmsm = |y, m, d, h, mn, s, mi| { + NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_milli_opt(h, mn, s, mi).unwrap() + }; + let positive_offset = FixedOffset::east_opt(2 * 60 * 60).unwrap(); + // regular date + let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0); + assert_eq!(dt.overflowing_add_offset(positive_offset), ymdhmsm(2023, 5, 5, 22, 10, 0, 0)); + // leap second is preserved + let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000); + assert_eq!(dt.overflowing_add_offset(positive_offset), ymdhmsm(2023, 7, 1, 1, 59, 59, 1_000)); + // out of range + assert!(NaiveDateTime::MAX.overflowing_add_offset(positive_offset) > NaiveDateTime::MAX); + + let negative_offset = FixedOffset::west_opt(2 * 60 * 60).unwrap(); + // regular date + let dt = ymdhmsm(2023, 5, 5, 20, 10, 0, 0); + assert_eq!(dt.overflowing_add_offset(negative_offset), ymdhmsm(2023, 5, 5, 18, 10, 0, 0)); + // leap second is preserved + let dt = ymdhmsm(2023, 6, 30, 23, 59, 59, 1_000); + assert_eq!(dt.overflowing_add_offset(negative_offset), ymdhmsm(2023, 6, 30, 21, 59, 59, 1_000)); + // out of range + assert!(NaiveDateTime::MIN.overflowing_add_offset(negative_offset) < NaiveDateTime::MIN); +} + #[test] fn test_and_timezone_min_max_dates() { for offset_hour in -23..=23 { From 4da95fad603174f0b9e592b9ba99765bb56a4217 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Fri, 19 May 2023 17:34:29 +0200 Subject: [PATCH 5/6] Use `overflowing_naive_local` in methods that don't return `DateTime` --- src/datetime/mod.rs | 40 ++++++++++++++--------------- src/datetime/tests.rs | 60 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index ec31028870..e85ef064c6 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -22,7 +22,7 @@ use crate::format::{ StrftimeItems, TOO_LONG, }; #[cfg(feature = "alloc")] -use crate::format::{write_rfc3339, DelayedFormat}; +use crate::format::{write_rfc2822, write_rfc3339, DelayedFormat}; use crate::naive::{Days, IsoWeek, NaiveDate, NaiveDateTime, NaiveTime}; #[cfg(feature = "clock")] use crate::offset::Local; @@ -553,7 +553,7 @@ impl DateTime { #[must_use] pub fn to_rfc2822(&self) -> String { let mut result = String::with_capacity(32); - crate::format::write_rfc2822(&mut result, self.naive_local(), self.offset.fix()) + write_rfc2822(&mut result, self.overflowing_naive_local(), self.offset.fix()) .expect("writing rfc2822 datetime to string should never fail"); result } @@ -564,7 +564,7 @@ impl DateTime { pub fn to_rfc3339(&self) -> String { // For some reason a string with a capacity less than 32 is ca 20% slower when benchmarking. let mut result = String::with_capacity(32); - let naive = self.naive_local(); + let naive = self.overflowing_naive_local(); let offset = self.offset.fix(); write_rfc3339(&mut result, naive, offset, SecondsFormat::AutoSi, false) .expect("writing rfc3339 datetime to string should never fail"); @@ -880,7 +880,7 @@ where I: Iterator + Clone, B: Borrow>, { - let local = self.naive_local(); + let local = self.overflowing_naive_local(); DelayedFormat::new_with_offset(Some(local.date()), Some(local.time()), &self.offset, items) } @@ -916,7 +916,7 @@ where I: Iterator + Clone, B: Borrow>, { - let local = self.naive_local(); + let local = self.overflowing_naive_local(); DelayedFormat::new_with_offset_and_locale( Some(local.date()), Some(local.time()), @@ -946,39 +946,39 @@ where impl Datelike for DateTime { #[inline] fn year(&self) -> i32 { - self.naive_local().year() + self.overflowing_naive_local().year() } #[inline] fn month(&self) -> u32 { - self.naive_local().month() + self.overflowing_naive_local().month() } #[inline] fn month0(&self) -> u32 { - self.naive_local().month0() + self.overflowing_naive_local().month0() } #[inline] fn day(&self) -> u32 { - self.naive_local().day() + self.overflowing_naive_local().day() } #[inline] fn day0(&self) -> u32 { - self.naive_local().day0() + self.overflowing_naive_local().day0() } #[inline] fn ordinal(&self) -> u32 { - self.naive_local().ordinal() + self.overflowing_naive_local().ordinal() } #[inline] fn ordinal0(&self) -> u32 { - self.naive_local().ordinal0() + self.overflowing_naive_local().ordinal0() } #[inline] fn weekday(&self) -> Weekday { - self.naive_local().weekday() + self.overflowing_naive_local().weekday() } #[inline] fn iso_week(&self) -> IsoWeek { - self.naive_local().iso_week() + self.overflowing_naive_local().iso_week() } #[inline] @@ -1097,19 +1097,19 @@ impl Datelike for DateTime { impl Timelike for DateTime { #[inline] fn hour(&self) -> u32 { - self.naive_local().hour() + self.overflowing_naive_local().hour() } #[inline] fn minute(&self) -> u32 { - self.naive_local().minute() + self.overflowing_naive_local().minute() } #[inline] fn second(&self) -> u32 { - self.naive_local().second() + self.overflowing_naive_local().second() } #[inline] fn nanosecond(&self) -> u32 { - self.naive_local().nanosecond() + self.overflowing_naive_local().nanosecond() } /// Makes a new `DateTime` with the hour number changed. @@ -1513,7 +1513,7 @@ impl Sub for DateTime { impl fmt::Debug for DateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.naive_local().fmt(f)?; + self.overflowing_naive_local().fmt(f)?; self.offset.fmt(f) } } @@ -1523,7 +1523,7 @@ where Tz::Offset: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.naive_local().fmt(f)?; + self.overflowing_naive_local().fmt(f)?; f.write_char(' ')?; self.offset.fmt(f) } diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 6230667b24..2c527b54cc 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -4,7 +4,7 @@ use crate::naive::{NaiveDate, NaiveTime}; use crate::offset::{FixedOffset, TimeZone, Utc}; #[cfg(feature = "clock")] use crate::offset::{Local, Offset}; -use crate::{Datelike, Days, LocalResult, Months, NaiveDateTime, Timelike}; +use crate::{Datelike, Days, LocalResult, Months, NaiveDateTime, Timelike, Weekday}; #[derive(Clone)] struct DstTester; @@ -1331,6 +1331,64 @@ fn test_datetime_sub_assign() { assert_eq!(datetime_sub, datetime - OldDuration::minutes(90)); } +#[test] +fn test_min_max_getters() { + 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); + + assert_eq!(format!("{:?}", beyond_min), "-262144-12-31T22:00:00-02:00"); + // RFC 2822 doesn't support years with more than 4 digits. + // assert_eq!(beyond_min.to_rfc2822(), ""); + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!(beyond_min.to_rfc3339(), "-262144-12-31T22:00:00-02:00"); + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!( + beyond_min.format("%Y-%m-%dT%H:%M:%S%:z").to_string(), + "-262144-12-31T22:00:00-02:00" + ); + assert_eq!(beyond_min.year(), -262144); + assert_eq!(beyond_min.month(), 12); + assert_eq!(beyond_min.month0(), 11); + assert_eq!(beyond_min.day(), 31); + assert_eq!(beyond_min.day0(), 30); + assert_eq!(beyond_min.ordinal(), 366); + assert_eq!(beyond_min.ordinal0(), 365); + assert_eq!(beyond_min.weekday(), Weekday::Wed); + assert_eq!(beyond_min.iso_week().year(), -262143); + assert_eq!(beyond_min.iso_week().week(), 1); + assert_eq!(beyond_min.hour(), 22); + assert_eq!(beyond_min.minute(), 0); + assert_eq!(beyond_min.second(), 0); + assert_eq!(beyond_min.nanosecond(), 0); + + assert_eq!(format!("{:?}", beyond_max), "+262143-01-01T01:59:59.999999999+02:00"); + // RFC 2822 doesn't support years with more than 4 digits. + // assert_eq!(beyond_max.to_rfc2822(), ""); + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!(beyond_max.to_rfc3339(), "+262143-01-01T01:59:59.999999999+02:00"); + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!( + beyond_max.format("%Y-%m-%dT%H:%M:%S%.9f%:z").to_string(), + "+262143-01-01T01:59:59.999999999+02:00" + ); + assert_eq!(beyond_max.year(), 262143); + assert_eq!(beyond_max.month(), 1); + assert_eq!(beyond_max.month0(), 0); + assert_eq!(beyond_max.day(), 1); + assert_eq!(beyond_max.day0(), 0); + assert_eq!(beyond_max.ordinal(), 1); + assert_eq!(beyond_max.ordinal0(), 0); + assert_eq!(beyond_max.weekday(), Weekday::Tue); + assert_eq!(beyond_max.iso_week().year(), 262143); + assert_eq!(beyond_max.iso_week().week(), 1); + assert_eq!(beyond_max.hour(), 1); + assert_eq!(beyond_max.minute(), 59); + assert_eq!(beyond_max.second(), 59); + assert_eq!(beyond_max.nanosecond(), 999_999_999); +} + #[test] #[should_panic] fn test_local_beyond_min_datetime() { From e002ce94b8374d26f1eb07f4c2b7a40ba30112da Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Fri, 19 May 2023 18:10:01 +0200 Subject: [PATCH 6/6] Use `overflowing_naive_local` in `map_local` This fixes out-of-range panics in all the `with_*` methods that use `map_local`. --- src/datetime/mod.rs | 4 +++- src/datetime/tests.rs | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index e85ef064c6..d599346459 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -742,7 +742,9 @@ fn map_local(dt: &DateTime, mut f: F) -> Option Option, { - f(dt.naive_local()).and_then(|datetime| dt.timezone().from_local_datetime(&datetime).single()) + f(dt.overflowing_naive_local()) + .and_then(|datetime| dt.timezone().from_local_datetime(&datetime).single()) + .filter(|dt| dt >= &DateTime::::MIN_UTC && dt <= &DateTime::::MAX_UTC) } impl DateTime { diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 2c527b54cc..1b351e22ff 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1389,6 +1389,54 @@ fn test_min_max_getters() { assert_eq!(beyond_max.nanosecond(), 999_999_999); } +#[test] +fn test_min_max_setters() { + 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); + + assert_eq!(beyond_min.with_year(2020).unwrap().year(), 2020); + assert_eq!(beyond_min.with_month(beyond_min.month()), Some(beyond_min)); + assert_eq!(beyond_min.with_month(3), None); + assert_eq!(beyond_min.with_month0(beyond_min.month0()), Some(beyond_min)); + assert_eq!(beyond_min.with_month0(3), None); + assert_eq!(beyond_min.with_day(beyond_min.day()), Some(beyond_min)); + assert_eq!(beyond_min.with_day(15), None); + assert_eq!(beyond_min.with_day0(beyond_min.day0()), Some(beyond_min)); + assert_eq!(beyond_min.with_day0(15), None); + assert_eq!(beyond_min.with_ordinal(beyond_min.ordinal()), Some(beyond_min)); + assert_eq!(beyond_min.with_ordinal(200), None); + assert_eq!(beyond_min.with_ordinal0(beyond_min.ordinal0()), Some(beyond_min)); + assert_eq!(beyond_min.with_ordinal0(200), None); + assert_eq!(beyond_min.with_hour(beyond_min.hour()), Some(beyond_min)); + assert_eq!(beyond_min.with_hour(23), beyond_min.checked_add_signed(OldDuration::hours(1))); + assert_eq!(beyond_min.with_hour(5), None); + assert_eq!(beyond_min.with_minute(0), Some(beyond_min)); + assert_eq!(beyond_min.with_second(0), Some(beyond_min)); + assert_eq!(beyond_min.with_nanosecond(0), Some(beyond_min)); + + assert_eq!(beyond_max.with_year(2020).unwrap().year(), 2020); + assert_eq!(beyond_max.with_month(beyond_max.month()), Some(beyond_max)); + assert_eq!(beyond_max.with_month(3), None); + assert_eq!(beyond_max.with_month0(beyond_max.month0()), Some(beyond_max)); + assert_eq!(beyond_max.with_month0(3), None); + assert_eq!(beyond_max.with_day(beyond_max.day()), Some(beyond_max)); + assert_eq!(beyond_max.with_day(15), None); + assert_eq!(beyond_max.with_day0(beyond_max.day0()), Some(beyond_max)); + assert_eq!(beyond_max.with_day0(15), None); + assert_eq!(beyond_max.with_ordinal(beyond_max.ordinal()), Some(beyond_max)); + assert_eq!(beyond_max.with_ordinal(200), None); + assert_eq!(beyond_max.with_ordinal0(beyond_max.ordinal0()), Some(beyond_max)); + assert_eq!(beyond_max.with_ordinal0(200), None); + assert_eq!(beyond_max.with_hour(beyond_max.hour()), Some(beyond_max)); + assert_eq!(beyond_max.with_hour(0), beyond_max.checked_sub_signed(OldDuration::hours(1))); + assert_eq!(beyond_max.with_hour(5), None); + assert_eq!(beyond_max.with_minute(beyond_max.minute()), Some(beyond_max)); + assert_eq!(beyond_max.with_second(beyond_max.second()), Some(beyond_max)); + assert_eq!(beyond_max.with_nanosecond(beyond_max.nanosecond()), Some(beyond_max)); +} + #[test] #[should_panic] fn test_local_beyond_min_datetime() {