Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/round_up method on DurationRound trait #1651

Merged
merged 12 commits into from
Jan 20, 2025
233 changes: 232 additions & 1 deletion src/round.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,47 @@
/// );
/// ```
fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err>;

/// Return a copy rounded **up** by TimeDelta.
///
/// # Example
/// ``` rust
/// # use chrono::{DurationRound, TimeDelta, NaiveDate};
/// let dt = NaiveDate::from_ymd_opt(2018, 1, 11)
/// .unwrap()
/// .and_hms_milli_opt(12, 0, 0, 154)
/// .unwrap()
/// .and_utc();
/// assert_eq!(
/// dt.duration_round_up(TimeDelta::milliseconds(10)).unwrap().to_string(),
/// "2018-01-11 12:00:00.160 UTC"
/// );
/// assert_eq!(
/// dt.duration_round_up(TimeDelta::hours(1)).unwrap().to_string(),
/// "2018-01-11 13:00:00 UTC"
/// );
///
/// assert_eq!(
/// dt.duration_round_up(TimeDelta::days(1)).unwrap().to_string(),
/// "2018-01-12 00:00:00 UTC"
/// );
/// ```
fn duration_round_up(self, duration: TimeDelta) -> Result<Self, Self::Err>;
}

impl<Tz: TimeZone> DurationRound for DateTime<Tz> {
type Err = RoundingError;

fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err> {
duration_round(self.naive_local(), self, duration)
}

fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err> {
duration_trunc(self.naive_local(), self, duration)
}

fn duration_round_up(self, duration: TimeDelta) -> Result<Self, Self::Err> {
duration_round_up(self.naive_local(), self, duration)
}
}

impl DurationRound for NaiveDateTime {
Expand All @@ -177,6 +206,9 @@
fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err> {
duration_trunc(self, self, duration)
}
fn duration_round_up(self, duration: TimeDelta) -> Result<Self, Self::Err> {
MagnumTrader marked this conversation as resolved.
Show resolved Hide resolved
duration_round_up(self, self, duration)
}
}

fn duration_round<T>(
Expand Down Expand Up @@ -238,6 +270,31 @@
}
}

fn duration_round_up<T>(
naive: NaiveDateTime,
original: T,
duration: TimeDelta,
) -> Result<T, RoundingError>
where
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
{
if let Some(span) = duration.num_nanoseconds() {
if span <= 0 {
return Err(RoundingError::DurationExceedsLimit);
}
let stamp =
naive.and_utc().timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
let delta_down = stamp % span;
match delta_down.cmp(&0) {
Ordering::Equal => Ok(original),

Check warning on line 289 in src/round.rs

View check run for this annotation

Codecov / codecov/patch

src/round.rs#L289

Added line #L289 was not covered by tests
Ordering::Greater => Ok(original + TimeDelta::nanoseconds(span - delta_down)),
Ordering::Less => Ok(original + TimeDelta::nanoseconds(delta_down.abs())),
}
} else {
Err(RoundingError::DurationExceedsLimit)

Check warning on line 294 in src/round.rs

View check run for this annotation

Codecov / codecov/patch

src/round.rs#L294

Added line #L294 was not covered by tests
}
}

/// An error from rounding by `TimeDelta`
///
/// See: [`DurationRound`]
Expand Down Expand Up @@ -835,4 +892,178 @@
let dt = DateTime::from_timestamp_nanos(i64::MAX / 2 - 1);
assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00 UTC");
}

