From 869e80eb642c33b9769f0ff10b86854d3d403cdc Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Thu, 7 Jul 2022 13:30:25 -0600 Subject: [PATCH 1/7] Add add_months() Cleanup Cleanup no_std No magic micromath trick PR feedback --- .gitignore | 3 ++ CHANGELOG.md | 1 + src/naive/date.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/.gitignore b/.gitignore index 9fac28caba..f02bb165c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ target Cargo.lock .tool-versions + +# for jetrains users +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 51fd48e902..1e31b4182a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Versions with only mechanical changes will be omitted from the following list. * Fix the behavior of `Duration::abs()` for negative durations with non-zero nanos * Add compatibility with rfc2822 comments (#733) * Make `js-sys` and `wasm-bindgen` enabled by default when target is `wasm32-unknown-unknown` for ease of API discovery +* Add the `add_months` method to `NaiveDate` ## 0.4.19 diff --git a/src/naive/date.rs b/src/naive/date.rs index 1fc895d11a..8097cec3a8 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -7,6 +7,7 @@ use core::borrow::Borrow; use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign}; use core::{fmt, str}; +use std::convert::TryFrom; use num_integer::div_mod_floor; use num_traits::ToPrimitive; @@ -596,6 +597,64 @@ impl NaiveDate { parsed.to_naive_date() } + /// An addition of months to `NaiveDate` clamped to valid days in resulting month. + /// + /// # Example + /// + /// ``` + /// use chrono::{Duration, NaiveDate}; + /// + /// let from_ymd = NaiveDate::from_ymd; + /// + /// assert_eq!(from_ymd(2014, 1, 1).add_months(1), from_ymd(2014, 2, 1)); + /// assert_eq!(from_ymd(2014, 1, 31).add_months(1), from_ymd(2014, 2, 28)); + /// assert_eq!(from_ymd(2020, 1, 31).add_months(1), from_ymd(2020, 2, 29)); + /// assert_eq!(from_ymd(2014, 1, 1).add_months(-11), from_ymd(2013, 2, 1)); + /// assert_eq!(from_ymd(2014, 1, 1).add_months(-12), from_ymd(2013, 1, 1)); + /// assert_eq!(from_ymd(2014, 1, 1).add_months(-13), from_ymd(2012, 12, 1)); + /// ``` + pub fn add_months(&self, months: i32) -> NaiveDate { + let target = self.add_months_get_first_day(months); + let target_plus = target.add_months_get_first_day(1); + let last_day = target_plus.sub(Duration::days(1)); + let day = core::cmp::min(self.day(), last_day.day()); + NaiveDate::from_ymd(target.year(), target.month(), day) + } + + /// Private function to calculate necessary primitives for `add_months()` + /// + /// # Arguments + /// + /// * `delta` - Number of months (+/-) to add + /// + /// # Returns + /// + /// A new NaiveDate on the first day of the resulting year & month + fn add_months_get_first_day(&self, delta: i32) -> NaiveDate { + let zeroed_months = + i32::try_from(self.month()) // zero-based for modulo operations + .expect("add_months_get_first_day does not support extreme values") + - 1; + let res_months = zeroed_months + delta; + let delta_years = if res_months < 0 { + // no f32::floor when no_std + if (-res_months) % 12 > 0 { + res_months / 12 - 1 + } else { + res_months / 12 + } + } else { + res_months / 12 + }; + let res_years = self.year() + delta_years; + let res_months = res_months % 12; + let res_months = if res_months < 0 { res_months + 12 } else { res_months }; + let res_months = u32::try_from(res_months) + .expect("add_month_get_first_day should never have a negative result") + + 1; + NaiveDate::from_ymd(res_years, res_months, 1) + } + /// Makes a new `NaiveDateTime` from the current date and given `NaiveTime`. /// /// # Example @@ -2002,6 +2061,46 @@ mod tests { use crate::{Datelike, Weekday}; use std::{i32, u32}; + #[test] + fn test_add_months_get_first_day() { + assert_eq!( + NaiveDate::from_ymd(2014, 1, 1).add_months_get_first_day(1), + NaiveDate::from_ymd(2014, 2, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2014, 1, 31).add_months_get_first_day(1), + NaiveDate::from_ymd(2014, 2, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2020, 1, 10).add_months_get_first_day(1), + NaiveDate::from_ymd(2020, 2, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2014, 1, 1).add_months_get_first_day(-1), + NaiveDate::from_ymd(2013, 12, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2014, 1, 31).add_months_get_first_day(-1), + NaiveDate::from_ymd(2013, 12, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2020, 1, 10).add_months_get_first_day(-1), + NaiveDate::from_ymd(2019, 12, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2014, 1, 10).add_months_get_first_day(-11), + NaiveDate::from_ymd(2013, 2, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2014, 1, 10).add_months_get_first_day(-12), + NaiveDate::from_ymd(2013, 1, 1) + ); + assert_eq!( + NaiveDate::from_ymd(2014, 1, 10).add_months_get_first_day(-13), + NaiveDate::from_ymd(2012, 12, 1) + ); + } + #[test] fn test_readme_doomsday() { use num_iter::range_inclusive; From 8ed12fb06352c666d24798c21f60a6b747e98264 Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Wed, 27 Jul 2022 10:52:02 -0600 Subject: [PATCH 2/7] PR feedback --- src/naive/date.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/naive/date.rs b/src/naive/date.rs index 8097cec3a8..1073d4bd38 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -5,9 +5,9 @@ #[cfg(any(feature = "alloc", feature = "std", test))] use core::borrow::Borrow; +use core::convert::TryFrom; use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign}; use core::{fmt, str}; -use std::convert::TryFrom; use num_integer::div_mod_floor; use num_traits::ToPrimitive; From b5ed0cd259aba2ca5af9e7161fa5656b4b54797b Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Thu, 28 Jul 2022 11:38:19 -0600 Subject: [PATCH 3/7] Undo try_from --- src/naive/date.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/naive/date.rs b/src/naive/date.rs index 1073d4bd38..e33acbb6bc 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -5,7 +5,6 @@ #[cfg(any(feature = "alloc", feature = "std", test))] use core::borrow::Borrow; -use core::convert::TryFrom; use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign}; use core::{fmt, str}; @@ -631,10 +630,7 @@ impl NaiveDate { /// /// A new NaiveDate on the first day of the resulting year & month fn add_months_get_first_day(&self, delta: i32) -> NaiveDate { - let zeroed_months = - i32::try_from(self.month()) // zero-based for modulo operations - .expect("add_months_get_first_day does not support extreme values") - - 1; + let zeroed_months = self.month() as i32 - 1; // zero-based for modulo operations let res_months = zeroed_months + delta; let delta_years = if res_months < 0 { // no f32::floor when no_std @@ -649,10 +645,7 @@ impl NaiveDate { let res_years = self.year() + delta_years; let res_months = res_months % 12; let res_months = if res_months < 0 { res_months + 12 } else { res_months }; - let res_months = u32::try_from(res_months) - .expect("add_month_get_first_day should never have a negative result") - + 1; - NaiveDate::from_ymd(res_years, res_months, 1) + NaiveDate::from_ymd(res_years, res_months as u32 + 1, 1) } /// Makes a new `NaiveDateTime` from the current date and given `NaiveTime`. From 4ad1416a214ea6e9cd2d7423fbe4b4f6f80fc2e3 Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Thu, 28 Jul 2022 12:00:30 -0600 Subject: [PATCH 4/7] Months struct with Add and Sub impls --- src/lib.rs | 2 +- src/month.rs | 4 +++ src/naive/date.rs | 80 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d3f3d673c3..e104ac9f79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -507,7 +507,7 @@ mod weekday; pub use weekday::{ParseWeekdayError, Weekday}; mod month; -pub use month::{Month, ParseMonthError}; +pub use month::{Month, ParseMonthError, Months}; mod traits; pub use traits::{Datelike, Timelike}; diff --git a/src/month.rs b/src/month.rs index 7b642f9ed9..ce0f17280c 100644 --- a/src/month.rs +++ b/src/month.rs @@ -189,6 +189,10 @@ impl num_traits::FromPrimitive for Month { } } +/// A duration in calendar months +#[derive(Clone, Debug, PartialEq)] +pub struct Months(pub usize); + /// An error resulting from reading `` value with `FromStr`. #[derive(Clone, PartialEq)] pub struct ParseMonthError { diff --git a/src/naive/date.rs b/src/naive/date.rs index e33acbb6bc..b2292e4e80 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -17,6 +17,7 @@ use rkyv::{Archive, Deserialize, Serialize}; use crate::format::DelayedFormat; use crate::format::{parse, ParseError, ParseResult, Parsed, StrftimeItems}; use crate::format::{Item, Numeric, Pad}; +use crate::month::Months; use crate::naive::{IsoWeek, NaiveDateTime, NaiveTime}; use crate::oldtime::Duration as OldDuration; use crate::{Datelike, Duration, Weekday}; @@ -596,31 +597,7 @@ impl NaiveDate { parsed.to_naive_date() } - /// An addition of months to `NaiveDate` clamped to valid days in resulting month. - /// - /// # Example - /// - /// ``` - /// use chrono::{Duration, NaiveDate}; - /// - /// let from_ymd = NaiveDate::from_ymd; - /// - /// assert_eq!(from_ymd(2014, 1, 1).add_months(1), from_ymd(2014, 2, 1)); - /// assert_eq!(from_ymd(2014, 1, 31).add_months(1), from_ymd(2014, 2, 28)); - /// assert_eq!(from_ymd(2020, 1, 31).add_months(1), from_ymd(2020, 2, 29)); - /// assert_eq!(from_ymd(2014, 1, 1).add_months(-11), from_ymd(2013, 2, 1)); - /// assert_eq!(from_ymd(2014, 1, 1).add_months(-12), from_ymd(2013, 1, 1)); - /// assert_eq!(from_ymd(2014, 1, 1).add_months(-13), from_ymd(2012, 12, 1)); - /// ``` - pub fn add_months(&self, months: i32) -> NaiveDate { - let target = self.add_months_get_first_day(months); - let target_plus = target.add_months_get_first_day(1); - let last_day = target_plus.sub(Duration::days(1)); - let day = core::cmp::min(self.day(), last_day.day()); - NaiveDate::from_ymd(target.year(), target.month(), day) - } - - /// Private function to calculate necessary primitives for `add_months()` + /// Private function to calculate necessary primitives for `Add` /// /// # Arguments /// @@ -1613,6 +1590,59 @@ impl AddAssign for NaiveDate { } } +impl Add for NaiveDate { + type Output = NaiveDate; + + /// An addition of months to `NaiveDate` clamped to valid days in resulting month. + /// + /// # Example + /// + /// ``` + /// use chrono::{Duration, NaiveDate, Months}; + /// + /// let from_ymd = NaiveDate::from_ymd; + /// + /// assert_eq!(from_ymd(2014, 1, 1) + Months(1), from_ymd(2014, 2, 1)); + /// assert_eq!(from_ymd(2014, 1, 1) + Months(11), from_ymd(2014, 12, 1)); + /// assert_eq!(from_ymd(2014, 1, 1) + Months(12), from_ymd(2015, 1, 1)); + /// assert_eq!(from_ymd(2014, 1, 1) + Months(13), from_ymd(2015, 2, 1)); + /// assert_eq!(from_ymd(2014, 1, 31) + Months(1), from_ymd(2014, 2, 28)); + /// assert_eq!(from_ymd(2020, 1, 31) + Months(1), from_ymd(2020, 2, 29)); + /// ``` + fn add(self, months: Months) -> Self::Output { + let target = self.add_months_get_first_day(months.0 as i32); + let target_plus = target.add_months_get_first_day(1); + let last_day = target_plus.sub(Duration::days(1)); + let day = core::cmp::min(self.day(), last_day.day()); + NaiveDate::from_ymd(target.year(), target.month(), day) + } +} + +impl Sub for NaiveDate { + type Output = NaiveDate; + + /// A subtraction of Months from `NaiveDate` clamped to valid days in resulting month. + /// + /// # Example + /// + /// ``` + /// use chrono::{Duration, NaiveDate, Months}; + /// + /// let from_ymd = NaiveDate::from_ymd; + /// + /// assert_eq!(from_ymd(2014, 1, 1) - Months(11), from_ymd(2013, 2, 1)); + /// assert_eq!(from_ymd(2014, 1, 1) - Months(12), from_ymd(2013, 1, 1)); + /// assert_eq!(from_ymd(2014, 1, 1) - Months(13), from_ymd(2012, 12, 1)); + /// ``` + fn sub(self, months: Months) -> Self::Output { + let target = self.add_months_get_first_day(-(months.0 as i32)); + let target_plus = target.add_months_get_first_day(1); + let last_day = target_plus.sub(Duration::days(1)); + let day = core::cmp::min(self.day(), last_day.day()); + NaiveDate::from_ymd(target.year(), target.month(), day) + } +} + /// 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`. From ef04889702c576e3b309150283f599a491862bb1 Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Thu, 28 Jul 2022 12:01:36 -0600 Subject: [PATCH 5/7] lint --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e104ac9f79..32e2887329 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -507,7 +507,7 @@ mod weekday; pub use weekday::{ParseWeekdayError, Weekday}; mod month; -pub use month::{Month, ParseMonthError, Months}; +pub use month::{Month, Months, ParseMonthError}; mod traits; pub use traits::{Datelike, Timelike}; From d364f738e051a179795022a1c4f3caf6421e5acb Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Thu, 28 Jul 2022 12:27:28 -0600 Subject: [PATCH 6/7] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e31b4182a..be98bd3e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ Versions with only mechanical changes will be omitted from the following list. * Fix the behavior of `Duration::abs()` for negative durations with non-zero nanos * Add compatibility with rfc2822 comments (#733) * Make `js-sys` and `wasm-bindgen` enabled by default when target is `wasm32-unknown-unknown` for ease of API discovery -* Add the `add_months` method to `NaiveDate` +* Add the `Months` struct and associated `Add` and `Sub` impls ## 0.4.19 From 4e4906f8e7630fc8513d47a9395b1c6f57210892 Mon Sep 17 00:00:00 2001 From: Brent Gardner Date: Fri, 29 Jul 2022 09:19:12 -0600 Subject: [PATCH 7/7] PR feedback --- .gitignore | 2 +- src/naive/date.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f02bb165c4..f989181e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ target Cargo.lock .tool-versions -# for jetrains users +# for jetbrains users .idea/ diff --git a/src/naive/date.rs b/src/naive/date.rs index b2292e4e80..31487fd50e 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -610,7 +610,6 @@ impl NaiveDate { let zeroed_months = self.month() as i32 - 1; // zero-based for modulo operations let res_months = zeroed_months + delta; let delta_years = if res_months < 0 { - // no f32::floor when no_std if (-res_months) % 12 > 0 { res_months / 12 - 1 } else {