diff --git a/src/lib.rs b/src/lib.rs index b5401a0cd1..859a3801c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -439,6 +439,8 @@ pub mod prelude { #[doc(no_inline)] pub use crate::Locale; #[doc(no_inline)] + pub use crate::MonthsDelta; + #[doc(no_inline)] pub use crate::SubsecRound; #[doc(no_inline)] pub use crate::{DateTime, SecondsFormat}; @@ -489,7 +491,7 @@ mod weekday; pub use weekday::{ParseWeekdayError, Weekday}; mod month; -pub use month::{Month, Months, ParseMonthError}; +pub use month::{Month, Months, MonthsDelta, ParseMonthError}; mod traits; pub use traits::{Datelike, Timelike}; diff --git a/src/month.rs b/src/month.rs index baa3a66022..fde3cce98f 100644 --- a/src/month.rs +++ b/src/month.rs @@ -191,7 +191,7 @@ impl num_traits::FromPrimitive for Month { } /// A duration in calendar months -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Months(pub(crate) u32); @@ -202,6 +202,66 @@ impl Months { } } +/// A difference in a number of months, either forwards or backwards. +/// +/// This type is often returned from fuctions but is generally not used as a parameter. +/// Instead the inner `Months` must first be extracted and then used. There are helper methods +/// which can assist with this. +#[derive(Clone, Copy, Debug, PartialOrd, Ord)] +pub enum MonthsDelta { + /// the forwards direction + Forwards(Months), + /// the backwards direction + Backwards(Months), +} + +impl MonthsDelta { + /// Assert that the direction is forwards and throw away the `Months` otherwise. + pub fn forwards(self) -> Option { + match self { + MonthsDelta::Forwards(d) => Some(d), + MonthsDelta::Backwards(_) => None, + } + } + + /// Assert that the direction is backwards and throw away the `Months` otherwise. + pub fn backwards(self) -> Option { + match self { + MonthsDelta::Forwards(_) => None, + MonthsDelta::Backwards(d) => Some(d), + } + } + + /// Get the contained `Months`, no matter which direction + pub fn abs(self) -> Months { + match self { + MonthsDelta::Backwards(d) => d, + MonthsDelta::Forwards(d) => d, + } + } +} + +impl PartialEq for MonthsDelta { + fn eq(&self, other: &MonthsDelta) -> bool { + match (self, other) { + (MonthsDelta::Forwards(f1), MonthsDelta::Forwards(f2)) => f1 == f2, + (MonthsDelta::Backwards(b1), MonthsDelta::Backwards(b2)) => b1 == b2, + (MonthsDelta::Forwards(lhs), MonthsDelta::Backwards(rhs)) + | (MonthsDelta::Backwards(lhs), MonthsDelta::Forwards(rhs)) => { + *lhs == Months(0) && *rhs == Months(0) + } + } + } +} + +impl Eq for MonthsDelta {} + +impl From for MonthsDelta { + fn from(s: Months) -> Self { + MonthsDelta::Forwards(s) + } +} + /// An error resulting from reading `` value with `FromStr`. #[derive(Clone, PartialEq, Eq)] pub struct ParseMonthError { diff --git a/src/naive/date.rs b/src/naive/date.rs index e362cd96ef..e42e5beb44 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -18,7 +18,7 @@ use rkyv::{Archive, Deserialize, Serialize}; use crate::format::DelayedFormat; use crate::format::{parse, write_hundreds, ParseError, ParseResult, Parsed, StrftimeItems}; use crate::format::{Item, Numeric, Pad}; -use crate::month::Months; +use crate::month::{Months, MonthsDelta}; use crate::naive::{IsoWeek, NaiveDateTime, NaiveTime}; use crate::oldtime::Duration as OldDuration; use crate::{Datelike, Duration, Weekday}; @@ -1054,6 +1054,49 @@ impl NaiveDate { ) } + /// Subtracts another `NaiveDate` from the current date. + /// Returns a `MonthsDelta` of the number of calendar months between the two dates. + /// + /// # Examples + /// + /// ``` + /// use chrono::{NaiveDate, MonthsDelta, Months}; + /// + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 4, 18).months_since(NaiveDate::from_ymd(2022, 4, 1)), + /// MonthsDelta::Forwards(Months::new(0)) + /// ); + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 5, 1).months_since(NaiveDate::from_ymd(2022, 4, 30)), + /// MonthsDelta::Forwards(Months::new(1)) + /// ); + /// assert_eq!( + /// NaiveDate::from_ymd(2023, 5, 1).months_since(NaiveDate::from_ymd(2022, 4, 30)), + /// MonthsDelta::Forwards(Months::new(13)) + /// ); + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 4, 1).months_since(NaiveDate::from_ymd(2022, 4, 18)), + /// MonthsDelta::Backwards(Months::new(0)) + /// ); + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 4, 30).months_since(NaiveDate::from_ymd(2022, 5, 1)), + /// MonthsDelta::Backwards(Months::new(1)) + /// ); + /// assert_eq!( + /// NaiveDate::from_ymd(2022, 4, 30).months_since(NaiveDate::from_ymd(2023, 5, 1)), + /// MonthsDelta::Backwards(Months::new(13)) + /// ); + /// ``` + pub fn months_since(self, rhs: NaiveDate) -> MonthsDelta { + if self >= rhs { + let years = u32::try_from(self.year() - rhs.year()).expect("Will succeed"); + MonthsDelta::Forwards(Months::new(years * 12 + self.month() - rhs.month())) + } else { + let years = u32::try_from((rhs.year() - self.year()).abs()).expect("Will succeed"); + MonthsDelta::Backwards(Months::new(years * 12 + rhs.month() - self.month())) + } + } + /// Formats the date with the specified formatting items. /// Otherwise it is the same as the ordinary `format` method. ///