Skip to content
8 changes: 6 additions & 2 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::naive::{Days, IsoWeek, NaiveDate, NaiveDateTime, NaiveTime};
#[cfg(feature = "clock")]
use crate::offset::Local;
use crate::offset::{FixedOffset, Offset, TimeZone, Utc};
use crate::try_opt;
#[allow(deprecated)]
use crate::Date;
use crate::{Datelike, Months, Timelike, Weekday};
Expand Down Expand Up @@ -626,8 +627,11 @@ impl DateTime<Utc> {
/// ```
#[inline]
#[must_use]
pub fn from_timestamp(secs: i64, nsecs: u32) -> Option<Self> {
NaiveDateTime::from_timestamp_opt(secs, nsecs).as_ref().map(NaiveDateTime::and_utc)
pub const fn from_timestamp(secs: i64, nsecs: u32) -> Option<Self> {
Some(DateTime {
datetime: try_opt!(NaiveDateTime::from_timestamp_opt(secs, nsecs)),
offset: Utc,
})
}

/// Makes a new [`DateTime<Utc>`] from the number of non-leap milliseconds
Expand Down
182 changes: 110 additions & 72 deletions src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use core::{fmt, i64};
#[cfg(feature = "std")]
use std::error::Error;

use crate::{expect, try_opt};

#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
use rkyv::{Archive, Deserialize, Serialize};

Expand All @@ -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 negative durations; see individual methods for details.
Expand Down Expand Up @@ -82,6 +75,23 @@ 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<Duration> {
if secs < MIN.secs
|| secs > MAX.secs
|| nanos > 1_000_000_000
Copy link

Choose a reason for hiding this comment

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

The functions documentation says >= 1_000_000_000, but here > 1_000_000_000 is used in the check?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for catching that!

|| (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 the given number of weeks.
///
/// Equivalent to `Duration::seconds(weeks * 7 * 24 * 60 * 60)` with
Expand All @@ -92,8 +102,8 @@ impl Duration {
/// 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 the given number of weeks.
Expand All @@ -105,8 +115,8 @@ impl Duration {
///
/// Returns `None` when the duration is out of bounds.
#[inline]
pub fn try_weeks(weeks: i64) -> Option<Duration> {
weeks.checked_mul(SECS_PER_WEEK).and_then(Duration::try_seconds)
pub const fn try_weeks(weeks: i64) -> Option<Duration> {
Duration::try_seconds(try_opt!(weeks.checked_mul(SECS_PER_WEEK)))
}

/// Makes a new `Duration` with the given number of days.
Expand All @@ -119,8 +129,8 @@ 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 the given number of days.
Expand All @@ -132,8 +142,8 @@ impl Duration {
///
/// Returns `None` when the duration is out of bounds.
#[inline]
pub fn try_days(days: i64) -> Option<Duration> {
days.checked_mul(SECS_PER_DAY).and_then(Duration::try_seconds)
pub const fn try_days(days: i64) -> Option<Duration> {
Duration::try_seconds(try_opt!(days.checked_mul(SECS_PER_DAY)))
}

/// Makes a new `Duration` with the given number of hours.
Expand All @@ -145,8 +155,8 @@ 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 out of bounds")
pub const fn hours(hours: i64) -> Duration {
expect!(Duration::try_hours(hours), "Duration::hours out of bounds")
}

/// Makes a new `Duration` with the given number of hours.
Expand All @@ -157,8 +167,8 @@ impl Duration {
///
/// Returns `None` when the duration is out of bounds.
#[inline]
pub fn try_hours(hours: i64) -> Option<Duration> {
hours.checked_mul(SECS_PER_HOUR).and_then(Duration::try_seconds)
pub const fn try_hours(hours: i64) -> Option<Duration> {
Duration::try_seconds(try_opt!(hours.checked_mul(SECS_PER_HOUR)))
}

/// Makes a new `Duration` with the given number of minutes.
Expand All @@ -170,8 +180,8 @@ 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 the given number of minutes.
Expand All @@ -182,8 +192,8 @@ impl Duration {
///
/// Returns `None` when the duration is out of bounds.
#[inline]
pub fn try_minutes(minutes: i64) -> Option<Duration> {
minutes.checked_mul(SECS_PER_MINUTE).and_then(Duration::try_seconds)
pub const fn try_minutes(minutes: i64) -> Option<Duration> {
Duration::try_seconds(try_opt!(minutes.checked_mul(SECS_PER_MINUTE)))
}

/// Makes a new `Duration` with the given number of seconds.
Expand All @@ -196,8 +206,8 @@ impl Duration {
/// rounding).
#[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 the given number of seconds.
Expand All @@ -208,12 +218,8 @@ impl Duration {
/// or less than `-i64::MAX / 1_000` seconds (in this context, this is the
/// same as `i64::MIN / 1_000` due to rounding).
#[inline]
pub fn try_seconds(seconds: i64) -> Option<Duration> {
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> {
Duration::new(seconds, 0)
}

/// Makes a new `Duration` with the given number of milliseconds.
Expand All @@ -224,8 +230,8 @@ impl Duration {
/// more than `i64::MAX` milliseconds or less than `-i64::MAX` milliseconds.
/// Notably, this is not the same as `i64::MIN`.
#[inline]
pub fn milliseconds(milliseconds: i64) -> Duration {
Duration::try_milliseconds(milliseconds).expect("Duration::milliseconds out of bounds")
pub const fn milliseconds(milliseconds: i64) -> Duration {
expect!(Duration::try_milliseconds(milliseconds), "Duration::milliseconds out of bounds")
}

/// Makes a new `Duration` with the given number of milliseconds.
Expand All @@ -236,14 +242,14 @@ impl Duration {
/// less than `-i64::MAX` milliseconds. Notably, this is not the same as
/// `i64::MIN`.
#[inline]
pub fn try_milliseconds(milliseconds: i64) -> Option<Duration> {
let (secs, millis) = div_mod_floor_64(milliseconds, MILLIS_PER_SEC);
let d = Duration { secs, nanos: millis as i32 * NANOS_PER_MILLI };
pub const fn try_milliseconds(milliseconds: i64) -> Option<Duration> {
// We don't need to compare against MAX, as this function accepts an
// i64, and MAX is aligned to i64::MAX milliseconds.
if d < MIN {
if milliseconds < -i64::MAX {
return None;
}
let (secs, millis) = div_mod_floor_64(milliseconds, MILLIS_PER_SEC);
let d = Duration { secs, nanos: millis as i32 * NANOS_PER_MILLI };
Some(d)
}

Expand Down Expand Up @@ -344,40 +350,30 @@ impl Duration {

/// Add two `Duration`s, returning `None` if overflow occurred.
#[must_use]
pub fn checked_add(&self, rhs: &Duration) -> Option<Duration> {
let mut secs = try_opt!(self.secs.checked_add(rhs.secs));
pub const fn checked_add(&self, rhs: &Duration) -> Option<Duration> {
// No overflow checks here because we stay comfortably within 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 `Duration`s, returning `None` if overflow occurred.
#[must_use]
pub fn checked_sub(&self, rhs: &Duration) -> Option<Duration> {
let mut secs = try_opt!(self.secs.checked_sub(rhs.secs));
pub const fn checked_sub(&self, rhs: &Duration) -> Option<Duration> {
// No overflow checks here because we stay comfortably within 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.
Expand Down Expand Up @@ -418,40 +414,48 @@ 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<Duration, OutOfRangeError> {
pub const fn from_std(duration: StdDuration) -> Result<Duration, OutOfRangeError> {
// 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.
pub fn to_std(&self) -> Result<StdDuration, OutOfRangeError> {
pub const fn to_std(&self) -> Result<StdDuration, OutOfRangeError> {
if self.secs < 0 {
return Err(OutOfRangeError(()));
}
Ok(StdDuration::new(self.secs as u64, self.nanos as u32))
}

/// This duplicates `Neg::neg` because trait methods can't be const yet.
pub(crate) const fn neg(self) -> Duration {
let (secs_diff, nanos) = match self.nanos {
0 => (0, 0),
nanos => (1, NANOS_PER_SEC - nanos),
};
Duration { secs: -self.secs - secs_diff, nanos }
}
}

impl Neg for Duration {
type Output = Duration;

#[inline]
fn neg(self) -> Duration {
if self.nanos == 0 {
Duration { secs: -self.secs, nanos: 0 }
} else {
Duration { secs: -self.secs - 1, nanos: NANOS_PER_SEC - self.nanos }
}
let (secs_diff, nanos) = match self.nanos {
0 => (0, 0),
nanos => (1, NANOS_PER_SEC - nanos),
};
Duration { secs: -self.secs - secs_diff, nanos }
}
}

Expand Down Expand Up @@ -1148,6 +1152,40 @@ mod tests {
);
}

#[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)
);
}

#[test]
#[cfg(feature = "rkyv-validation")]
fn test_rkyv_validation() {
Expand Down
Loading