#[test]
fn test_duration_round_up() {
let dt = NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_hms_nano_opt(23, 59, 59, 175_500_000)
.unwrap()
.and_utc();

assert_eq!(
dt.duration_round_up(TimeDelta::new(-1, 0).unwrap()),
Err(RoundingError::DurationExceedsLimit)
);
assert_eq!(
dt.duration_round_up(TimeDelta::zero()),
Err(RoundingError::DurationExceedsLimit)
);

assert_eq!(
dt.duration_round_up(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
"2016-12-31 23:59:59.180 UTC"
);

// round up
let dt = NaiveDate::from_ymd_opt(2012, 12, 12)
.unwrap()
.and_hms_milli_opt(18, 22, 30, 0)
.unwrap()
.and_utc();

assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
"2012-12-12 18:25:00 UTC"
);

assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
"2012-12-12 18:30:00 UTC"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
"2012-12-12 18:30:00 UTC"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
"2012-12-12 19:00:00 UTC"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
"2012-12-13 00:00:00 UTC"
);

// timezone east
let dt =
FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
assert_eq!(
dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
"2020-10-28 00:00:00 +01:00"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
"2020-10-29 00:00:00 +01:00"
);

// timezone west
let dt =
FixedOffset::west_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
assert_eq!(
dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
"2020-10-28 00:00:00 -01:00"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
"2020-10-29 00:00:00 -01:00"
);
}

#[test]
fn test_duration_round_up_naive() {
let dt = NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_hms_nano_opt(23, 59, 59, 175_500_000)
.unwrap();

assert_eq!(
dt.duration_round_up(TimeDelta::new(-1, 0).unwrap()),
Err(RoundingError::DurationExceedsLimit)
);
assert_eq!(
dt.duration_round_up(TimeDelta::zero()),
Err(RoundingError::DurationExceedsLimit)
);

assert_eq!(
dt.duration_round_up(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
"2016-12-31 23:59:59.180"
);

let dt = Utc
.from_local_datetime(
&NaiveDate::from_ymd_opt(2012, 12, 12)
.unwrap()
.and_hms_milli_opt(18, 22, 30, 0)
.unwrap(),
)
.unwrap()
.naive_utc();
assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
"2012-12-12 18:25:00"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
"2012-12-12 18:30:00"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
"2012-12-12 18:30:00"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
"2012-12-12 19:00:00"
);
assert_eq!(
dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
"2012-12-13 00:00:00"
);
}

#[test]
fn test_duration_round_up_pre_epoch() {
let dt = Utc.with_ymd_and_hms(1969, 12, 12, 12, 12, 12).unwrap();
assert_eq!(
dt.duration_round_up(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
"1969-12-12 12:20:00 UTC"
);
}

#[test]
fn test_duration_round_up_close_to_min_max() {
let mut dt = NaiveDate::from_ymd_opt(2012, 12, 12)
.unwrap()
.and_hms_milli_opt(18, 22, 30, 0)
.unwrap()
.and_utc();

let span = TimeDelta::nanoseconds(i64::MAX);

assert_eq!(
dt.duration_round_up(span).unwrap().to_string(),
DateTime::from_timestamp_nanos(i64::MAX).to_string()
);

dt = DateTime::UNIX_EPOCH + TimeDelta::nanoseconds(1);
assert_eq!(dt.duration_round_up(span).unwrap(), DateTime::from_timestamp_nanos(i64::MAX));

let dt = DateTime::from_timestamp_nanos(1);
assert_eq!(
dt.duration_round_up(span).unwrap().to_string(),
"2262-04-11 23:47:16.854775807 UTC"
);

let dt = DateTime::from_timestamp_nanos(-1);
assert_eq!(dt.duration_round_up(span).unwrap(), DateTime::UNIX_EPOCH);

// Rounds to 1677-09-21 00:12:43.145224193 UTC if at i64::MIN.
// because i64::MIN is 1677-09-21 00:12:43.145224192 UTC.
//
// v
// We add 2 to get to 1677-09-21 00:12:43.145224194 UTC
// this issue is because abs(i64::MIN) == i64::MAX + 1
let dt = DateTime::from_timestamp_nanos(i64::MIN + 2);
assert_eq!(dt.duration_round_up(span).unwrap(), DateTime::UNIX_EPOCH);
}
}
Loading