Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add/sub Days #784

Merged
merged 3 commits into from Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/datetime/mod.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -366,6 +366,26 @@ impl<Tz: TimeZone> DateTime<Tz> {
.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> {
self.datetime
.checked_add_days(days)?
.and_local_timezone(TimeZone::from_offset(&self.offset))
.single()
}

/// 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<Self> {
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]
Expand Down Expand Up @@ -952,6 +972,22 @@ impl<Tz: TimeZone> Sub<DateTime<Tz>> for DateTime<Tz> {
}
}

impl<Tz: TimeZone> Add<Days> for DateTime<Tz> {
type Output = DateTime<Tz>;

fn add(self, days: Days) -> Self::Output {
self.checked_add_days(days).unwrap()
}
}

impl<Tz: TimeZone> Sub<Days> for DateTime<Tz> {
type Output = DateTime<Tz>;

fn sub(self, days: Days) -> Self::Output {
self.checked_sub_days(days).unwrap()
}
}

impl<Tz: TimeZone> fmt::Debug for DateTime<Tz> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}{:?}", self.naive_local(), self.offset)
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Expand Up @@ -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")]
Expand Down
131 changes: 129 additions & 2 deletions src/naive/date.rs
Expand Up @@ -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};

Expand Down Expand Up @@ -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<TimeZone>` 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.
Expand Down Expand Up @@ -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<Self> {
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<Self> {
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> {
self.checked_add_signed(Duration::days(days))
}

/// Makes a new `NaiveDateTime` from the current date and given `NaiveTime`.
///
/// # Example
Expand Down Expand Up @@ -1718,6 +1781,22 @@ impl Sub<Months> for NaiveDate {
}
}

impl Add<Days> for NaiveDate {
type Output = NaiveDate;

fn add(self, days: Days) -> Self::Output {
self.checked_add_days(days).unwrap()
}
}

impl Sub<Days> 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`.
Expand Down Expand Up @@ -2156,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() {
Expand Down Expand Up @@ -2603,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;
Expand Down
32 changes: 31 additions & 1 deletion src/naive/datetime/mod.rs
Expand Up @@ -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};

Expand Down Expand Up @@ -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<Self> {
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<Self> {
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.
///
Expand Down Expand Up @@ -1537,6 +1551,22 @@ impl Sub<NaiveDateTime> for NaiveDateTime {
}
}

impl Add<Days> for NaiveDateTime {
type Output = NaiveDateTime;

fn add(self, days: Days) -> Self::Output {
self.checked_add_days(days).unwrap()
}
}

impl Sub<Days> 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).
///
Expand Down
2 changes: 1 addition & 1 deletion src/naive/mod.rs
Expand Up @@ -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;
Expand Down