diff --git a/src/duration.rs b/src/duration.rs index 1e5eb67512..af1d73026a 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -16,6 +16,8 @@ use core::{fmt, i64}; #[cfg(feature = "std")] use std::error::Error; +use crate::{expect, try_opt}; + #[cfg(feature = "rkyv")] use rkyv::{Archive, Deserialize, Serialize}; @@ -38,15 +40,6 @@ const SECS_PER_DAY: i64 = 86_400; /// The number of (non-leap) seconds in a week. const SECS_PER_WEEK: i64 = 604_800; -macro_rules! try_opt { - ($e:expr) => { - match $e { - Some(v) => v, - None => return None, - } - }; -} - /// ISO 8601 time duration with nanosecond precision. /// /// This also allows for the negative duration; see individual methods for details. @@ -74,21 +67,38 @@ pub(crate) const MAX: Duration = Duration { }; impl Duration { + /// Makes a new `Duration` with given number of seconds and nanoseconds. + /// + /// # Errors + /// + /// Returns `None` when the duration is out of bounds, or if `nanos` ≥ 1,000,000,000. + pub(crate) const fn new(secs: i64, nanos: u32) -> Option { + if secs < MIN.secs + || secs > MAX.secs + || nanos > 1_000_000_000 + || (secs == MAX.secs && nanos > MAX.nanos as u32) + || (secs == MIN.secs && nanos < MIN.nanos as u32) + { + return None; + } + Some(Duration { secs, nanos: nanos as i32 }) + } + /// Makes a new `Duration` with given number of weeks. /// Equivalent to `Duration::seconds(weeks * 7 * 24 * 60 * 60)` with overflow checks. /// Panics when the duration is out of bounds. #[inline] #[must_use] - pub fn weeks(weeks: i64) -> Duration { - Duration::try_weeks(weeks).expect("Duration::weeks out of bounds") + pub const fn weeks(weeks: i64) -> Duration { + expect!(Duration::try_weeks(weeks), "Duration::weeks out of bounds") } /// Makes a new `Duration` with given number of weeks. /// Equivalent to `Duration::seconds(weeks * 7 * 24 * 60 * 60)` with overflow checks. /// Returns `None` when the duration is out of bounds. #[inline] - pub fn try_weeks(weeks: i64) -> Option { - weeks.checked_mul(SECS_PER_WEEK).and_then(Duration::try_seconds) + pub const fn try_weeks(weeks: i64) -> Option { + Duration::try_seconds(try_opt!(weeks.checked_mul(SECS_PER_WEEK))) } /// Makes a new `Duration` with given number of days. @@ -96,16 +106,16 @@ impl Duration { /// Panics when the duration is out of bounds. #[inline] #[must_use] - pub fn days(days: i64) -> Duration { - Duration::try_days(days).expect("Duration::days out of bounds") + pub const fn days(days: i64) -> Duration { + expect!(Duration::try_days(days), "Duration::days out of bounds") } /// Makes a new `Duration` with given number of days. /// Equivalent to `Duration::seconds(days * 24 * 60 * 60)` with overflow checks. /// Returns `None` when the duration is out of bounds. #[inline] - pub fn try_days(days: i64) -> Option { - days.checked_mul(SECS_PER_DAY).and_then(Duration::try_seconds) + pub const fn try_days(days: i64) -> Option { + Duration::try_seconds(try_opt!(days.checked_mul(SECS_PER_DAY))) } /// Makes a new `Duration` with given number of hours. @@ -113,16 +123,16 @@ impl Duration { /// Panics when the duration is out of bounds. #[inline] #[must_use] - pub fn hours(hours: i64) -> Duration { - Duration::try_hours(hours).expect("Duration::hours ouf of bounds") + pub const fn hours(hours: i64) -> Duration { + expect!(Duration::try_hours(hours), "Duration::hours ouf of bounds") } /// Makes a new `Duration` with given number of hours. /// Equivalent to `Duration::seconds(hours * 60 * 60)` with overflow checks. /// Returns `None` when the duration is out of bounds. #[inline] - pub fn try_hours(hours: i64) -> Option { - hours.checked_mul(SECS_PER_HOUR).and_then(Duration::try_seconds) + pub const fn try_hours(hours: i64) -> Option { + Duration::try_seconds(try_opt!(hours.checked_mul(SECS_PER_HOUR))) } /// Makes a new `Duration` with given number of minutes. @@ -130,16 +140,16 @@ impl Duration { /// Panics when the duration is out of bounds. #[inline] #[must_use] - pub fn minutes(minutes: i64) -> Duration { - Duration::try_minutes(minutes).expect("Duration::minutes out of bounds") + pub const fn minutes(minutes: i64) -> Duration { + expect!(Duration::try_minutes(minutes), "Duration::minutes out of bounds") } /// Makes a new `Duration` with given number of minutes. /// Equivalent to `Duration::seconds(minutes * 60)` with overflow checks. /// Returns `None` when the duration is out of bounds. #[inline] - pub fn try_minutes(minutes: i64) -> Option { - minutes.checked_mul(SECS_PER_MINUTE).and_then(Duration::try_seconds) + pub const fn try_minutes(minutes: i64) -> Option { + Duration::try_seconds(try_opt!(minutes.checked_mul(SECS_PER_MINUTE))) } /// Makes a new `Duration` with given number of seconds. @@ -147,20 +157,16 @@ impl Duration { /// or less than `-i64::MAX` milliseconds. #[inline] #[must_use] - pub fn seconds(seconds: i64) -> Duration { - Duration::try_seconds(seconds).expect("Duration::seconds out of bounds") + pub const fn seconds(seconds: i64) -> Duration { + expect!(Duration::try_seconds(seconds), "Duration::seconds out of bounds") } /// Makes a new `Duration` with given number of seconds. /// Returns `None` when the duration is more than `i64::MAX` milliseconds /// or less than `-i64::MAX` milliseconds. #[inline] - pub fn try_seconds(seconds: i64) -> Option { - let d = Duration { secs: seconds, nanos: 0 }; - if d < MIN || d > MAX { - return None; - } - Some(d) + pub const fn try_seconds(seconds: i64) -> Option { + Duration::new(seconds, 0) } /// Makes a new `Duration` with given number of milliseconds. @@ -257,40 +263,30 @@ impl Duration { /// Add two durations, returning `None` if overflow occurred. #[must_use] - pub fn checked_add(&self, rhs: &Duration) -> Option { - let mut secs = try_opt!(self.secs.checked_add(rhs.secs)); + pub const fn checked_add(&self, rhs: &Duration) -> Option { + // No overflow checks here because we stay comfortably wihtin the range of an `i64`. + // Range checks happen in `Duration::new`. + let mut secs = self.secs + rhs.secs; let mut nanos = self.nanos + rhs.nanos; if nanos >= NANOS_PER_SEC { nanos -= NANOS_PER_SEC; - secs = try_opt!(secs.checked_add(1)); - } - let d = Duration { secs, nanos }; - // Even if d is within the bounds of i64 seconds, - // it might still overflow i64 milliseconds. - if d < MIN || d > MAX { - None - } else { - Some(d) + secs += 1; } + Duration::new(secs, nanos as u32) } /// Subtract two durations, returning `None` if overflow occurred. #[must_use] - pub fn checked_sub(&self, rhs: &Duration) -> Option { - let mut secs = try_opt!(self.secs.checked_sub(rhs.secs)); + pub const fn checked_sub(&self, rhs: &Duration) -> Option { + // No overflow checks here because we stay comfortably wihtin the range of an `i64`. + // Range checks happen in `Duration::new`. + let mut secs = self.secs - rhs.secs; let mut nanos = self.nanos - rhs.nanos; if nanos < 0 { nanos += NANOS_PER_SEC; - secs = try_opt!(secs.checked_sub(1)); - } - let d = Duration { secs, nanos }; - // Even if d is within the bounds of i64 seconds, - // it might still overflow i64 milliseconds. - if d < MIN || d > MAX { - None - } else { - Some(d) + secs -= 1; } + Duration::new(secs, nanos as u32) } /// Returns the duration as an absolute (non-negative) value. @@ -331,22 +327,22 @@ impl Duration { /// /// This function errors when original duration is larger than the maximum /// value supported for this type. - pub fn from_std(duration: StdDuration) -> Result { + pub const fn from_std(duration: StdDuration) -> Result { // We need to check secs as u64 before coercing to i64 if duration.as_secs() > MAX.secs as u64 { return Err(OutOfRangeError(())); } - let d = Duration { secs: duration.as_secs() as i64, nanos: duration.subsec_nanos() as i32 }; - if d > MAX { - return Err(OutOfRangeError(())); + match Duration::new(duration.as_secs() as i64, duration.subsec_nanos()) { + Some(d) => Ok(d), + None => Err(OutOfRangeError(())), } - Ok(d) } /// Creates a `std::time::Duration` object from `time::Duration` /// /// This function errors when duration is less than zero. As standard /// library implementation is limited to non-negative values. + // TODO: make this method const once our MSRV is 1.58+ pub fn to_std(&self) -> Result { if self.secs < 0 { return Err(OutOfRangeError(())); @@ -795,4 +791,38 @@ mod tests { Err(OutOfRangeError(())) ); } + + #[test] + fn test_duration_const() { + const ONE_WEEK: Duration = Duration::weeks(1); + const ONE_DAY: Duration = Duration::days(1); + const ONE_HOUR: Duration = Duration::hours(1); + const ONE_MINUTE: Duration = Duration::minutes(1); + const ONE_SECOND: Duration = Duration::seconds(1); + const ONE_MILLI: Duration = Duration::milliseconds(1); + const ONE_MICRO: Duration = Duration::microseconds(1); + const ONE_NANO: Duration = Duration::nanoseconds(1); + let combo: Duration = ONE_WEEK + + ONE_DAY + + ONE_HOUR + + ONE_MINUTE + + ONE_SECOND + + ONE_MILLI + + ONE_MICRO + + ONE_NANO; + + assert!(ONE_WEEK != Duration::zero()); + assert!(ONE_DAY != Duration::zero()); + assert!(ONE_HOUR != Duration::zero()); + assert!(ONE_MINUTE != Duration::zero()); + assert!(ONE_SECOND != Duration::zero()); + assert!(ONE_MILLI != Duration::zero()); + assert!(ONE_MICRO != Duration::zero()); + assert!(ONE_NANO != Duration::zero()); + assert_eq!( + combo, + Duration::seconds(86400 * 7 + 86400 + 3600 + 60 + 1) + + Duration::nanoseconds(1 + 1_000 + 1_000_000) + ); + } } diff --git a/src/naive/time/mod.rs b/src/naive/time/mod.rs index de29fa465e..e05bc99eda 100644 --- a/src/naive/time/mod.rs +++ b/src/naive/time/mod.rs @@ -1194,7 +1194,7 @@ impl Add for NaiveTime { // overflow during the conversion to `chrono::Duration`. // But we limit to double that just in case `self` is a leap-second. let secs = rhs.as_secs() % (2 * 24 * 60 * 60); - let d = OldDuration::from_std(Duration::new(secs, rhs.subsec_nanos())).unwrap(); + let d = OldDuration::new(secs as i64, rhs.subsec_nanos()).unwrap(); self.overflowing_add_signed(d).0 } } @@ -1304,7 +1304,7 @@ impl Sub for NaiveTime { // overflow during the conversion to `chrono::Duration`. // But we limit to double that just in case `self` is a leap-second. let secs = rhs.as_secs() % (2 * 24 * 60 * 60); - let d = OldDuration::from_std(Duration::new(secs, rhs.subsec_nanos())).unwrap(); + let d = OldDuration::new(secs as i64, rhs.subsec_nanos()).unwrap(); self.overflowing_sub_signed(d).0 } }