diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 0907b2a6c0..cbf383737d 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -24,7 +24,7 @@ use crate::format::DelayedFormat; use crate::format::Locale; use crate::format::{parse, ParseError, ParseResult, Parsed, StrftimeItems}; use crate::format::{Fixed, Item}; -use crate::naive::{IsoWeek, NaiveDate, NaiveDateTime, NaiveTime}; +use crate::naive::{Days, IsoWeek, NaiveDate, NaiveDateTime, NaiveTime}; #[cfg(feature = "clock")] use crate::offset::Local; use crate::offset::{FixedOffset, Offset, TimeZone, Utc}; @@ -339,6 +339,26 @@ impl DateTime { Some(tz.from_utc_datetime(&datetime)) } + /// Add a duration in [`Days`] to the date part of the `DateTime` + /// + /// Returns `None` if the resulting date would be out of range. + pub fn checked_add_days(self, days: Days) -> Option { + self.datetime + .checked_add_days(days)? + .and_local_timezone(TimeZone::from_offset(&self.offset)) + .single() + } + + /// Subtract a duration in [`Days`] to the date part of the `DateTime` + /// + /// Returns `None` if the resulting date would be out of range. + pub fn checked_sub_days(self, days: Days) -> Option { + self.datetime + .checked_sub_days(days)? + .and_local_timezone(TimeZone::from_offset(&self.offset)) + .single() + } + /// Subtracts another `DateTime` from the current date and time. /// This does not overflow or underflow at all. #[inline] @@ -897,6 +917,22 @@ impl Sub> for DateTime { } } +impl Add for DateTime { + type Output = DateTime; + + fn add(self, days: Days) -> Self::Output { + self.checked_add_days(days).unwrap() + } +} + +impl Sub for DateTime { + type Output = DateTime; + + fn sub(self, days: Days) -> Self::Output { + self.checked_sub_days(days).unwrap() + } +} + impl fmt::Debug for DateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}{:?}", self.naive_local(), self.offset) diff --git a/src/lib.rs b/src/lib.rs index eed9b259ac..fb4951ad9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -481,7 +481,7 @@ pub use format::{ParseError, ParseResult}; pub mod naive; #[doc(no_inline)] -pub use naive::{IsoWeek, NaiveDate, NaiveDateTime, NaiveTime, NaiveWeek}; +pub use naive::{Days, IsoWeek, NaiveDate, NaiveDateTime, NaiveTime, NaiveWeek}; pub mod offset; #[cfg(feature = "clock")] diff --git a/src/naive/date.rs b/src/naive/date.rs index 0c17a79b4e..3c656f7af9 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -115,6 +115,19 @@ impl NaiveWeek { } } +/// A duration in calendar days. This is useful becuase when using `TimeDelta` it is possible +/// that adding TimeDelta::days(1) doesn't increment the day value as expected due to it being a +/// fixed number of seconds. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] +pub struct Days(pub(crate) u64); + +impl Days { + /// Construct a new `Days` from a number of months + pub fn new(num: u64) -> Self { + Self(num) + } +} + /// ISO 8601 calendar date without timezone. /// Allows for every [proleptic Gregorian date](#calendar-date) /// from Jan 1, 262145 BCE to Dec 31, 262143 CE. @@ -625,31 +638,6 @@ impl NaiveDate { } } - /// Subtract a duration in [`Months`] from the date - /// - /// If the day would be out of range for the resulting month, use the last day for that month. - /// - /// Returns `None` if the resulting date would be out of range. - /// - /// ``` - /// # use chrono::{NaiveDate, Months}; - /// assert_eq!( - /// NaiveDate::from_ymd(2022, 2, 20).checked_sub_months(Months::new(6)), - /// Some(NaiveDate::from_ymd(2021, 8, 20)) - /// ); - /// ``` - pub fn checked_sub_months(self, months: Months) -> Option { - if months.0 == 0 { - return Some(self); - } - - // Copy `i32::MIN` here so we don't have to do a complicated cast - match months.0 <= 2_147_483_648 { - true => self.diff_months(-(months.0 as i32)), - false => None, - } - } - fn diff_months(self, months: i32) -> Option { let (years, left) = ((months / 12), (months % 12)); @@ -692,6 +680,83 @@ impl NaiveDate { NaiveDate::from_mdf(year, Mdf::new(month as u32, day, flags)) } + /// Subtract a duration in [`Months`] from the date + /// + /// If the day would be out of range for the resulting month, use the last day for that month. + /// + /// Returns `None` if the resulting date would be out of range. + /// + /// ``` + /// # use chrono::{NaiveDate, Months}; + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 2, 20).checked_sub_months(Months::new(6)), + /// Some(NaiveDate::from_ymd(2021, 8, 20)) + /// ); + /// ``` + pub fn checked_sub_months(self, months: Months) -> Option { + if months.0 == 0 { + return Some(self); + } + + // Copy `i32::MIN` here so we don't have to do a complicated cast + match months.0 <= 2_147_483_648 { + true => self.diff_months(-(months.0 as i32)), + false => None, + } + } + + /// Add a duration in [`Days`] to the date\ + /// + /// Returns `None` if the resulting date would be out of range. + /// + /// ``` + /// # use chrono::{NaiveDate, Days}; + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 2, 20).checked_add_days(Days::new(9)), + /// Some(NaiveDate::from_ymd(2022, 3, 1)) + /// ); + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 7, 31).checked_add_days(Days::new(2)), + /// Some(NaiveDate::from_ymd(2022, 8, 2)) + /// ); + /// ``` + pub fn checked_add_days(self, days: Days) -> Option { + if days.0 == 0 { + return Some(self); + } + + match days.0 <= core::i64::MAX as u64 { + true => self.diff_days(days.0 as i64), + false => None, + } + } + + /// Subtract a duration in [`Days`] from the date + /// + /// Returns `None` if the resulting date would be out of range. + /// + /// ``` + /// # use chrono::{NaiveDate, Days}; + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 2, 20).checked_sub_days(Days::new(6)), + /// Some(NaiveDate::from_ymd(2022, 2, 14)) + /// ); + /// ``` + pub fn checked_sub_days(self, days: Days) -> Option { + if days.0 == 0 { + return Some(self); + } + + match days.0 <= core::i64::MAX as u64 { + true => self.diff_days(-(days.0 as i64)), + false => None, + } + } + + fn diff_days(self, days: i64) -> Option { + self.checked_add_signed(Duration::days(days)) + } + /// Makes a new `NaiveDateTime` from the current date and given `NaiveTime`. /// /// # Example @@ -1710,6 +1775,22 @@ impl Sub for NaiveDate { } } +impl Add for NaiveDate { + type Output = NaiveDate; + + fn add(self, days: Days) -> Self::Output { + self.checked_add_days(days).unwrap() + } +} + +impl Sub for NaiveDate { + type Output = NaiveDate; + + fn sub(self, days: Days) -> Self::Output { + self.checked_sub_days(days).unwrap() + } +} + /// A subtraction of `Duration` from `NaiveDate` discards the fractional days, /// rounding to the closest integral number of days towards `Duration::zero()`. /// It is the same as the addition with a negated `Duration`. diff --git a/src/naive/datetime/mod.rs b/src/naive/datetime/mod.rs index 7c60c41e30..cc685e9d5e 100644 --- a/src/naive/datetime/mod.rs +++ b/src/naive/datetime/mod.rs @@ -17,7 +17,7 @@ use rkyv::{Archive, Deserialize, Serialize}; use crate::format::DelayedFormat; use crate::format::{parse, ParseError, ParseResult, Parsed, StrftimeItems}; use crate::format::{Fixed, Item, Numeric, Pad}; -use crate::naive::{IsoWeek, NaiveDate, NaiveTime}; +use crate::naive::{Days, IsoWeek, NaiveDate, NaiveTime}; use crate::oldtime::Duration as OldDuration; use crate::{DateTime, Datelike, LocalResult, TimeZone, Timelike, Weekday}; @@ -606,6 +606,24 @@ impl NaiveDateTime { Some(NaiveDateTime { date, time }) } + /// Add a duration in [`Days`] to the date part of the `NaiveDateTime` + /// + /// Returns `None` if the resulting date would be out of range. + pub fn checked_add_days(self, days: Days) -> Option { + let new_date = self.date().checked_add_days(days)?; + + Some(new_date.and_time(self.time())) + } + + /// Subtract a duration in [`Days`] from the date part of the `NaiveDateTime` + /// + /// Returns `None` if the resulting date would be out of range. + pub fn checked_sub_days(self, days: Days) -> Option { + let new_date = self.date().checked_sub_days(days)?; + + Some(new_date.and_time(self.time())) + } + /// Subtracts another `NaiveDateTime` from the current date and time. /// This does not overflow or underflow at all. /// @@ -1401,6 +1419,22 @@ impl Sub for NaiveDateTime { } } +impl Add for NaiveDateTime { + type Output = NaiveDateTime; + + fn add(self, days: Days) -> Self::Output { + self.checked_add_days(days).unwrap() + } +} + +impl Sub for NaiveDateTime { + type Output = NaiveDateTime; + + fn sub(self, days: Days) -> Self::Output { + self.checked_sub_days(days).unwrap() + } +} + /// The `Debug` output of the naive date and time `dt` is the same as /// [`dt.format("%Y-%m-%dT%H:%M:%S%.f")`](crate::format::strftime). /// diff --git a/src/naive/mod.rs b/src/naive/mod.rs index 2ddd9a0134..bc7c0f4eb8 100644 --- a/src/naive/mod.rs +++ b/src/naive/mod.rs @@ -11,7 +11,7 @@ mod isoweek; mod time; #[allow(deprecated)] -pub use self::date::{NaiveDate, NaiveWeek, MAX_DATE, MIN_DATE}; +pub use self::date::{Days, NaiveDate, NaiveWeek, MAX_DATE, MIN_DATE}; #[cfg(feature = "rustc-serialize")] #[allow(deprecated)] pub use self::datetime::rustc_serialize::TsSeconds;