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 a "DateDelta" datatype for differences in number of days #858

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 139 additions & 60 deletions src/naive/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

#[cfg(any(feature = "alloc", feature = "std", test))]
use core::borrow::Borrow;
use core::convert::TryFrom;
use core::convert::{TryFrom, TryInto};
use core::hash::Hash;
use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign};
use core::{fmt, str};

Expand Down Expand Up @@ -81,7 +82,7 @@ impl NaiveWeek {
let start = self.start.num_days_from_monday();
let end = self.date.weekday().num_days_from_monday();
let days = if start > end { 7 - start + end } else { end - start };
self.date - Duration::days(days.into())
self.date - Days::from_u32(days)
}

/// Returns a date representing the last day of the week.
Expand All @@ -97,7 +98,7 @@ impl NaiveWeek {
/// ```
#[inline]
pub fn last_day(&self) -> NaiveDate {
self.first_day() + Duration::days(6)
self.first_day() + Days::from_u32(6)
}

/// Returns a [`RangeInclusive<T>`] representing the whole week bounded by
Expand Down Expand Up @@ -125,17 +126,29 @@ impl NaiveWeek {
/// 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);
/// and in other cases `Duration::days(n)` and `Days::from_u32(n)` are equivalent.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Days(pub(crate) u32);

impl Days {
/// Construct a new `Days` from a number of days
#[deprecated(since = "0.4.23", note = "Use Days::from_u32 instead")]
pub fn new(num: u64) -> Self {
Self(u32::try_from(num).unwrap())
}

/// Construct a new `Days` from a number of days
pub fn from_u32(num: u32) -> Self {
Self(num)
}
}

impl From<Days> for Result<Days, Days> {
fn from(s: Days) -> Self {
Ok(s)
}
}

/// 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 @@ -226,7 +239,8 @@ fn test_date_bounds() {

// let's also check that the entire range do not exceed 2^44 seconds
// (sometimes used for bounding `Duration` against overflow)
let maxsecs = NaiveDate::MAX.signed_duration_since(NaiveDate::MIN).num_seconds();
let days = NaiveDate::MAX.days_since(NaiveDate::MIN).ok().unwrap();
let maxsecs = u64::from(days.0) * 24 * 60 * 60;
let maxsecs = maxsecs + 86401; // also take care of DateTime
assert!(
maxsecs < (1 << MAX_BITS),
Expand Down Expand Up @@ -639,11 +653,11 @@ impl NaiveDate {
/// ```
/// # use chrono::{NaiveDate, Days};
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2022, 2, 20).unwrap().checked_add_days(Days::new(9)),
/// NaiveDate::from_ymd_opt(2022, 2, 20).unwrap().checked_add_days(Days::from_u32(9)),
/// Some(NaiveDate::from_ymd_opt(2022, 3, 1).unwrap())
/// );
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2022, 7, 31).unwrap().checked_add_days(Days::new(2)),
/// NaiveDate::from_ymd_opt(2022, 7, 31).unwrap().checked_add_days(Days::from_u32(2)),
/// Some(NaiveDate::from_ymd_opt(2022, 8, 2).unwrap())
/// );
/// ```
Expand All @@ -662,7 +676,7 @@ impl NaiveDate {
/// ```
/// # use chrono::{NaiveDate, Days};
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2022, 2, 20).unwrap().checked_sub_days(Days::new(6)),
/// NaiveDate::from_ymd_opt(2022, 2, 20).unwrap().checked_sub_days(Days::from_u32(6)),
/// Some(NaiveDate::from_ymd_opt(2022, 2, 14).unwrap())
/// );
/// ```
Expand Down Expand Up @@ -1071,6 +1085,34 @@ impl NaiveDate {
}
}

/// Subtracts another `NaiveDate` from the current date.
///
/// Returns a `Result` where `Ok` means the `self` was later than
/// the `rhs`, and the `Err` variant means the inverse.
///
/// The `Ok` variant will be returned when `self` and `rhs` are equal
/// days between the two dates.
///
/// note: we explicitly don't use an implementor of the `Error`
/// type for the `E` parameter of the `Result`. This is because
/// this isn't intended to be handled with the `?` operator
pub fn days_since(self, rhs: NaiveDate) -> Result<Days, Days> {
let diff = self.signed_duration_since(rhs).num_days();

if diff >= 0 {
Ok(Days::from_u32(diff.try_into().unwrap()))
} else {
Err(Days::from_u32(diff.abs().try_into().unwrap()))
}
}

/// Finds the absolute difference in days between two `NaiveDate`s
pub fn abs_diff(self, rhs: NaiveDate) -> Days {
let diff = self.signed_duration_since(rhs).num_days();

Days::from_u32(diff.abs().try_into().unwrap())
}

/// Formats the date with the specified formatting items.
/// Otherwise it is the same as the ordinary `format` method.
///
Expand Down Expand Up @@ -1207,7 +1249,7 @@ impl NaiveDate {
/// ```
#[inline]
pub fn iter_days(&self) -> NaiveDateDaysIterator {
NaiveDateDaysIterator { value: *self }
NaiveDateDaysIterator { value: Some(*self) }
}

/// Returns an iterator that steps by weeks across all representable dates.
Expand Down Expand Up @@ -1238,7 +1280,7 @@ impl NaiveDate {
/// ```
#[inline]
pub fn iter_weeks(&self) -> NaiveDateWeeksIterator {
NaiveDateWeeksIterator { value: *self }
NaiveDateWeeksIterator { value: Some(*self) }
}

/// Returns the [`NaiveWeek`] that the date belongs to, starting with the [`Weekday`]
Expand Down Expand Up @@ -1695,6 +1737,12 @@ impl Add<Days> for NaiveDate {
}
}

impl AddAssign<Days> for NaiveDate {
fn add_assign(&mut self, days: Days) {
*self = self.checked_add_days(days).unwrap();
}
}

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

Expand All @@ -1703,6 +1751,12 @@ impl Sub<Days> for NaiveDate {
}
}

impl SubAssign<Days> for NaiveDate {
fn sub_assign(&mut self, days: Days) {
*self = 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 @@ -1778,75 +1832,72 @@ impl Sub<NaiveDate> for NaiveDate {
/// Iterator over `NaiveDate` with a step size of one day.
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)]
pub struct NaiveDateDaysIterator {
value: NaiveDate,
value: Option<NaiveDate>,
}

impl Iterator for NaiveDateDaysIterator {
type Item = NaiveDate;

fn next(&mut self) -> Option<Self::Item> {
if self.value == NaiveDate::MAX {
return None;
}
// current < NaiveDate::MAX from here on:
let current = self.value;
// This can't panic because current is < NaiveDate::MAX:
self.value = current.succ_opt().unwrap();
Some(current)
self.value = current?.succ_opt();
current
}

fn size_hint(&self) -> (usize, Option<usize>) {
let exact_size = NaiveDate::MAX.signed_duration_since(self.value).num_days();
(exact_size as usize, Some(exact_size as usize))
match self.value {
Some(v) => {
let exact_size = NaiveDate::MAX.days_since(v).ok().unwrap().0;
(exact_size as usize, Some(exact_size as usize))
}
None => (0, Some(0)),
}
}
}

impl ExactSizeIterator for NaiveDateDaysIterator {}

impl DoubleEndedIterator for NaiveDateDaysIterator {
fn next_back(&mut self) -> Option<Self::Item> {
if self.value == NaiveDate::MIN {
return None;
}
let current = self.value;
self.value = current.pred_opt().unwrap();
Some(current)
self.value = current?.pred_opt();
current
}
}

#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)]
pub struct NaiveDateWeeksIterator {
value: NaiveDate,
value: Option<NaiveDate>,
}

impl Iterator for NaiveDateWeeksIterator {
type Item = NaiveDate;

fn next(&mut self) -> Option<Self::Item> {
if NaiveDate::MAX - self.value < OldDuration::weeks(1) {
return None;
}
let current = self.value;
self.value = current + OldDuration::weeks(1);
Some(current)
self.value = current?.checked_add_days(Days::from_u32(7));
current
}

fn size_hint(&self) -> (usize, Option<usize>) {
let exact_size = NaiveDate::MAX.signed_duration_since(self.value).num_weeks();
(exact_size as usize, Some(exact_size as usize))
match self.value {
Some(v) => {
let exact_size = NaiveDate::MAX.days_since(v).err().unwrap().0 / 7;
(exact_size as usize, Some(exact_size as usize))
}
None => (0, Some(0)),
}
}
}

impl ExactSizeIterator for NaiveDateWeeksIterator {}

impl DoubleEndedIterator for NaiveDateWeeksIterator {
fn next_back(&mut self) -> Option<Self::Item> {
if self.value - NaiveDate::MIN < OldDuration::weeks(1) {
return None;
}
// NaiveDate::MAX.days_since(self.value?).forwards()?;
let current = self.value;
self.value = current - OldDuration::weeks(1);
Some(current)
self.value = current?.checked_sub_days(Days::from_u32(7));
current
}
}

Expand Down Expand Up @@ -2673,22 +2724,22 @@ mod tests {
assert_eq!(lhs.checked_add_days(rhs), sum);
}

check((2014, 1, 1), Days::new(0), Some((2014, 1, 1)));
check((2014, 1, 1), Days::from_u32(0), Some((2014, 1, 1)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility checks, could you add some tests cases (or statements within a test case) to compare Days::new and Days::from_u32 result in the same values?

I know new is the "old way", but wouldn't hurt to make sure it's still working.

// 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((2014, 1, 1), Days::from_u32(1), Some((2014, 1, 2)));
check((2014, 1, 1), Days::from_u32(364), Some((2014, 12, 31)));
check((2014, 1, 1), Days::from_u32(365 * 4 + 1), Some((2018, 1, 1)));
check((2014, 1, 1), Days::from_u32(365 * 400 + 97), Some((2414, 1, 1)));

check((-7, 1, 1), Days::new(365 * 12 + 3), Some((5, 1, 1)));
check((-7, 1, 1), Days::from_u32(365 * 12 + 3), Some((5, 1, 1)));

// overflow check
check(
(0, 1, 1),
Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()),
Days::from_u32(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);
check((0, 1, 1), Days::from_u32(u32::try_from(MAX_DAYS_FROM_YEAR_0).unwrap() + 1), None);
}

#[test]
Expand All @@ -2699,15 +2750,23 @@ mod tests {
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((2014, 1, 1), (2014, 1, 1), Days::from_u32(0));
check((2014, 1, 2), (2014, 1, 1), Days::from_u32(1));
check((2014, 12, 31), (2014, 1, 1), Days::from_u32(364));
check((2015, 1, 3), (2014, 1, 1), Days::from_u32(365 + 2));
check((2018, 1, 1), (2014, 1, 1), Days::from_u32(365 * 4 + 1));
check((2414, 1, 1), (2014, 1, 1), Days::from_u32(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()));
check(
(MAX_YEAR, 12, 31),
(0, 1, 1),
Days::from_u32(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()),
);
check(
(0, 1, 1),
(MIN_YEAR, 1, 1),
Days::from_u32((-MIN_DAYS_FROM_YEAR_0).try_into().unwrap()),
);
}

#[test]
Expand All @@ -2730,6 +2789,26 @@ mod tests {
assert_eq!(date, ymd(2016, 9, 29));
}

#[test]
fn test_days_addassignment() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
let mut date = ymd(2016, 10, 1);
date += Days::from_u32(10);
assert_eq!(date, ymd(2016, 10, 11));
date += Days::from_u32(30);
assert_eq!(date, ymd(2016, 11, 10));
}

#[test]
fn test_days_subassignment() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
let mut date = ymd(2016, 10, 11);
date -= Days::from_u32(10);
assert_eq!(date, ymd(2016, 10, 1));
date -= Days::from_u32(2);
assert_eq!(date, ymd(2016, 9, 29));
}

#[test]
fn test_date_fmt() {
assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(2012, 3, 4).unwrap()), "2012-03-04");
Expand Down Expand Up @@ -2867,22 +2946,22 @@ mod tests {

#[test]
fn test_day_iterator_limit() {
assert_eq!(NaiveDate::from_ymd_opt(262143, 12, 29).unwrap().iter_days().take(4).count(), 2);
assert_eq!(NaiveDate::from_ymd_opt(262143, 12, 29).unwrap().iter_days().take(4).count(), 3);
assert_eq!(
NaiveDate::from_ymd_opt(-262144, 1, 3).unwrap().iter_days().rev().take(4).count(),
2
3
);
}

#[test]
fn test_week_iterator_limit() {
assert_eq!(
NaiveDate::from_ymd_opt(262143, 12, 12).unwrap().iter_weeks().take(4).count(),
2
3
);
assert_eq!(
NaiveDate::from_ymd_opt(-262144, 1, 15).unwrap().iter_weeks().rev().take(4).count(),
2
3
);
}

Expand Down