From 381b2343c237dbc0545660b7544f854a1e8fea89 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Wed, 17 Aug 2022 23:18:15 +1000 Subject: [PATCH 1/3] add/sub Days docs fixes improve checked impls --- src/datetime/mod.rs | 38 ++++++++++++++++++- src/lib.rs | 2 +- src/naive/date.rs | 79 +++++++++++++++++++++++++++++++++++++++ src/naive/datetime/mod.rs | 32 +++++++++++++++- src/naive/mod.rs | 2 +- 5 files changed, 149 insertions(+), 4 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 8705c2b76d..48eb85001a 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}; @@ -366,6 +366,26 @@ impl DateTime { .single() } + /// 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] @@ -952,6 +972,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 f9a00b161d..53b7706cce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -487,7 +487,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 d2fdc6d9d3..cc674c70b8 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -5,6 +5,7 @@ #[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}; @@ -115,6 +116,22 @@ impl NaiveWeek { } } +/// A duration in calendar days. +/// +/// This is useful becuase when using `Duration` it is possible +/// that adding `Duration::days(1)` doesn't increment the day value as expected due to it being a +/// fixed number of seconds. This difference applies only when dealing with `DateTime` data types +/// and in other cases `Duration::days(n)` and `Days::new(n)` are equivalent. +#[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. @@ -698,6 +715,52 @@ impl NaiveDate { NaiveDate::from_mdf(year, Mdf::new(month as u32, day, flags)) } + /// 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); + } + + i64::try_from(days.0).ok().and_then(|d| self.diff_days(d)) + } + + /// 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); + } + + i64::try_from(days.0).ok().and_then(|d| self.diff_days(-d)) + } + + 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 @@ -1718,6 +1781,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 577fe87d00..c0e851e4e3 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, Months, TimeZone, Timelike, Weekday}; @@ -662,6 +662,20 @@ impl NaiveDateTime { Some(Self { date: self.date.checked_sub_months(rhs)?, time: self.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 { + Some(Self { date: self.date.checked_add_days(days)?, ..self }) + } + + /// 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 { + Some(Self { date: self.date.checked_sub_days(days)?, ..self }) + } + /// Subtracts another `NaiveDateTime` from the current date and time. /// This does not overflow or underflow at all. /// @@ -1537,6 +1551,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 f7cb298b66..c41acba8d3 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; From 5721f2b36525752574895f6d19ffeeda9f067c98 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Tue, 23 Aug 2022 22:33:05 +1000 Subject: [PATCH 2/3] to -> from --- src/datetime/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 48eb85001a..7131279bc3 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -376,7 +376,7 @@ impl DateTime { .single() } - /// Subtract a duration in [`Days`] to the date part of the `DateTime` + /// Subtract a duration in [`Days`] from 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 { From 3e4f4448f80ea8d9d82823e73e8ad39d9fb975f6 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Fri, 26 Aug 2022 21:28:50 +1000 Subject: [PATCH 3/3] duplicate add/sub days tests --- src/naive/date.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/naive/date.rs b/src/naive/date.rs index cc674c70b8..27dcafbb6e 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -2235,11 +2235,14 @@ mod serde { #[cfg(test)] mod tests { use super::{ - Months, NaiveDate, MAX_DAYS_FROM_YEAR_0, MAX_YEAR, MIN_DAYS_FROM_YEAR_0, MIN_YEAR, + Days, 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}; + use std::{ + convert::{TryFrom, TryInto}, + i32, u32, + }; #[test] fn diff_months() { @@ -2682,6 +2685,51 @@ mod tests { check((MIN_YEAR, 1, 1), (0, 1, 1), Duration::days(MIN_DAYS_FROM_YEAR_0 as i64)); } + #[test] + fn test_date_add_days() { + fn check((y1, m1, d1): (i32, u32, u32), rhs: Days, ymd: Option<(i32, u32, u32)>) { + let lhs = NaiveDate::from_ymd(y1, m1, d1); + let sum = ymd.map(|(y, m, d)| NaiveDate::from_ymd(y, m, d)); + assert_eq!(lhs.checked_add_days(rhs), sum); + } + + check((2014, 1, 1), Days::new(0), Some((2014, 1, 1))); + // always round towards zero + check((2014, 1, 1), Days::new(1), Some((2014, 1, 2))); + check((2014, 1, 1), Days::new(364), Some((2014, 12, 31))); + check((2014, 1, 1), Days::new(365 * 4 + 1), Some((2018, 1, 1))); + check((2014, 1, 1), Days::new(365 * 400 + 97), Some((2414, 1, 1))); + + check((-7, 1, 1), Days::new(365 * 12 + 3), Some((5, 1, 1))); + + // overflow check + check( + (0, 1, 1), + Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()), + Some((MAX_YEAR, 12, 31)), + ); + check((0, 1, 1), Days::new(u64::try_from(MAX_DAYS_FROM_YEAR_0).unwrap() + 1), None); + } + + #[test] + fn test_date_sub_days() { + fn check((y1, m1, d1): (i32, u32, u32), (y2, m2, d2): (i32, u32, u32), diff: Days) { + let lhs = NaiveDate::from_ymd(y1, m1, d1); + let rhs = NaiveDate::from_ymd(y2, m2, d2); + assert_eq!(lhs - diff, rhs); + } + + check((2014, 1, 1), (2014, 1, 1), Days::new(0)); + check((2014, 1, 2), (2014, 1, 1), Days::new(1)); + check((2014, 12, 31), (2014, 1, 1), Days::new(364)); + check((2015, 1, 3), (2014, 1, 1), Days::new(365 + 2)); + check((2018, 1, 1), (2014, 1, 1), Days::new(365 * 4 + 1)); + check((2414, 1, 1), (2014, 1, 1), Days::new(365 * 400 + 97)); + + check((MAX_YEAR, 12, 31), (0, 1, 1), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap())); + check((0, 1, 1), (MIN_YEAR, 1, 1), Days::new((-MIN_DAYS_FROM_YEAR_0).try_into().unwrap())); + } + #[test] fn test_date_addassignment() { let ymd = NaiveDate::from_ymd;