added `checked_{add,sub}` methods to `[Naive]Date[Time]` types.

- Existing `+` and `-` operators use them, and properly panics with
  a correct error message on overflow/underflow.
This commit is contained in:
Kang Seonghoon 2015-01-29 02:31:18 +09:00
parent 43ee68b522
commit 3d00a0fd5a
5 changed files with 222 additions and 60 deletions

View File

@ -175,6 +175,24 @@ impl<Off:Offset> Date<Off> {
Date::from_utc(self.date, offset)
}
/// Adds given `Duration` to the current date.
///
/// Returns `None` when it will result in overflow.
#[inline]
pub fn checked_add(self, rhs: Duration) -> Option<Date<Off>> {
let date = try_opt!(self.date.checked_add(rhs));
Some(Date { date: date, offset: self.offset })
}
/// Subtracts given `Duration` from the current date.
///
/// Returns `None` when it will result in overflow.
#[inline]
pub fn checked_sub(self, rhs: Duration) -> Option<Date<Off>> {
let date = try_opt!(self.date.checked_sub(rhs));
Some(Date { date: date, offset: self.offset })
}
/// Returns a view to the local date.
fn local(&self) -> NaiveDate {
self.offset.to_local_date(&self.date)
@ -269,14 +287,16 @@ impl<Off: Offset, H: hash::Hasher + hash::Writer> hash::Hash<H> for Date<Off> {
impl<Off:Offset> Add<Duration> for Date<Off> {
type Output = Date<Off>;
#[inline]
fn add(self, rhs: Duration) -> Date<Off> {
Date { date: self.date + rhs, offset: self.offset }
self.checked_add(rhs).expect("`Date + Duration` overflowed")
}
}
impl<Off:Offset, Off2:Offset> Sub<Date<Off2>> for Date<Off> {
type Output = Duration;
#[inline]
fn sub(self, rhs: Date<Off2>) -> Duration { self.date - rhs.date }
}
@ -284,7 +304,9 @@ impl<Off:Offset> Sub<Duration> for Date<Off> {
type Output = Date<Off>;
#[inline]
fn sub(self, rhs: Duration) -> Date<Off> { self.add(-rhs) }
fn sub(self, rhs: Duration) -> Date<Off> {
self.checked_sub(rhs).expect("`Date - Duration` overflowed")
}
}
impl<Off: Offset> fmt::Debug for Date<Off> {

View File

@ -64,6 +64,24 @@ impl<Off:Offset> DateTime<Off> {
DateTime::from_utc(self.datetime, offset)
}
/// Adds given `Duration` to the current date and time.
///
/// Returns `None` when it will result in overflow.
#[inline]
pub fn checked_add(self, rhs: Duration) -> Option<DateTime<Off>> {
let datetime = try_opt!(self.datetime.checked_add(rhs));
Some(DateTime { datetime: datetime, offset: self.offset })
}
/// Subtracts given `Duration` from the current date and time.
///
/// Returns `None` when it will result in overflow.
#[inline]
pub fn checked_sub(self, rhs: Duration) -> Option<DateTime<Off>> {
let datetime = try_opt!(self.datetime.checked_sub(rhs));
Some(DateTime { datetime: datetime, offset: self.offset })
}
/// Returns a view to the local datetime.
fn local(&self) -> NaiveDateTime {
self.offset.to_local_datetime(&self.datetime)
@ -190,14 +208,16 @@ impl<Off: Offset, H: hash::Hasher + hash::Writer> hash::Hash<H> for DateTime<Off
impl<Off:Offset> Add<Duration> for DateTime<Off> {
type Output = DateTime<Off>;
#[inline]
fn add(self, rhs: Duration) -> DateTime<Off> {
DateTime { datetime: self.datetime + rhs, offset: self.offset }
self.checked_add(rhs).expect("`DateTime + Duration` overflowed")
}
}
impl<Off:Offset, Off2:Offset> Sub<DateTime<Off2>> for DateTime<Off> {
type Output = Duration;
#[inline]
fn sub(self, rhs: DateTime<Off2>) -> Duration { self.datetime - rhs.datetime }
}
@ -205,7 +225,9 @@ impl<Off:Offset> Sub<Duration> for DateTime<Off> {
type Output = DateTime<Off>;
#[inline]
fn sub(self, rhs: Duration) -> DateTime<Off> { self.add(-rhs) }
fn sub(self, rhs: Duration) -> DateTime<Off> {
self.checked_sub(rhs).expect("`DateTime - Duration` overflowed")
}
}
impl<Off: Offset> fmt::Debug for DateTime<Off> {

View File

@ -201,6 +201,11 @@ pub use date::Date;
pub use time::Time;
pub use datetime::DateTime;
// useful throughout the codebase
macro_rules! try_opt {
($e:expr) => (match $e { Some(v) => v, None => return None })
}
mod div;
pub mod duration {
//! ISO 8601 duration.

View File

@ -22,6 +22,28 @@ use self::internals::{DateImpl, Of, Mdf, YearFlags};
const MAX_YEAR: i32 = internals::MAX_YEAR as i32;
const MIN_YEAR: i32 = internals::MIN_YEAR as i32;
// MAX_YEAR-12-31 minus 0000-01-01
// = ((MAX_YEAR+1)-01-01 minus 0001-01-01) + (0001-01-01 minus 0000-01-01) - 1 day
// = ((MAX_YEAR+1)-01-01 minus 0001-01-01) + 365 days
// = MAX_YEAR * 365 + (# of leap years from 0001 to MAX_YEAR) + 365 days
#[cfg(test)] // only used for testing
const MAX_DAYS_FROM_YEAR_0: i32 = MAX_YEAR * 365 +
MAX_YEAR / 4 -
MAX_YEAR / 100 +
MAX_YEAR / 400 + 365;
// MIN_YEAR-01-01 minus 0000-01-01
// = (MIN_YEAR+400n+1)-01-01 minus (400n+1)-01-01
// = ((MIN_YEAR+400n+1)-01-01 minus 0001-01-01) - ((400n+1)-01-01 minus 0001-01-01)
// = ((MIN_YEAR+400n+1)-01-01 minus 0001-01-01) - 146097n days
//
// n is set to 1000 for convenience.
#[cfg(test)] // only used for testing
const MIN_DAYS_FROM_YEAR_0: i32 = (MIN_YEAR + 400_000) * 365 +
(MIN_YEAR + 400_000) / 4 -
(MIN_YEAR + 400_000) / 100 +
(MIN_YEAR + 400_000) / 400 - 146097_000;
/// ISO 8601 calendar date without timezone.
/// Allows for every proleptic Gregorian date from Jan 1, 262145 BCE to Dec 31, 262143 CE.
/// Also supports the conversion from ISO 8601 ordinal and week date.
@ -302,6 +324,40 @@ impl NaiveDate {
self.with_of(self.of().pred()).or_else(|| NaiveDate::from_ymd_opt(self.year() - 1, 12, 31))
}
/// Adds the `days` part of given `Duration` to the current date.
///
/// Returns `None` when it will result in overflow.
pub fn checked_add(self, rhs: Duration) -> Option<NaiveDate> {
let year = self.year();
let (mut year_div_400, year_mod_400) = div_mod_floor(year, 400);
let cycle = internals::yo_to_cycle(year_mod_400 as u32, self.of().ordinal());
let cycle = try_opt!((cycle as i32).checked_add(try_opt!(rhs.num_days().to_i32())));
let (cycle_div_400y, cycle) = div_mod_floor(cycle, 146097);
year_div_400 += cycle_div_400y;
let (year_mod_400, ordinal) = internals::cycle_to_yo(cycle as u32);
let flags = YearFlags::from_year_mod_400(year_mod_400 as i32);
NaiveDate::from_of(year_div_400 * 400 + year_mod_400 as i32,
Of::new(ordinal, flags))
}
/// Subtracts the `days` part of given `Duration` from the current date.
///
/// Returns `None` when it will result in overflow.
pub fn checked_sub(self, rhs: Duration) -> Option<NaiveDate> {
let year = self.year();
let (mut year_div_400, year_mod_400) = div_mod_floor(year, 400);
let cycle = internals::yo_to_cycle(year_mod_400 as u32, self.of().ordinal());
let cycle = try_opt!((cycle as i32).checked_sub(try_opt!(rhs.num_days().to_i32())));
let (cycle_div_400y, cycle) = div_mod_floor(cycle, 146097);
year_div_400 += cycle_div_400y;
let (year_mod_400, ordinal) = internals::cycle_to_yo(cycle as u32);
let flags = YearFlags::from_year_mod_400(year_mod_400 as i32);
NaiveDate::from_of(year_div_400 * 400 + year_mod_400 as i32,
Of::new(ordinal, flags))
}
/// Formats the date in the specified format string.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
@ -387,20 +443,9 @@ impl<H: hash::Hasher + hash::Writer> hash::Hash<H> for NaiveDate {
impl Add<Duration> for NaiveDate {
type Output = NaiveDate;
#[inline]
fn add(self, rhs: Duration) -> NaiveDate {
// TODO overflow currently fails
let year = self.year();
let (mut year_div_400, year_mod_400) = div_mod_floor(year, 400);
let cycle = internals::yo_to_cycle(year_mod_400 as u32, self.of().ordinal());
let cycle = (cycle as i32).checked_add(rhs.num_days().to_i32().unwrap()).unwrap();
let (cycle_div_400y, cycle) = div_mod_floor(cycle, 146097);
year_div_400 += cycle_div_400y;
let (year_mod_400, ordinal) = internals::cycle_to_yo(cycle as u32);
let flags = YearFlags::from_year_mod_400(year_mod_400 as i32);
NaiveDate::from_of(year_div_400 * 400 + year_mod_400 as i32,
Of::new(ordinal, flags)).unwrap()
self.checked_add(rhs).expect("`NaiveDate + Duration` overflowed")
}
}
@ -422,7 +467,9 @@ impl Sub<Duration> for NaiveDate {
type Output = NaiveDate;
#[inline]
fn sub(self, rhs: Duration) -> NaiveDate { self.add(-rhs) }
fn sub(self, rhs: Duration) -> NaiveDate {
self.checked_sub(rhs).expect("`NaiveDate - Duration` overflowed")
}
}
impl fmt::Debug for NaiveDate {
@ -452,7 +499,9 @@ impl fmt::Display for NaiveDate {
#[cfg(test)]
mod tests {
use super::{NaiveDate, MIN, MAX};
use super::NaiveDate;
use super::{MIN, MIN_YEAR, MIN_DAYS_FROM_YEAR_0};
use super::{MAX, MAX_YEAR, MAX_DAYS_FROM_YEAR_0};
use {Datelike, Weekday};
use duration::Duration;
use std::{i32, u32};
@ -716,23 +765,32 @@ mod tests {
#[test]
fn test_date_add() {
fn check((y1,m1,d1): (i32, u32, u32), rhs: Duration, (y,m,d): (i32, u32, u32)) {
fn check((y1,m1,d1): (i32, u32, u32), rhs: Duration, ymd: Option<(i32, u32, u32)>) {
let lhs = NaiveDate::from_ymd(y1, m1, d1);
let sum = NaiveDate::from_ymd(y, m, d);
assert_eq!(lhs + rhs, sum);
//assert_eq!(rhs + lhs, sum);
let sum = ymd.map(|(y,m,d)| NaiveDate::from_ymd(y, m, d));
assert_eq!(lhs.checked_add(rhs), sum);
assert_eq!(lhs.checked_sub(-rhs), sum);
}
check((2014, 1, 1), Duration::zero(), (2014, 1, 1));
check((2014, 1, 1), Duration::seconds(86399), (2014, 1, 1));
check((2014, 1, 1), Duration::seconds(-86399), (2014, 1, 1)); // always round towards zero
check((2014, 1, 1), Duration::days(1), (2014, 1, 2));
check((2014, 1, 1), Duration::days(-1), (2013, 12, 31));
check((2014, 1, 1), Duration::days(364), (2014, 12, 31));
check((2014, 1, 1), Duration::days(365*4 + 1), (2018, 1, 1));
check((2014, 1, 1), Duration::days(365*400 + 97), (2414, 1, 1));
check((2014, 1, 1), Duration::zero(), Some((2014, 1, 1)));
check((2014, 1, 1), Duration::seconds(86399), Some((2014, 1, 1)));
// always round towards zero
check((2014, 1, 1), Duration::seconds(-86399), Some((2014, 1, 1)));
check((2014, 1, 1), Duration::days(1), Some((2014, 1, 2)));
check((2014, 1, 1), Duration::days(-1), Some((2013, 12, 31)));
check((2014, 1, 1), Duration::days(364), Some((2014, 12, 31)));
check((2014, 1, 1), Duration::days(365*4 + 1), Some((2018, 1, 1)));
check((2014, 1, 1), Duration::days(365*400 + 97), Some((2414, 1, 1)));
check((-7, 1, 1), Duration::days(365*12 + 3), (5, 1, 1));
check((-7, 1, 1), Duration::days(365*12 + 3), Some((5, 1, 1)));
// overflow check
check((0, 1, 1), Duration::days(MAX_DAYS_FROM_YEAR_0 as i64), Some((MAX_YEAR, 12, 31)));
check((0, 1, 1), Duration::days(MAX_DAYS_FROM_YEAR_0 as i64 + 1), None);
check((0, 1, 1), Duration::max_value(), None);
check((0, 1, 1), Duration::days(MIN_DAYS_FROM_YEAR_0 as i64), Some((MIN_YEAR, 1, 1)));
check((0, 1, 1), Duration::days(MIN_DAYS_FROM_YEAR_0 as i64 - 1), None);
check((0, 1, 1), Duration::min_value(), None);
}
#[test]
@ -750,6 +808,9 @@ mod tests {
check((2015, 1, 3), (2014, 1, 1), Duration::days(365 + 2));
check((2018, 1, 1), (2014, 1, 1), Duration::days(365*4 + 1));
check((2414, 1, 1), (2014, 1, 1), Duration::days(365*400 + 97));
check((MAX_YEAR, 12, 31), (0, 1, 1), Duration::days(MAX_DAYS_FROM_YEAR_0 as i64));
check((MIN_YEAR, 1, 1), (0, 1, 1), Duration::days(MIN_DAYS_FROM_YEAR_0 as i64));
}
#[test]

View File

@ -81,6 +81,50 @@ impl NaiveDateTime {
(ndays - 719163) * 86400 + nseconds
}
/// Adds given `Duration` to the current date and time.
///
/// Returns `None` when it will result in overflow.
pub fn checked_add(self, rhs: Duration) -> Option<NaiveDateTime> {
// Duration does not directly give its parts, so we need some additional calculations.
let days = rhs.num_days();
let nanos = (rhs - Duration::days(days)).num_nanoseconds().unwrap();
debug_assert!(Duration::days(days) + Duration::nanoseconds(nanos) == rhs);
debug_assert!(-86400_000_000_000 < nanos && nanos < 86400_000_000_000);
let mut date = try_opt!(self.date.checked_add(Duration::days(days)));
let time = self.time + Duration::nanoseconds(nanos);
// time always wraps around, but date needs to be adjusted for overflow.
if nanos < 0 && time > self.time {
date = try_opt!(date.pred_opt());
} else if nanos > 0 && time < self.time {
date = try_opt!(date.succ_opt());
}
Some(NaiveDateTime { date: date, time: time })
}
/// Subtracts given `Duration` from the current date and time.
///
/// Returns `None` when it will result in overflow.
pub fn checked_sub(self, rhs: Duration) -> Option<NaiveDateTime> {
// Duration does not directly give its parts, so we need some additional calculations.
let days = rhs.num_days();
let nanos = (rhs - Duration::days(days)).num_nanoseconds().unwrap();
debug_assert!(Duration::days(days) + Duration::nanoseconds(nanos) == rhs);
debug_assert!(-86400_000_000_000 < nanos && nanos < 86400_000_000_000);
let mut date = try_opt!(self.date.checked_sub(Duration::days(days)));
let time = self.time - Duration::nanoseconds(nanos);
// time always wraps around, but date needs to be adjusted for overflow.
if nanos > 0 && time > self.time {
date = try_opt!(date.pred_opt());
} else if nanos < 0 && time < self.time {
date = try_opt!(date.succ_opt());
}
Some(NaiveDateTime { date: date, time: time })
}
/// Formats the combined date and time in the specified format string.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
@ -171,23 +215,9 @@ impl<H: hash::Hasher + hash::Writer> hash::Hash<H> for NaiveDateTime {
impl Add<Duration> for NaiveDateTime {
type Output = NaiveDateTime;
#[inline]
fn add(self, rhs: Duration) -> NaiveDateTime {
// Duration does not directly give its parts, so we need some additional calculations.
let days = rhs.num_days();
let nanos = (rhs - Duration::days(days)).num_nanoseconds().unwrap();
debug_assert!(Duration::days(days) + Duration::nanoseconds(nanos) == rhs);
debug_assert!(-86400_000_000_000 < nanos && nanos < 86400_000_000_000);
let mut date = self.date + Duration::days(days);
let time = self.time + Duration::nanoseconds(nanos);
// time always wraps around, but date needs to be adjusted for overflow.
if nanos < 0 && time > self.time {
date = date.pred();
} else if nanos > 0 && time < self.time {
date = date.succ();
}
NaiveDateTime { date: date, time: time }
self.checked_add(rhs).expect("`NaiveDateTime + Duration` overflowed")
}
}
@ -203,7 +233,9 @@ impl Sub<Duration> for NaiveDateTime {
type Output = NaiveDateTime;
#[inline]
fn sub(self, rhs: Duration) -> NaiveDateTime { self.add(-rhs) }
fn sub(self, rhs: Duration) -> NaiveDateTime {
self.checked_sub(rhs).expect("`NaiveDateTime - Duration` overflowed")
}
}
impl fmt::Debug for NaiveDateTime {
@ -221,7 +253,9 @@ impl fmt::Display for NaiveDateTime {
#[cfg(test)]
mod tests {
use super::NaiveDateTime;
use Datelike;
use duration::Duration;
use naive::date as naive_date;
use naive::date::NaiveDate;
use std::i64;
@ -240,17 +274,35 @@ mod tests {
#[test]
fn test_datetime_add() {
let ymdhms = |&: y,m,d,h,n,s| NaiveDate::from_ymd(y,m,d).and_hms(h,n,s);
assert_eq!(ymdhms(2014, 5, 6, 7, 8, 9) + Duration::seconds(3600 + 60 + 1),
ymdhms(2014, 5, 6, 8, 9, 10));
assert_eq!(ymdhms(2014, 5, 6, 7, 8, 9) + Duration::seconds(-(3600 + 60 + 1)),
ymdhms(2014, 5, 6, 6, 7, 8));
assert_eq!(ymdhms(2014, 5, 6, 7, 8, 9) + Duration::seconds(86399),
ymdhms(2014, 5, 7, 7, 8, 8));
assert_eq!(ymdhms(2014, 5, 6, 7, 8, 9) + Duration::seconds(86400 * 10),
ymdhms(2014, 5, 16, 7, 8, 9));
assert_eq!(ymdhms(2014, 5, 6, 7, 8, 9) + Duration::seconds(-86400 * 10),
ymdhms(2014, 4, 26, 7, 8, 9));
fn check((y,m,d,h,n,s): (i32,u32,u32,u32,u32,u32), rhs: Duration,
result: Option<(i32,u32,u32,u32,u32,u32)>) {
let lhs = NaiveDate::from_ymd(y, m, d).and_hms(h, n, s);
let sum = result.map(|(y,m,d,h,n,s)| NaiveDate::from_ymd(y, m, d).and_hms(h, n, s));
assert_eq!(lhs.checked_add(rhs), sum);
assert_eq!(lhs.checked_sub(-rhs), sum);
};
check((2014,5,6, 7,8,9), Duration::seconds(3600 + 60 + 1), Some((2014,5,6, 8,9,10)));
check((2014,5,6, 7,8,9), Duration::seconds(-(3600 + 60 + 1)), Some((2014,5,6, 6,7,8)));
check((2014,5,6, 7,8,9), Duration::seconds(86399), Some((2014,5,7, 7,8,8)));
check((2014,5,6, 7,8,9), Duration::seconds(86400 * 10), Some((2014,5,16, 7,8,9)));
check((2014,5,6, 7,8,9), Duration::seconds(-86400 * 10), Some((2014,4,26, 7,8,9)));
check((2014,5,6, 7,8,9), Duration::seconds(86400 * 10), Some((2014,5,16, 7,8,9)));
// overflow check
// assumes that we have correct values for MAX/MIN_DAYS_FROM_YEAR_0 from `naive::date`.
// (they are private constants, but the equivalence is tested in that module.)
let max_days_from_year_0 = naive_date::MAX - NaiveDate::from_ymd(0,1,1);
check((0,1,1, 0,0,0), max_days_from_year_0, Some((naive_date::MAX.year(),12,31, 0,0,0)));
check((0,1,1, 0,0,0), max_days_from_year_0 + Duration::seconds(86399),
Some((naive_date::MAX.year(),12,31, 23,59,59)));
check((0,1,1, 0,0,0), max_days_from_year_0 + Duration::seconds(86400), None);
check((0,1,1, 0,0,0), Duration::max_value(), None);
let min_days_from_year_0 = naive_date::MIN - NaiveDate::from_ymd(0,1,1);
check((0,1,1, 0,0,0), min_days_from_year_0, Some((naive_date::MIN.year(),1,1, 0,0,0)));
check((0,1,1, 0,0,0), min_days_from_year_0 - Duration::seconds(1), None);
check((0,1,1, 0,0,0), Duration::min_value(), None);
}
#[test]