Skip to content

Commit

Permalink
Make DurationRound fallible and fix method naming
Browse files Browse the repository at this point in the history
Addresses first two review suggestions from
#445 (review)
  • Loading branch information
robyoung committed Jul 4, 2020
1 parent 540ad01 commit bbeb0cd
Showing 1 changed file with 127 additions and 47 deletions.
174 changes: 127 additions & 47 deletions src/round.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See README.md and LICENSE.txt for details.

use core::cmp::Ordering;
use core::fmt;
use core::ops::{Add, Sub};
use datetime::DateTime;
use oldtime::Duration;
Expand Down Expand Up @@ -95,23 +96,26 @@ fn span_for_digits(digits: u16) -> u32 {
/// [`DateTime::timestamp_nanos`]. This means that they will fail if either the
/// `Duration` or the `DateTime` are too big to represented as nanoseconds. They
/// will also fail if the `Duration` is bigger than the timestamp.
pub trait DurationRound {
pub trait DurationRound: core::marker::Sized {
/// Error that can occur in rounding or truncating
type Err: core::error::Error;

/// Return a copy rounded by Duration.
///
/// # Example
/// ``` rust
/// # use chrono::{DateTime, DurationRound, Duration, TimeZone, Utc};
/// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154);
/// assert_eq!(
/// dt.round_duration(Duration::milliseconds(10)).to_string(),
/// dt.duration_round(Duration::milliseconds(10)).unwrap().to_string(),
/// "2018-01-11 12:00:00.150 UTC"
/// );
/// assert_eq!(
/// dt.round_duration(Duration::days(1)).to_string(),
/// dt.duration_round(Duration::days(1)).unwrap().to_string(),
/// "2018-01-12 00:00:00 UTC"
/// );
/// ```
fn round_duration(self, duration: Duration) -> Self;
fn duration_round(self, duration: Duration) -> Result<Self, Self::Err>;

/// Return a copy truncated by Duration.
///
Expand All @@ -120,65 +124,102 @@ pub trait DurationRound {
/// # use chrono::{DateTime, DurationRound, Duration, TimeZone, Utc};
/// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154);
/// assert_eq!(
/// dt.trunc_duration(Duration::milliseconds(10)).to_string(),
/// dt.duration_trunc(Duration::milliseconds(10)).unwrap().to_string(),
/// "2018-01-11 12:00:00.150 UTC"
/// );
/// assert_eq!(
/// dt.trunc_duration(Duration::days(1)).to_string(),
/// dt.duration_trunc(Duration::days(1)).unwrap().to_string(),
/// "2018-01-11 00:00:00 UTC"
/// );
/// ```
fn trunc_duration(self, duration: Duration) -> Self;
fn duration_trunc(self, duration: Duration) -> Result<Self, Self::Err>;
}

impl<Tz: TimeZone> DurationRound for DateTime<Tz> {
fn round_duration(self, duration: Duration) -> Self {
type Err = RoundingError;

fn duration_round(self, duration: Duration) -> Result<Self, Self::Err> {
if let Some(span) = duration.num_nanoseconds() {
if self.timestamp().abs() > 9_223_372_036 {
return Err(RoundingError::TimestampExceedsLimit);
}
let stamp = self.timestamp_nanos();
if span > stamp.abs() {
panic!("duration in nanoseconds is bigger than timestamp");
return Err(RoundingError::DurationExceedsTimestamp);
}
let delta_down = stamp % span;
if delta_down == 0 {
self
Ok(self)
} else {
let (delta_up, delta_down) = if delta_down < 0 {
(delta_down.abs(), span - delta_down.abs())
} else {
(span - delta_down, delta_down)
};
if delta_up <= delta_down {
self + Duration::nanoseconds(delta_up)
Ok(self + Duration::nanoseconds(delta_up))
} else {
self - Duration::nanoseconds(delta_down)
Ok(self - Duration::nanoseconds(delta_down))
}
}
} else {
panic!("duration exceeds num_nanoseconds limit");
Err(RoundingError::DurationExceedsLimit)
}
}

fn trunc_duration(self, duration: Duration) -> Self {
fn duration_trunc(self, duration: Duration) -> Result<Self, Self::Err> {
if let Some(span) = duration.num_nanoseconds() {
if self.timestamp().abs() > 9_223_372_036 {
return Err(RoundingError::TimestampExceedsLimit);
}
let stamp = self.timestamp_nanos();
if span > stamp.abs() {
panic!("duration in nanoseconds is bigger than timestamp");
return Err(RoundingError::DurationExceedsTimestamp);
}
let delta_down = stamp % span;
match delta_down.cmp(&0) {
Ordering::Equal => self,
Ordering::Greater => self - Duration::nanoseconds(delta_down),
Ordering::Less => self - Duration::nanoseconds(span - delta_down.abs()),
Ordering::Equal => Ok(self),
Ordering::Greater => Ok(self - Duration::nanoseconds(delta_down)),
Ordering::Less => Ok(self - Duration::nanoseconds(span - delta_down.abs())),
}
} else {
panic!("duration exceeds num_nanoseconds limit");
Err(RoundingError::DurationExceedsLimit)
}
}
}

/// An error from rounding by `Duration`
///
/// See: [`DurationRound`]
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum RoundingError {
DurationExceedsTimestamp,
DurationExceedsLimit,
TimestampExceedsLimit,
}

impl fmt::Display for RoundingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RoundingError::DurationExceedsTimestamp => {
write!(f, "duration in nanoseconds exceeds timestamp")
}
RoundingError::DurationExceedsLimit => {
write!(f, "duration exceeds num_nanoseconds limit")
}
RoundingError::TimestampExceedsLimit => {
write!(f, "timestamp exceeds num_nanoseconds limit")
}
}
}
}

#[cfg(any(feature = "std", test))]
impl std::error::Error for RoundingError {}

#[cfg(test)]
mod tests {
use super::{Duration, DurationRound, SubsecRound};
use super::{Duration, DurationRound, RoundingError, SubsecRound};
use offset::{FixedOffset, TimeZone, Utc};
use Timelike;

Expand Down Expand Up @@ -269,72 +310,111 @@ mod tests {
}

#[test]
fn test_round_duration() {
fn test_duration_round() {
let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_75_500_000);

assert_eq!(
dt.round_duration(Duration::milliseconds(10)).to_string(),
dt.duration_round(Duration::milliseconds(10)).unwrap().to_string(),
"2016-12-31 23:59:59.180 UTC"
);

let dt = Utc.ymd(2012, 12, 12).and_hms(18, 24, 12);
assert_eq!(dt.round_duration(Duration::minutes(5)).to_string(), "2012-12-12 18:25:00 UTC");
assert_eq!(dt.round_duration(Duration::minutes(10)).to_string(), "2012-12-12 18:20:00 UTC");
assert_eq!(dt.round_duration(Duration::minutes(30)).to_string(), "2012-12-12 18:30:00 UTC");
assert_eq!(dt.round_duration(Duration::hours(1)).to_string(), "2012-12-12 18:00:00 UTC");
assert_eq!(dt.round_duration(Duration::days(1)).to_string(), "2012-12-13 00:00:00 UTC");
assert_eq!(
dt.duration_round(Duration::minutes(5)).unwrap().to_string(),
"2012-12-12 18:25:00 UTC"
);
assert_eq!(
dt.duration_round(Duration::minutes(10)).unwrap().to_string(),
"2012-12-12 18:20:00 UTC"
);
assert_eq!(
dt.duration_round(Duration::minutes(30)).unwrap().to_string(),
"2012-12-12 18:30:00 UTC"
);
assert_eq!(
dt.duration_round(Duration::hours(1)).unwrap().to_string(),
"2012-12-12 18:00:00 UTC"
);
assert_eq!(
dt.duration_round(Duration::days(1)).unwrap().to_string(),
"2012-12-13 00:00:00 UTC"
);
}

#[test]
fn test_round_duration_pre_epoch() {
fn test_duration_round_pre_epoch() {
let dt = Utc.ymd(1969, 12, 12).and_hms(12, 12, 12);
assert_eq!(dt.round_duration(Duration::minutes(10)).to_string(), "1969-12-12 12:10:00 UTC");
assert_eq!(
dt.duration_round(Duration::minutes(10)).unwrap().to_string(),
"1969-12-12 12:10:00 UTC"
);
}

#[test]
#[should_panic]
fn test_round_duration_fails_when_duration_exceeds_nanosecond_limit() {
fn test_duration_round_fails_when_duration_exceeds_nanosecond_limit() {
let dt = Utc.ymd(2260, 12, 31).and_hms_nano(23, 59, 59, 1_75_500_000);

dt.round_duration(Duration::days(300 * 365));
assert_eq!(
dt.duration_round(Duration::days(300 * 365)),
Err(RoundingError::DurationExceedsLimit)
);
}

#[test]
#[should_panic]
fn test_round_duration_fails_when_datetime_exceeds_nanosecond_limit() {
fn test_duration_round_fails_when_datetime_exceeds_nanosecond_limit() {
let dt = Utc.ymd(2300, 12, 12).and_hms(0, 0, 0);

dt.round_duration(Duration::days(1));
assert_eq!(dt.duration_round(Duration::days(1)), Err(RoundingError::TimestampExceedsLimit),);
}

#[test]
#[should_panic]
fn test_round_duration_fails_when_duration_is_longer_than_date_beyond_timestamp_nanos() {
fn test_duration_round_fails_when_duration_is_longer_than_date_beyond_timestamp_nanos() {
let dt = Utc.ymd(1970, 12, 12).and_hms(0, 0, 0);

dt.round_duration(Duration::days(365));
assert_eq!(
dt.duration_round(Duration::days(365)),
Err(RoundingError::DurationExceedsTimestamp),
);
}

#[test]
fn test_trunc_duration() {
fn test_duration_trunc() {
let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_75_500_000);

assert_eq!(
dt.trunc_duration(Duration::milliseconds(10)).to_string(),
dt.duration_trunc(Duration::milliseconds(10)).unwrap().to_string(),
"2016-12-31 23:59:59.170 UTC"
);

let dt = Utc.ymd(2012, 12, 12).and_hms(18, 24, 12);
assert_eq!(dt.trunc_duration(Duration::minutes(5)).to_string(), "2012-12-12 18:20:00 UTC");
assert_eq!(dt.trunc_duration(Duration::minutes(10)).to_string(), "2012-12-12 18:20:00 UTC");
assert_eq!(dt.trunc_duration(Duration::minutes(30)).to_string(), "2012-12-12 18:00:00 UTC");
assert_eq!(dt.trunc_duration(Duration::hours(1)).to_string(), "2012-12-12 18:00:00 UTC");
assert_eq!(dt.trunc_duration(Duration::days(1)).to_string(), "2012-12-12 00:00:00 UTC");
assert_eq!(
dt.duration_trunc(Duration::minutes(5)).unwrap().to_string(),
"2012-12-12 18:20:00 UTC"
);
assert_eq!(
dt.duration_trunc(Duration::minutes(10)).unwrap().to_string(),
"2012-12-12 18:20:00 UTC"
);
assert_eq!(
dt.duration_trunc(Duration::minutes(30)).unwrap().to_string(),
"2012-12-12 18:00:00 UTC"
);
assert_eq!(
dt.duration_trunc(Duration::hours(1)).unwrap().to_string(),
"2012-12-12 18:00:00 UTC"
);
assert_eq!(
dt.duration_trunc(Duration::days(1)).unwrap().to_string(),
"2012-12-12 00:00:00 UTC"
);
}

#[test]
fn test_trunc_duration_pre_epoch() {
fn test_duration_trunc_pre_epoch() {
let dt = Utc.ymd(1969, 12, 12).and_hms(12, 12, 12);
assert_eq!(dt.trunc_duration(Duration::minutes(10)).to_string(), "1969-12-12 12:10:00 UTC");
assert_eq!(
dt.duration_trunc(Duration::minutes(10)).unwrap().to_string(),
"1969-12-12 12:10:00 UTC"
);
}
}

0 comments on commit bbeb0cd

Please sign in to comment.