From d8bc9965b1b4d6251ad1c28b12fd23fd84200620 Mon Sep 17 00:00:00 2001 From: Moritz Oberhauser Date: Mon, 19 Feb 2024 11:08:22 +0100 Subject: [PATCH] Return `Result` for `Datelike::with_year` --- src/datetime/mod.rs | 29 ++++++++++++++++++-------- src/naive/date/mod.rs | 43 +++++++++++++++++++++++---------------- src/naive/date/tests.rs | 16 +++++++-------- src/naive/datetime/mod.rs | 25 ++++++++--------------- src/traits.rs | 37 +++------------------------------ 5 files changed, 67 insertions(+), 83 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 5bd1ea0b37..12557ad7f3 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -29,7 +29,7 @@ use crate::offset::{FixedOffset, Offset, TimeZone, Utc}; use crate::try_opt; #[cfg(any(feature = "clock", feature = "std"))] use crate::OutOfRange; -use crate::{Datelike, Months, TimeDelta, Timelike, Weekday}; +use crate::{Datelike, Error, Months, TimeDelta, Timelike, Weekday}; #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] use rkyv::{Archive, Deserialize, Serialize}; @@ -736,6 +736,21 @@ where .filter(|dt| dt >= &DateTime::::MIN_UTC && dt <= &DateTime::::MAX_UTC) } +/// Maps the local datetime to other datetime result with given conversion function. +fn map_local_result(dt: &DateTime, mut f: F) -> Result, Error> +where + F: FnMut(NaiveDateTime) -> Result, +{ + let naive_dt = f(dt.overflowing_naive_local())?; + let local_dt = dt.timezone().from_local_datetime(&naive_dt).map_result_unique()?; + + if local_dt < DateTime::::MIN_UTC || local_dt > DateTime::::MAX_UTC { + return Err(Error::OutOfRange); + } + + Ok(local_dt) +} + impl DateTime { /// Parses an RFC 2822 date-and-time string into a `DateTime` value. /// @@ -996,13 +1011,11 @@ impl Datelike for DateTime { /// /// # Errors /// - /// Returns `None` if: - /// - The resulting date does not exist. - /// - When the `NaiveDateTime` would be out of range. - /// - 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)) + /// - See [`NaiveDateTime::with_year`] if the intermediate conversion to [`NaiveDateTime`] fails. + /// - See [`LocalResult::map_result_unique`] if the conversion back to `DateTime` fails. + /// - Returns [`Error::OutOfRange`] if the final date is out of range for a `DateTime`. + fn with_year(&self, year: i32) -> Result, Error> { + map_local_result(self, |datetime| datetime.with_year(year)) } /// Makes a new `DateTime` with the month number (starting from 1) changed. diff --git a/src/naive/date/mod.rs b/src/naive/date/mod.rs index fe35dc4d5e..64c427c9f9 100644 --- a/src/naive/date/mod.rs +++ b/src/naive/date/mod.rs @@ -1452,33 +1452,42 @@ impl Datelike for NaiveDate { /// /// # Errors /// - /// Returns `None` if the resulting date does not exist, or when the `NaiveDate` would be - /// out of range. + /// - Returns [`Error::DoesNotExist`] when the resulting date does not exist. + /// - Returns [`Error::OutOfRange`] if `year` is out of range for a `NaiveDate`. /// /// # Example /// /// ``` - /// use chrono::{Datelike, NaiveDate}; + /// # use chrono::{Datelike, Error, NaiveDate}; /// - /// assert_eq!( - /// NaiveDate::from_ymd(2015, 9, 8).unwrap().with_year(2016), - /// Some(NaiveDate::from_ymd(2016, 9, 8).unwrap()) - /// ); - /// assert_eq!( - /// NaiveDate::from_ymd(2015, 9, 8).unwrap().with_year(-308), - /// Some(NaiveDate::from_ymd(-308, 9, 8).unwrap()) - /// ); + /// assert_eq!(NaiveDate::from_ymd(2015, 9, 8)?.with_year(2016), + /// NaiveDate::from_ymd(2016, 9, 8)); + /// assert_eq!(NaiveDate::from_ymd(2015, 9, 8)?.with_year(-308), + /// NaiveDate::from_ymd(-308, 9, 8)); + /// # Ok::<(), Error>(()) /// ``` /// - /// A leap day (February 29) is a good example that this method can return `None`. + /// A leap day (February 29) in a non-leap year will return [`Err(Error::DoesNotExist)`]. /// /// ``` - /// # use chrono::{NaiveDate, Datelike}; - /// assert!(NaiveDate::from_ymd(2016, 2, 29).unwrap().with_year(2015).is_none()); - /// assert!(NaiveDate::from_ymd(2016, 2, 29).unwrap().with_year(2020).is_some()); + /// # use chrono::{Datelike, Error, NaiveDate}; + /// assert!(NaiveDate::from_ymd(2016, 2, 29)?.with_year(2015).is_err()); + /// assert!(NaiveDate::from_ymd(2016, 2, 29)?.with_year(2020).is_ok()); + /// # Ok::<(), Error>(()) + /// ``` + /// + /// Don't use `with_year` if you want the ordinal date to stay the same. + /// + /// ``` + /// # use chrono::{Datelike, Error, NaiveDate}; + /// assert_ne!( + /// NaiveDate::from_yo(2020, 100).unwrap().with_year(2023)?, + /// NaiveDate::from_yo(2023, 100).unwrap() // result is 2023-101 + /// ); + /// # Ok::<(), Error>(()) /// ``` #[inline] - fn with_year(&self, year: i32) -> Option { + fn with_year(&self, year: i32) -> Result { // we need to operate with `mdf` since we should keep the month and day number as is let mdf = self.mdf(); @@ -1486,7 +1495,7 @@ impl Datelike for NaiveDate { let flags = YearFlags::from_year(year); let mdf = mdf.with_flags(flags); - ok!(NaiveDate::from_mdf(year, mdf)) + NaiveDate::from_mdf(year, mdf) } /// Makes a new `NaiveDate` with the month number (starting from 1) changed. diff --git a/src/naive/date/tests.rs b/src/naive/date/tests.rs index 908c489f65..fcc1ac8260 100644 --- a/src/naive/date/tests.rs +++ b/src/naive/date/tests.rs @@ -367,14 +367,14 @@ fn test_date_weekday() { #[test] fn test_date_with_fields() { let d = NaiveDate::from_ymd(2000, 2, 29).unwrap(); - assert_eq!(d.with_year(-400), Some(NaiveDate::from_ymd(-400, 2, 29).unwrap())); - assert_eq!(d.with_year(-100), None); - assert_eq!(d.with_year(1600), Some(NaiveDate::from_ymd(1600, 2, 29).unwrap())); - assert_eq!(d.with_year(1900), None); - assert_eq!(d.with_year(2000), Some(NaiveDate::from_ymd(2000, 2, 29).unwrap())); - assert_eq!(d.with_year(2001), None); - assert_eq!(d.with_year(2004), Some(NaiveDate::from_ymd(2004, 2, 29).unwrap())); - assert_eq!(d.with_year(i32::MAX), None); + assert_eq!(d.with_year(-400), NaiveDate::from_ymd(-400, 2, 29)); + assert_eq!(d.with_year(-100), Err(Error::DoesNotExist)); + assert_eq!(d.with_year(1600), NaiveDate::from_ymd(1600, 2, 29)); + assert_eq!(d.with_year(1900), Err(Error::DoesNotExist)); + assert_eq!(d.with_year(2000), NaiveDate::from_ymd(2000, 2, 29)); + assert_eq!(d.with_year(2001), Err(Error::DoesNotExist)); + assert_eq!(d.with_year(2004), NaiveDate::from_ymd(2004, 2, 29)); + assert_eq!(d.with_year(i32::MAX), Err(Error::OutOfRange)); let d = NaiveDate::from_ymd(2000, 4, 30).unwrap(); assert_eq!(d.with_month(0), None); diff --git a/src/naive/datetime/mod.rs b/src/naive/datetime/mod.rs index a9c42c3400..0930c12c9d 100644 --- a/src/naive/datetime/mod.rs +++ b/src/naive/datetime/mod.rs @@ -21,8 +21,8 @@ use crate::naive::{Days, IsoWeek, NaiveDate, NaiveTime}; use crate::offset::Utc; use crate::time_delta::NANOS_PER_SEC; use crate::{ - expect, ok, try_opt, DateTime, Datelike, FixedOffset, LocalResult, Months, TimeDelta, TimeZone, - Timelike, Weekday, + expect, ok, try_opt, DateTime, Datelike, Error, FixedOffset, LocalResult, Months, TimeDelta, + TimeZone, Timelike, Weekday, }; /// Tools to help serializing/deserializing `NaiveDateTime`s @@ -1263,26 +1263,19 @@ impl Datelike for NaiveDateTime { /// /// # Errors /// - /// Returns `None` if the resulting date does not exist, or when the `NaiveDateTime` would be - /// out of range. + /// - See [`NaiveDate::with_year`] if the intermediate conversion to [`NaiveDate`] fails. + /// - Cannot fail on the [`NaiveTime`] component. /// /// # Example /// /// ``` - /// use chrono::{Datelike, NaiveDate, NaiveDateTime}; - /// - /// let dt: NaiveDateTime = NaiveDate::from_ymd(2015, 9, 25).unwrap().and_hms(12, 34, 56).unwrap(); - /// assert_eq!( - /// dt.with_year(2016), - /// Some(NaiveDate::from_ymd(2016, 9, 25).unwrap().and_hms(12, 34, 56).unwrap()) - /// ); - /// assert_eq!( - /// dt.with_year(-308), - /// Some(NaiveDate::from_ymd(-308, 9, 25).unwrap().and_hms(12, 34, 56).unwrap()) - /// ); + /// # use chrono::{NaiveDate, NaiveDateTime, Datelike, Error}; + /// let dt: NaiveDateTime = NaiveDate::from_ymd(2015, 9, 25)?.and_hms(12, 34, 56)?; + /// assert_eq!(dt.with_year(2016), NaiveDate::from_ymd(2016, 9, 25)?.and_hms(12, 34, 56)); + /// # Ok::<(), Error>(()) /// ``` #[inline] - fn with_year(&self, year: i32) -> Option { + fn with_year(&self, year: i32) -> Result { self.date.with_year(year).map(|d| NaiveDateTime { date: d, ..*self }) } diff --git a/src/traits.rs b/src/traits.rs index 30faa1b9c2..684fc7ec56 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,4 +1,4 @@ -use crate::{IsoWeek, Weekday}; +use crate::{Error, IsoWeek, Weekday}; /// The common set of methods for date component. /// @@ -85,38 +85,7 @@ pub trait Datelike: Sized { /// This method assumes you want to work on the date as a year-month-day value. Don't use it if /// you want the ordinal to stay the same after changing the year, of if you want the week and /// weekday values to stay the same. - /// - /// # Errors - /// - /// Returns `None` when: - /// - /// - The resulting date does not exist (February 29 in a non-leap year). - /// - The year is out of range for [`NaiveDate`]. - /// - In case of [`DateTime`] if the resulting date and time fall within a timezone - /// transition such as from DST to standard time. - /// - /// [`NaiveDate`]: crate::NaiveDate - /// [`DateTime`]: crate::DateTime - /// - /// # Examples - /// - /// ``` - /// use chrono::{Datelike, NaiveDate}; - /// - /// assert_eq!( - /// NaiveDate::from_ymd(2020, 5, 13).unwrap().with_year(2023).unwrap(), - /// NaiveDate::from_ymd(2023, 5, 13).unwrap() - /// ); - /// // Resulting date 2023-02-29 does not exist: - /// assert!(NaiveDate::from_ymd(2020, 2, 29).unwrap().with_year(2023).is_none()); - /// - /// // Don't use `with_year` if you want the ordinal date to stay the same: - /// assert_ne!( - /// NaiveDate::from_yo(2020, 100).unwrap().with_year(2023).unwrap(), - /// NaiveDate::from_yo(2023, 100).unwrap() // result is 2023-101 - /// ); - /// ``` - fn with_year(&self, year: i32) -> Option; + fn with_year(&self, year: i32) -> Result; /// Makes a new value with the month number (starting from 1) changed. /// @@ -149,7 +118,7 @@ pub trait Datelike: Sized { /// use chrono::{Datelike, Error, NaiveDate}; /// /// fn with_year_month(date: NaiveDate, year: i32, month: u32) -> Option { - /// date.with_year(year)?.with_month(month) + /// date.with_year(year).ok()?.with_month(month) /// } /// let d = NaiveDate::from_ymd(2020, 2, 29).unwrap(); /// assert!(with_year_month(d, 2019, 1).is_none()); // fails because of invalid intermediate value