Skip to content

Commit

Permalink
Return Result for Datelike::with_year
Browse files Browse the repository at this point in the history
  • Loading branch information
Zomtir committed Feb 28, 2024
1 parent b9f133b commit 76c5746
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 84 deletions.
29 changes: 21 additions & 8 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -736,6 +736,21 @@ where
.filter(|dt| dt >= &DateTime::<Utc>::MIN_UTC && dt <= &DateTime::<Utc>::MAX_UTC)
}

/// Maps the local datetime to other datetime result with given conversion function.
fn map_local_result<Tz: TimeZone, F>(dt: &DateTime<Tz>, mut f: F) -> Result<DateTime<Tz>, Error>
where
F: FnMut(NaiveDateTime) -> Result<NaiveDateTime, Error>,
{
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::<Utc>::MIN_UTC || local_dt > DateTime::<Utc>::MAX_UTC {
return Err(Error::OutOfRange);

Check warning on line 748 in src/datetime/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/datetime/mod.rs#L748

Added line #L748 was not covered by tests
}

Ok(local_dt)
}

impl DateTime<FixedOffset> {
/// Parses an RFC 2822 date-and-time string into a `DateTime<FixedOffset>` value.
///
Expand Down Expand Up @@ -996,13 +1011,11 @@ impl<Tz: TimeZone> Datelike for DateTime<Tz> {
///
/// # 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<DateTime<Tz>> {
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<Tz>` fails.
/// - Returns [`Error::OutOfRange`] if the final date is out of range for a `DateTime<Utc>`.
fn with_year(&self, year: i32) -> Result<DateTime<Tz>, Error> {
map_local_result(self, |datetime| datetime.with_year(year))
}

/// Makes a new `DateTime` with the month number (starting from 1) changed.
Expand Down
43 changes: 26 additions & 17 deletions src/naive/date/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1452,41 +1452,50 @@ 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<NaiveDate> {
fn with_year(&self, year: i32) -> Result<NaiveDate, Error> {
// 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);

ok!(NaiveDate::from_mdf(year, mdf))
NaiveDate::from_mdf(year, mdf)
}

/// Makes a new `NaiveDate` with the month number (starting from 1) changed.
Expand Down
16 changes: 8 additions & 8 deletions src/naive/date/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 9 additions & 16 deletions src/naive/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<NaiveDateTime> {
fn with_year(&self, year: i32) -> Result<NaiveDateTime, Error> {
self.date.with_year(year).map(|d| NaiveDateTime { date: d, ..*self })
}

Expand Down
2 changes: 1 addition & 1 deletion src/offset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use core::fmt;

use crate::naive::{NaiveDate, NaiveDateTime};
use crate::DateTime;
use crate::{DateTime, Error};

pub(crate) mod fixed;
pub use self::fixed::FixedOffset;
Expand Down
37 changes: 3 additions & 34 deletions src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{IsoWeek, Weekday};
use crate::{Error, IsoWeek, Weekday};

/// The common set of methods for date component.
///
Expand Down Expand Up @@ -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<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
///
/// [`NaiveDate`]: crate::NaiveDate
/// [`DateTime<Tz>`]: 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<Self>;
fn with_year(&self, year: i32) -> Result<Self, Error>;

/// Makes a new value with the month number (starting from 1) changed.
///
Expand Down Expand Up @@ -149,7 +118,7 @@ pub trait Datelike: Sized {
/// use chrono::{Datelike, Error, NaiveDate};
///
/// fn with_year_month(date: NaiveDate, year: i32, month: u32) -> Option<NaiveDate> {
/// 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
Expand Down

0 comments on commit 76c5746

Please sign in to comment.