use core::cmp::Ordering;
use core::fmt;
use core::ops::{Add, Sub};
use crate::datetime::DateTime;
use crate::naive::datetime::NaiveDateTime;
use crate::offset::TimeZone;
use crate::traits::Timelike;
use crate::duration::TimeDelta;
pub trait SubsecRound {
fn round_subsecs(self, digits: u16) -> Self;
fn trunc_subsecs(self, digits: u16) -> Self;
}
impl<T> SubsecRound for T
where
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
{
fn round_subsecs(self, digits: u16) -> T {
let span = span_for_digits(digits);
let delta_down = self.nanosecond() % span;
if delta_down == 0 {
return self;
}
let delta_up = span - delta_down;
if delta_up <= delta_down {
self + TimeDelta::nanoseconds(i64::from(delta_up))
} else {
self - TimeDelta::nanoseconds(i64::from(delta_down))
}
}
fn trunc_subsecs(self, digits: u16) -> T {
let span = span_for_digits(digits);
let delta_down = self.nanosecond() % span;
if delta_down == 0 {
self
} else {
self - TimeDelta::nanoseconds(i64::from(delta_down))
}
}
}
const fn span_for_digits(digits: u16) -> u32 {
match digits {
0 => 1_000_000_000,
1 => 100_000_000,
2 => 10_000_000,
3 => 1_000_000,
4 => 100_000,
5 => 10_000,
6 => 1_000,
7 => 100,
8 => 10,
_ => 1,
}
}
pub trait DurationRound: Sized {
type Err: fmt::Debug + fmt::Display;
fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err>;
fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err>;
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 {
type Err = RoundingError;
fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err> {
duration_round(self, self, duration)
}
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> {
duration_round_up(self, self, duration)
}
}
fn duration_round<T>(naive: NaiveDateTime, original: T, duration: TimeDelta) -> Result<T, RoundingError>
where
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
{
let span = duration.num_nanoseconds().filter(|&s| s > 0).ok_or(RoundingError::DurationExceedsLimit)?;
let stamp = naive.and_utc().timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
let delta_down = stamp % span;
if delta_down == 0 {
return Ok(original);
}
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 {
Ok(original + TimeDelta::nanoseconds(delta_up))
} else {
Ok(original - TimeDelta::nanoseconds(delta_down))
}
}
fn duration_trunc<T>(naive: NaiveDateTime, original: T, duration: TimeDelta) -> Result<T, RoundingError>
where
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
{
let span = duration.num_nanoseconds().filter(|&s| s > 0).ok_or(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),
Ordering::Greater => Ok(original - TimeDelta::nanoseconds(delta_down)),
Ordering::Less => Ok(original - TimeDelta::nanoseconds(span - delta_down.abs())),
}
}
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>,
{
let span = duration.num_nanoseconds().filter(|&s| s > 0).ok_or(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),
Ordering::Greater => Ok(original + TimeDelta::nanoseconds(span - delta_down)),
Ordering::Less => Ok(original + TimeDelta::nanoseconds(delta_down.abs())),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
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")
}
}
}
}
impl std::error::Error for RoundingError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::naive::date::NaiveDate;
use crate::naive::time::NaiveTime;
fn ndt(h: u32, mi: u32, s: u32) -> NaiveDateTime {
NaiveDate::from_ymd_opt(2023, 6, 15).unwrap().and_hms_opt(h, mi, s).unwrap()
}
#[test]
fn round_subsecs_exact_halfway_rounds_away_from_zero() {
let t = NaiveTime::from_hms_nano_opt(12, 0, 0, 500_000_000).unwrap();
assert_eq!(t.round_subsecs(0), NaiveTime::from_hms_opt(12, 0, 1).unwrap());
assert_eq!(t.round_subsecs(1), t); }
#[test]
fn round_subsecs_rounds_to_the_closer_multiple() {
let below = NaiveTime::from_hms_milli_opt(12, 0, 0, 100).unwrap();
assert_eq!(below.round_subsecs(0), NaiveTime::from_hms_opt(12, 0, 0).unwrap());
let above = NaiveTime::from_hms_milli_opt(12, 0, 0, 900).unwrap();
assert_eq!(above.round_subsecs(0), NaiveTime::from_hms_opt(12, 0, 1).unwrap());
}
#[test]
fn round_subsecs_is_a_no_op_at_9_or_more_digits() {
let t = NaiveTime::from_hms_nano_opt(12, 0, 0, 123_456_789).unwrap();
assert_eq!(t.round_subsecs(9), t);
assert_eq!(t.round_subsecs(20), t);
}
#[test]
fn trunc_subsecs_always_rounds_toward_zero() {
let t = NaiveTime::from_hms_nano_opt(12, 0, 0, 999_000_000).unwrap();
assert_eq!(t.trunc_subsecs(0), NaiveTime::from_hms_opt(12, 0, 0).unwrap());
assert_eq!(t.trunc_subsecs(1), NaiveTime::from_hms_milli_opt(12, 0, 0, 900).unwrap());
assert_eq!(t.trunc_subsecs(2), NaiveTime::from_hms_milli_opt(12, 0, 0, 990).unwrap());
assert_eq!(t.trunc_subsecs(9), t);
}
#[test]
fn round_and_trunc_subsecs_also_work_on_naivedatetime() {
let dt = ndt(12, 0, 0) + TimeDelta::milliseconds(600);
assert_eq!(dt.trunc_subsecs(1).time().nanosecond(), 600_000_000);
assert_eq!(dt.round_subsecs(1).time(), NaiveTime::from_hms_milli_opt(12, 0, 0, 600).unwrap());
}
#[test]
fn duration_round_rounds_to_the_nearer_multiple() {
assert_eq!(ndt(12, 40, 0).duration_round(TimeDelta::hours(1)).unwrap(), ndt(13, 0, 0));
assert_eq!(ndt(12, 10, 0).duration_round(TimeDelta::hours(1)).unwrap(), ndt(12, 0, 0));
}
#[test]
fn duration_trunc_always_rounds_down() {
assert_eq!(ndt(12, 40, 0).duration_trunc(TimeDelta::hours(1)).unwrap(), ndt(12, 0, 0));
assert_eq!(ndt(12, 10, 0).duration_trunc(TimeDelta::hours(1)).unwrap(), ndt(12, 0, 0));
}
#[test]
fn duration_round_up_always_rounds_up_unless_already_exact() {
assert_eq!(ndt(12, 40, 0).duration_round_up(TimeDelta::hours(1)).unwrap(), ndt(13, 0, 0));
assert_eq!(ndt(12, 10, 0).duration_round_up(TimeDelta::hours(1)).unwrap(), ndt(13, 0, 0));
assert_eq!(ndt(12, 0, 0).duration_round_up(TimeDelta::hours(1)).unwrap(), ndt(12, 0, 0));
}
#[test]
fn all_three_are_no_ops_on_an_exact_multiple() {
let exact = ndt(12, 0, 0);
assert_eq!(exact.duration_round(TimeDelta::hours(1)).unwrap(), exact);
assert_eq!(exact.duration_trunc(TimeDelta::hours(1)).unwrap(), exact);
assert_eq!(exact.duration_round_up(TimeDelta::hours(1)).unwrap(), exact);
}
#[test]
fn duration_round_rejects_zero_or_negative_span() {
let d = ndt(12, 0, 0);
assert_eq!(d.duration_round(TimeDelta::zero()), Err(RoundingError::DurationExceedsLimit));
assert_eq!(d.duration_round(TimeDelta::hours(-1)), Err(RoundingError::DurationExceedsLimit));
}
#[test]
fn duration_round_also_works_on_datetime_utc() {
let d = ndt(12, 40, 0).and_utc();
let rounded = d.duration_round(TimeDelta::hours(1)).unwrap();
assert_eq!(rounded.naive_utc(), ndt(13, 0, 0));
}
#[test]
fn rounding_error_display() {
assert_eq!(
RoundingError::DurationExceedsLimit.to_string(),
"duration exceeds num_nanoseconds limit"
);
assert_eq!(
RoundingError::TimestampExceedsLimit.to_string(),
"timestamp exceeds num_nanoseconds limit"
);
}
}