mirror of
https://github.com/chronotope/chrono.git
synced 2025-10-02 15:26:12 +00:00
Revise Months API
* Provide `checked_add_months()` and `checked_sub_months()` for callers that would like to avoid panics * Document panic potential in `Add` and `Sub` implementations * Implement additional traits for `Months` per API guidelines * Hide inner type for `Months` and add constructor * Use lower-level APIs to clamp day
This commit is contained in:
parent
ab688c384f
commit
bd3b48d1a9
11
src/month.rs
11
src/month.rs
@ -190,8 +190,15 @@ impl num_traits::FromPrimitive for Month {
|
||||
}
|
||||
|
||||
/// A duration in calendar months
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Months(pub usize);
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
|
||||
pub struct Months(pub(crate) u32);
|
||||
|
||||
impl Months {
|
||||
/// Construct a new `Months` from a number of months
|
||||
pub fn new(num: u32) -> Self {
|
||||
Self(num)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error resulting from reading `<Month>` value with `FromStr`.
|
||||
#[derive(Clone, PartialEq)]
|
||||
|
@ -597,31 +597,99 @@ impl NaiveDate {
|
||||
parsed.to_naive_date()
|
||||
}
|
||||
|
||||
/// Private function to calculate necessary primitives for `Add<Month>`
|
||||
/// Add a duration in [`Months`] to the date
|
||||
///
|
||||
/// # Arguments
|
||||
/// If the day would be out of range for the resulting month, use the last day for that month.
|
||||
///
|
||||
/// * `delta` - Number of months (+/-) to add
|
||||
/// Returns `None` if the resulting date would be out of range.
|
||||
///
|
||||
/// # 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 = self.month() as i32 - 1; // zero-based for modulo operations
|
||||
let res_months = zeroed_months + delta;
|
||||
let delta_years = if res_months < 0 {
|
||||
if (-res_months) % 12 > 0 {
|
||||
res_months / 12 - 1
|
||||
} else {
|
||||
res_months / 12
|
||||
/// ```
|
||||
/// # use chrono::{NaiveDate, Months};
|
||||
/// assert_eq!(
|
||||
/// NaiveDate::from_ymd(2022, 2, 20).checked_add_months(Months::new(6)),
|
||||
/// Some(NaiveDate::from_ymd(2022, 8, 20))
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// NaiveDate::from_ymd(2022, 7, 31).checked_add_months(Months::new(2)),
|
||||
/// Some(NaiveDate::from_ymd(2022, 9, 30))
|
||||
/// );
|
||||
/// ```
|
||||
pub fn checked_add_months(self, months: Months) -> Option<Self> {
|
||||
if months.0 == 0 {
|
||||
return Some(self);
|
||||
}
|
||||
|
||||
match months.0 <= core::i32::MAX as u32 {
|
||||
true => self.diff_months(months.0 as i32),
|
||||
false => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Self> {
|
||||
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<Self> {
|
||||
let (years, left) = ((months / 12), (months % 12));
|
||||
|
||||
// Determine new year (without taking months into account for now
|
||||
|
||||
let year = if (years > 0 && years > (MAX_YEAR - self.year()))
|
||||
|| (years < 0 && years < (MIN_YEAR - self.year()))
|
||||
{
|
||||
return None;
|
||||
} else {
|
||||
res_months / 12
|
||||
self.year() + years
|
||||
};
|
||||
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 };
|
||||
NaiveDate::from_ymd(res_years, res_months as u32 + 1, 1)
|
||||
|
||||
// Determine new month
|
||||
|
||||
let month = self.month() as i32 + left;
|
||||
let (year, month) = if month <= 0 {
|
||||
if year == MIN_YEAR {
|
||||
return None;
|
||||
}
|
||||
|
||||
(year - 1, month + 12)
|
||||
} else if month > 12 {
|
||||
if year == MAX_YEAR {
|
||||
return None;
|
||||
}
|
||||
|
||||
(year + 1, month - 12)
|
||||
} else {
|
||||
(year, month)
|
||||
};
|
||||
|
||||
// Clamp original day in case new month is shorter
|
||||
|
||||
let flags = YearFlags::from_year(year);
|
||||
let feb_days = if flags.ndays() == 366 { 29 } else { 28 };
|
||||
let days = [31, feb_days, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let day = Ord::min(self.day(), days[(month - 1) as usize]);
|
||||
|
||||
NaiveDate::from_mdf(year, Mdf::new(month as u32, day, flags))
|
||||
}
|
||||
|
||||
/// Makes a new `NaiveDateTime` from the current date and given `NaiveTime`.
|
||||
@ -1594,6 +1662,10 @@ impl Add<Months> for NaiveDate {
|
||||
|
||||
/// An addition of months to `NaiveDate` clamped to valid days in resulting month.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the resulting date would be out of range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
@ -1601,19 +1673,15 @@ impl Add<Months> for NaiveDate {
|
||||
///
|
||||
/// 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));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) + Months::new(1), from_ymd(2014, 2, 1));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) + Months::new(11), from_ymd(2014, 12, 1));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) + Months::new(12), from_ymd(2015, 1, 1));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) + Months::new(13), from_ymd(2015, 2, 1));
|
||||
/// assert_eq!(from_ymd(2014, 1, 31) + Months::new(1), from_ymd(2014, 2, 28));
|
||||
/// assert_eq!(from_ymd(2020, 1, 31) + Months::new(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)
|
||||
self.checked_add_months(months).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1622,6 +1690,10 @@ impl Sub<Months> for NaiveDate {
|
||||
|
||||
/// A subtraction of Months from `NaiveDate` clamped to valid days in resulting month.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the resulting date would be out of range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
@ -1629,16 +1701,12 @@ impl Sub<Months> for NaiveDate {
|
||||
///
|
||||
/// 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));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) - Months::new(11), from_ymd(2013, 2, 1));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) - Months::new(12), from_ymd(2013, 1, 1));
|
||||
/// assert_eq!(from_ymd(2014, 1, 1) - Months::new(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)
|
||||
self.checked_sub_months(months).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2077,49 +2145,92 @@ mod serde {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NaiveDate;
|
||||
use super::{MAX_DAYS_FROM_YEAR_0, MAX_YEAR, MIN_DAYS_FROM_YEAR_0, MIN_YEAR};
|
||||
use super::{
|
||||
Months, NaiveDate, MAX_DAYS_FROM_YEAR_0, MAX_YEAR, MIN_DAYS_FROM_YEAR_0, MIN_YEAR,
|
||||
};
|
||||
use crate::oldtime::Duration;
|
||||
use crate::{Datelike, Weekday};
|
||||
use std::{i32, u32};
|
||||
|
||||
#[test]
|
||||
fn test_add_months_get_first_day() {
|
||||
fn diff_months() {
|
||||
// identity
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 1).add_months_get_first_day(1),
|
||||
NaiveDate::from_ymd(2014, 2, 1)
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_add_months(Months::new(0)),
|
||||
Some(NaiveDate::from_ymd(2022, 8, 3))
|
||||
);
|
||||
|
||||
// add with months exceeding `i32::MAX`
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 31).add_months_get_first_day(1),
|
||||
NaiveDate::from_ymd(2014, 2, 1)
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_add_months(Months::new(i32::MAX as u32 + 1)),
|
||||
None
|
||||
);
|
||||
|
||||
// sub with months exceeindg `i32::MIN`
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2020, 1, 10).add_months_get_first_day(1),
|
||||
NaiveDate::from_ymd(2020, 2, 1)
|
||||
NaiveDate::from_ymd(2022, 8, 3)
|
||||
.checked_sub_months(Months::new((i32::MIN as i64).abs() as u32 + 1)),
|
||||
None
|
||||
);
|
||||
|
||||
// add overflowing year
|
||||
assert_eq!(NaiveDate::MAX.checked_add_months(Months::new(1)), None);
|
||||
|
||||
// add underflowing year
|
||||
assert_eq!(NaiveDate::MIN.checked_sub_months(Months::new(1)), None);
|
||||
|
||||
// sub crossing year 0 boundary
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 1).add_months_get_first_day(-1),
|
||||
NaiveDate::from_ymd(2013, 12, 1)
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_sub_months(Months::new(2050 * 12)),
|
||||
Some(NaiveDate::from_ymd(-28, 8, 3))
|
||||
);
|
||||
|
||||
// add crossing year boundary
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 31).add_months_get_first_day(-1),
|
||||
NaiveDate::from_ymd(2013, 12, 1)
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_add_months(Months::new(6)),
|
||||
Some(NaiveDate::from_ymd(2023, 2, 3))
|
||||
);
|
||||
|
||||
// sub crossing year boundary
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2020, 1, 10).add_months_get_first_day(-1),
|
||||
NaiveDate::from_ymd(2019, 12, 1)
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_sub_months(Months::new(10)),
|
||||
Some(NaiveDate::from_ymd(2021, 10, 3))
|
||||
);
|
||||
|
||||
// add clamping day, non-leap year
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 10).add_months_get_first_day(-11),
|
||||
NaiveDate::from_ymd(2013, 2, 1)
|
||||
NaiveDate::from_ymd(2022, 1, 29).checked_add_months(Months::new(1)),
|
||||
Some(NaiveDate::from_ymd(2022, 2, 28))
|
||||
);
|
||||
|
||||
// add to leap day
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 10).add_months_get_first_day(-12),
|
||||
NaiveDate::from_ymd(2013, 1, 1)
|
||||
NaiveDate::from_ymd(2022, 10, 29).checked_add_months(Months::new(16)),
|
||||
Some(NaiveDate::from_ymd(2024, 2, 29))
|
||||
);
|
||||
|
||||
// add into december
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2014, 1, 10).add_months_get_first_day(-13),
|
||||
NaiveDate::from_ymd(2012, 12, 1)
|
||||
NaiveDate::from_ymd(2022, 10, 31).checked_add_months(Months::new(2)),
|
||||
Some(NaiveDate::from_ymd(2022, 12, 31))
|
||||
);
|
||||
|
||||
// sub into december
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2022, 10, 31).checked_sub_months(Months::new(10)),
|
||||
Some(NaiveDate::from_ymd(2021, 12, 31))
|
||||
);
|
||||
|
||||
// add into january
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_add_months(Months::new(5)),
|
||||
Some(NaiveDate::from_ymd(2023, 1, 3))
|
||||
);
|
||||
|
||||
// sub into january
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd(2022, 8, 3).checked_sub_months(Months::new(7)),
|
||||
Some(NaiveDate::from_ymd(2022, 1, 3))
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user