From 3d00a0fd5a83af04e0c4afa94576f2900a18b92b Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Thu, 29 Jan 2015 02:31:18 +0900 Subject: [PATCH] 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. --- src/date.rs | 26 +++++++++- src/datetime.rs | 26 +++++++++- src/lib.rs | 5 ++ src/naive/date.rs | 117 ++++++++++++++++++++++++++++++++---------- src/naive/datetime.rs | 108 ++++++++++++++++++++++++++++---------- 5 files changed, 222 insertions(+), 60 deletions(-) diff --git a/src/date.rs b/src/date.rs index 4d6d0fe..7498f86 100644 --- a/src/date.rs +++ b/src/date.rs @@ -175,6 +175,24 @@ impl Date { 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> { + 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> { + 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 hash::Hash for Date { impl Add for Date { type Output = Date; + #[inline] fn add(self, rhs: Duration) -> Date { - Date { date: self.date + rhs, offset: self.offset } + self.checked_add(rhs).expect("`Date + Duration` overflowed") } } impl Sub> for Date { type Output = Duration; + #[inline] fn sub(self, rhs: Date) -> Duration { self.date - rhs.date } } @@ -284,7 +304,9 @@ impl Sub for Date { type Output = Date; #[inline] - fn sub(self, rhs: Duration) -> Date { self.add(-rhs) } + fn sub(self, rhs: Duration) -> Date { + self.checked_sub(rhs).expect("`Date - Duration` overflowed") + } } impl fmt::Debug for Date { diff --git a/src/datetime.rs b/src/datetime.rs index 18070b6..007a10b 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -64,6 +64,24 @@ impl DateTime { 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> { + 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> { + 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 hash::Hash for DateTime Add for DateTime { type Output = DateTime; + #[inline] fn add(self, rhs: Duration) -> DateTime { - DateTime { datetime: self.datetime + rhs, offset: self.offset } + self.checked_add(rhs).expect("`DateTime + Duration` overflowed") } } impl Sub> for DateTime { type Output = Duration; + #[inline] fn sub(self, rhs: DateTime) -> Duration { self.datetime - rhs.datetime } } @@ -205,7 +225,9 @@ impl Sub for DateTime { type Output = DateTime; #[inline] - fn sub(self, rhs: Duration) -> DateTime { self.add(-rhs) } + fn sub(self, rhs: Duration) -> DateTime { + self.checked_sub(rhs).expect("`DateTime - Duration` overflowed") + } } impl fmt::Debug for DateTime { diff --git a/src/lib.rs b/src/lib.rs index 7221f5d..8218b0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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. diff --git a/src/naive/date.rs b/src/naive/date.rs index 9f89199..ff5fcc1 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -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 { + 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 { + 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 hash::Hash for NaiveDate { impl Add 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 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] diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index e9856a8..ced799f 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -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 { + // 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 { + // 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 hash::Hash for NaiveDateTime { impl Add 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 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]