use crate::errors::{DurationError, HifitimeError};
use crate::{SECONDS_PER_CENTURY, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
pub use crate::{Freq, Frequencies, TimeUnits, Unit};
use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "serde")]
use core::str::FromStr;
pub mod parse;
#[cfg(feature = "python")]
mod python;
#[cfg(feature = "python")]
use pyo3::prelude::pyclass;
#[cfg(not(feature = "std"))]
#[allow(unused_imports)] use num_traits::Float;
#[cfg(kani)]
mod kani_verif;
pub const DAYS_PER_CENTURY_U64: u64 = 36_525;
pub const NANOSECONDS_PER_MICROSECOND: u64 = 1_000;
pub const NANOSECONDS_PER_MILLISECOND: u64 = 1_000 * NANOSECONDS_PER_MICROSECOND;
pub const NANOSECONDS_PER_SECOND: u64 = 1_000 * NANOSECONDS_PER_MILLISECOND;
pub(crate) const NANOSECONDS_PER_SECOND_U32: u32 = 1_000_000_000;
pub const NANOSECONDS_PER_MINUTE: u64 = 60 * NANOSECONDS_PER_SECOND;
pub const NANOSECONDS_PER_HOUR: u64 = 60 * NANOSECONDS_PER_MINUTE;
pub const NANOSECONDS_PER_DAY: u64 = 24 * NANOSECONDS_PER_HOUR;
pub const NANOSECONDS_PER_CENTURY: u64 = DAYS_PER_CENTURY_U64 * NANOSECONDS_PER_DAY;
pub mod ops;
#[derive(Clone, Copy, Debug, PartialOrd, Eq, Ord)]
#[repr(C)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "hifitime"))]
pub struct Duration {
pub(crate) centuries: i16,
pub(crate) nanoseconds: u64,
}
impl PartialEq for Duration {
fn eq(&self, other: &Self) -> bool {
self.centuries == other.centuries && self.nanoseconds == other.nanoseconds
}
}
impl Hash for Duration {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.centuries.hash(hasher);
self.nanoseconds.hash(hasher);
}
}
impl Default for Duration {
fn default() -> Self {
Duration::ZERO
}
}
#[cfg(feature = "serde")]
impl Serialize for Duration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = self.to_string();
serializer.serialize_str(&s)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Duration::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl Duration {
pub const ZERO: Self = Self {
centuries: 0,
nanoseconds: 0,
};
pub const MAX: Self = Self {
centuries: i16::MAX,
nanoseconds: NANOSECONDS_PER_CENTURY,
};
pub const MIN: Self = Self {
centuries: i16::MIN,
nanoseconds: 0,
};
pub const EPSILON: Self = Self {
centuries: 0,
nanoseconds: 1,
};
pub const MIN_POSITIVE: Self = Self::EPSILON;
pub const MIN_NEGATIVE: Self = Self {
centuries: -1,
nanoseconds: NANOSECONDS_PER_CENTURY - 1,
};
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub const fn from_parts(centuries: i16, nanoseconds: u64) -> Self {
Self {
centuries,
nanoseconds,
}
.as_normalized()
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub const fn from_total_nanoseconds(nanos: i128) -> Self {
if nanos == 0 {
Self::ZERO
} else {
let centuries_i128 = nanos.div_euclid(NANOSECONDS_PER_CENTURY as i128);
let remaining_nanos_i128 = nanos.rem_euclid(NANOSECONDS_PER_CENTURY as i128);
if centuries_i128 > (i16::MAX as i128) {
Self::MAX
} else if centuries_i128 < (i16::MIN as i128) {
Self::MIN
} else {
Self::from_parts(centuries_i128 as i16, remaining_nanos_i128 as u64)
}
}
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub const fn from_truncated_nanoseconds(nanos: i64) -> Self {
if nanos < 0 {
let ns = nanos.unsigned_abs();
let extra_centuries = ns.div_euclid(NANOSECONDS_PER_CENTURY);
let rem_nanos = ns.rem_euclid(NANOSECONDS_PER_CENTURY);
Self::from_parts(
-1 - (extra_centuries as i16),
NANOSECONDS_PER_CENTURY - rem_nanos,
)
} else {
Self::from_parts(0, nanos.unsigned_abs())
}
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
#[cfg_attr(kani, kani::requires(value.is_finite()))]
pub const fn from_days(value: f64) -> Self {
Unit::Day.const_multiply(value)
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
#[cfg_attr(kani, kani::requires(value.is_finite()))]
pub const fn from_hours(value: f64) -> Self {
Unit::Hour.const_multiply(value)
}
#[must_use]
#[cfg_attr(kani, kani::requires(value.is_finite()))]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub const fn from_seconds(value: f64) -> Self {
Unit::Second.const_multiply(value)
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
#[cfg_attr(kani, kani::requires(value.is_finite()))]
pub const fn from_milliseconds(value: f64) -> Self {
Unit::Millisecond.const_multiply(value)
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
#[cfg_attr(kani, kani::requires(value.is_finite()))]
pub const fn from_microseconds(value: f64) -> Self {
Unit::Microsecond.const_multiply(value)
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
#[cfg_attr(kani, kani::requires(value.is_finite()))]
pub const fn from_nanoseconds(value: f64) -> Self {
Unit::Nanosecond.const_multiply(value)
}
#[allow(clippy::too_many_arguments)]
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub fn compose(
sign: i8,
days: u64,
hours: u64,
minutes: u64,
seconds: u64,
milliseconds: u64,
microseconds: u64,
nanoseconds: u64,
) -> Self {
Self::compose_f64(
sign,
days as f64,
hours as f64,
minutes as f64,
seconds as f64,
milliseconds as f64,
microseconds as f64,
nanoseconds as f64,
)
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn compose_f64(
sign: i8,
days: f64,
hours: f64,
minutes: f64,
seconds: f64,
milliseconds: f64,
microseconds: f64,
nanoseconds: f64,
) -> Self {
let me: Self = days.days()
+ hours.hours()
+ minutes.minutes()
+ seconds.seconds()
+ milliseconds.milliseconds()
+ microseconds.microseconds()
+ nanoseconds.nanoseconds();
if sign < 0 {
-me
} else {
me
}
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub fn from_tz_offset(sign: i8, hours: i64, minutes: i64) -> Self {
let dur = hours * Unit::Hour + minutes * Unit::Minute;
if sign < 0 {
-dur
} else {
dur
}
}
}
impl Duration {
fn normalize(&mut self) {
*self = self.as_normalized();
}
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
const fn as_normalized(self) -> Self {
let mut normalized_self = self;
let extra_centuries = self.nanoseconds / NANOSECONDS_PER_CENTURY;
if extra_centuries > 0 {
let rem_nanos = self.nanoseconds % NANOSECONDS_PER_CENTURY;
if self.centuries == i16::MAX {
if self.nanoseconds.saturating_add(rem_nanos) > Self::MAX.nanoseconds {
normalized_self = Self::MAX;
}
} else if !self.parts_are_equal(Self::MIN) {
match self.centuries.checked_add(extra_centuries as i16) {
Some(centuries) => {
normalized_self.centuries = centuries;
normalized_self.nanoseconds = rem_nanos;
}
None => {
if self.centuries >= 0 {
normalized_self = Self::MAX;
} else {
normalized_self = Self::MIN;
}
}
}
}
}
normalized_self
}
const fn parts_are_equal(&self, other: Duration) -> bool {
self.centuries == other.centuries && self.nanoseconds == other.nanoseconds
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result| result.0 == self.centuries && result.1 == self.nanoseconds))]
pub const fn to_parts(&self) -> (i16, u64) {
(self.centuries, self.nanoseconds)
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result| {
*result == i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY)
+ i128::from(self.nanoseconds)
}))]
pub fn total_nanoseconds(&self) -> i128 {
i128::from(self.centuries) * i128::from(NANOSECONDS_PER_CENTURY)
+ i128::from(self.nanoseconds)
}
pub fn try_truncated_nanoseconds(&self) -> Result<i64, HifitimeError> {
let total = self.total_nanoseconds();
if total > i64::MAX as i128 {
Err(HifitimeError::Duration {
source: DurationError::Overflow,
})
} else if total < i64::MIN as i128 {
Err(HifitimeError::Duration {
source: DurationError::Underflow,
})
} else {
Ok(total as i64)
}
}
#[must_use]
pub fn truncated_nanoseconds(&self) -> i64 {
match self.try_truncated_nanoseconds() {
Ok(val) => val,
Err(_) => {
if self.centuries < 0 {
i64::MIN
} else {
i64::MAX
}
}
}
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &f64| result.is_finite() && result.abs() < 1.1e14))]
pub fn to_seconds(&self) -> f64 {
let seconds = self.nanoseconds.div_euclid(NANOSECONDS_PER_SECOND);
let subseconds = self.nanoseconds.rem_euclid(NANOSECONDS_PER_SECOND);
if self.centuries == 0 {
(seconds as f64) + (subseconds as f64) * 1e-9
} else {
f64::from(self.centuries) * SECONDS_PER_CENTURY
+ (seconds as f64)
+ (subseconds as f64) * 1e-9
}
}
#[must_use]
pub fn to_unit(&self, unit: Unit) -> f64 {
self.to_seconds() * unit.from_seconds()
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Self| !result.centuries.is_negative()))]
pub fn abs(&self) -> Self {
if self.centuries.is_negative() {
-*self
} else {
*self
}
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result| *result == -1 || *result == 0 || *result == 1))]
pub const fn signum(&self) -> i8 {
self.centuries.signum() as i8
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &(i8, u64, u64, u64, u64, u64, u64, u64)| {
let (sign, _days, hours, minutes, seconds, ms, us, ns) = *result;
(sign == -1 || sign == 0 || sign == 1)
&& hours < 24
&& minutes < 60
&& seconds < 60
&& ms < 1000
&& us < 1000
&& ns < 1000
}))]
pub fn decompose(&self) -> (i8, u64, u64, u64, u64, u64, u64, u64) {
let mut me = *self;
let sign = me.signum();
me = me.abs();
let days = me.to_unit(Unit::Day).floor();
me -= days.days();
let hours = me.to_unit(Unit::Hour).floor();
me -= hours.hours();
let minutes = me.to_unit(Unit::Minute).floor();
me -= minutes.minutes();
let seconds = me.to_unit(Unit::Second).floor();
me -= seconds.seconds();
let milliseconds = me.to_unit(Unit::Millisecond).floor();
me -= milliseconds.milliseconds();
let microseconds = me.to_unit(Unit::Microsecond).floor();
me -= microseconds.microseconds();
let nanoseconds = me.to_unit(Unit::Nanosecond).round();
(
sign,
days as u64,
hours as u64,
minutes as u64,
seconds as u64,
milliseconds as u64,
microseconds as u64,
nanoseconds as u64,
)
}
#[must_use]
#[cfg_attr(kani, kani::ensures(|result: &Option<Duration>| {
match result {
Some(d) => {
d.nanoseconds < NANOSECONDS_PER_CENTURY
|| d.parts_are_equal(Self::MAX)
|| d.parts_are_equal(Self::MIN)
}
None => true,
}
}))]
pub fn subdivision(&self, unit: Unit) -> Option<Duration> {
let (_, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) =
self.decompose();
match unit {
Unit::Nanosecond => Some((nanoseconds as i64) * unit),
Unit::Microsecond => Some((microseconds as i64) * unit),
Unit::Millisecond => Some((milliseconds as i64) * unit),
Unit::Second => Some((seconds as i64) * unit),
Unit::Minute => Some((minutes as i64) * unit),
Unit::Hour => Some((hours as i64) * unit),
Unit::Day => Some((days as i64) * unit),
Unit::Week | Unit::Century => None,
}
}
#[cfg_attr(kani, kani::ensures(|result: &Self| {
result.nanoseconds < NANOSECONDS_PER_CENTURY
|| result.parts_are_equal(Self::MAX)
|| result.parts_are_equal(Self::MIN)
}))]
pub fn floor(&self, duration: Self) -> Self {
Self::from_total_nanoseconds(if duration.total_nanoseconds() == 0 {
0
} else {
self.total_nanoseconds() - self.total_nanoseconds() % duration.total_nanoseconds()
})
}
pub fn ceil(&self, duration: Self) -> Self {
let floored = self.floor(duration);
match floored
.total_nanoseconds()
.checked_add(duration.abs().total_nanoseconds())
{
Some(total_ns) => Self::from_total_nanoseconds(total_ns),
None => Self::MAX,
}
}
pub fn round(&self, duration: Self) -> Self {
let floored = self.floor(duration);
let ceiled = self.ceil(duration);
if *self - floored < (ceiled - *self).abs() {
floored
} else {
ceiled
}
}
pub fn approx(&self) -> Self {
let (_, days, hours, minutes, seconds, milli, us, _) = self.decompose();
let round_to = if days > 0 {
1 * Unit::Day
} else if hours > 0 {
1 * Unit::Hour
} else if minutes > 0 {
1 * Unit::Minute
} else if seconds > 0 {
1 * Unit::Second
} else if milli > 0 {
1 * Unit::Millisecond
} else if us > 0 {
1 * Unit::Microsecond
} else {
1 * Unit::Nanosecond
};
self.round(round_to)
}
#[cfg_attr(kani, kani::ensures(|result: &Self| *result <= self && *result <= other && (*result == self || *result == other)))]
pub fn min(self, other: Self) -> Self {
if self < other {
self
} else {
other
}
}
#[cfg_attr(kani, kani::ensures(|result: &Self| *result >= self && *result >= other && (*result == self || *result == other)))]
pub fn max(self, other: Self) -> Self {
if self > other {
self
} else {
other
}
}
#[cfg_attr(kani, kani::ensures(|result| *result == self.centuries.is_negative()))]
pub const fn is_negative(&self) -> bool {
self.centuries.is_negative()
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.total_nanoseconds() == 0 {
write!(f, "0 ns")
} else {
let (sign, days, hours, minutes, seconds, milli, us, nano) = self.decompose();
if sign == -1 {
write!(f, "-")?;
}
let values = [days, hours, minutes, seconds, milli, us, nano];
let units = [
if days > 1 { "days" } else { "day" },
"h",
"min",
"s",
"ms",
"μs",
"ns",
];
let mut insert_space = false;
for (val, unit) in values.iter().zip(units.iter()) {
if *val > 0 {
if insert_space {
write!(f, " ")?;
}
write!(f, "{val} {unit}")?;
insert_space = true;
}
}
Ok(())
}
}
}
impl fmt::LowerExp for Duration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let seconds_f64 = self.to_seconds();
let seconds_f64_abs = seconds_f64.abs();
if seconds_f64_abs < 1e-5 {
fmt::Display::fmt(&(seconds_f64 * 1e9), f)?;
write!(f, " ns")
} else if seconds_f64_abs < 1e-2 {
fmt::Display::fmt(&(seconds_f64 * 1e3), f)?;
write!(f, " ms")
} else if seconds_f64_abs < 3.0 * SECONDS_PER_MINUTE {
fmt::Display::fmt(&(seconds_f64), f)?;
write!(f, " s")
} else if seconds_f64_abs < SECONDS_PER_HOUR {
fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_MINUTE), f)?;
write!(f, " min")
} else if seconds_f64_abs < SECONDS_PER_DAY {
fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_HOUR), f)?;
write!(f, " h")
} else {
fmt::Display::fmt(&(seconds_f64 / SECONDS_PER_DAY), f)?;
write!(f, " days")
}
}
}
impl PartialEq<Unit> for Duration {
#[allow(clippy::identity_op)]
fn eq(&self, unit: &Unit) -> bool {
*self == *unit * 1
}
}
impl PartialOrd<Unit> for Duration {
#[allow(clippy::identity_op, clippy::comparison_chain)]
fn partial_cmp(&self, unit: &Unit) -> Option<Ordering> {
let unit_deref = *unit;
let unit_as_duration: Duration = unit_deref * 1;
if self < &unit_as_duration {
Some(Ordering::Less)
} else if self > &unit_as_duration {
Some(Ordering::Greater)
} else {
Some(Ordering::Equal)
}
}
}
impl From<Duration> for core::time::Duration {
fn from(hf_duration: Duration) -> Self {
use crate::NANOSECONDS_PER_SECOND;
if hf_duration.signum().is_negative() {
core::time::Duration::ZERO
} else {
let unsigned_nanos = hf_duration.total_nanoseconds() as u128;
let secs: u64 = (unsigned_nanos / NANOSECONDS_PER_SECOND as u128)
.try_into()
.unwrap_or(u64::MAX);
let subsec_nanos = (unsigned_nanos % NANOSECONDS_PER_SECOND as u128) as u32;
core::time::Duration::new(secs, subsec_nanos)
}
}
}
impl From<core::time::Duration> for Duration {
fn from(core_duration: core::time::Duration) -> Self {
Duration::from_total_nanoseconds(core_duration.as_nanos() as i128)
}
}
#[cfg(test)]
mod ut_duration {
use super::{Duration, TimeUnits, Unit, NANOSECONDS_PER_CENTURY};
#[test]
#[cfg(feature = "serde")]
fn test_serdes() {
for (dt, content) in [
(Duration::from_seconds(10.1), r#""10 s 100 ms""#),
(1.0_f64.days() + 99.nanoseconds(), r#""1 day 99 ns""#),
(
1.0_f64.centuries() + 99.seconds(),
r#""36525 days 1 min 39 s""#,
),
] {
assert_eq!(content, serde_json::to_string(&dt).unwrap());
let parsed: Duration = serde_json::from_str(content).unwrap();
assert_eq!(dt, parsed);
}
}
#[test]
fn test_bounds() {
let min = Duration::MIN;
assert_eq!(min.centuries, i16::MIN);
assert_eq!(min.nanoseconds, 0);
let max = Duration::MAX;
assert_eq!(max.centuries, i16::MAX);
assert_eq!(max.nanoseconds, NANOSECONDS_PER_CENTURY);
let min_p = Duration::MIN_POSITIVE;
assert_eq!(min_p.centuries, 0);
assert_eq!(min_p.nanoseconds, 1);
let min_n = Duration::MIN_NEGATIVE;
assert_eq!(min_n.centuries, -1);
assert_eq!(min_n.nanoseconds, NANOSECONDS_PER_CENTURY - 1);
let min_n1 = Duration::MIN - 1 * Unit::Nanosecond;
assert_eq!(min_n1, Duration::MIN);
let max_n1 = Duration::MAX - 1 * Unit::Nanosecond;
assert_eq!(max_n1.centuries, i16::MAX);
assert_eq!(max_n1.nanoseconds, NANOSECONDS_PER_CENTURY - 1);
}
#[test]
fn test_decompose() {
let d = -73000.days();
let out_days = d.to_unit(Unit::Day);
assert_eq!(out_days, -73000.0);
let (sign, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) =
d.decompose();
assert_eq!(sign, -1);
assert_eq!(days, 73000);
assert_eq!(hours, 0);
assert_eq!(minutes, 0);
assert_eq!(seconds, 0);
assert_eq!(milliseconds, 0);
assert_eq!(microseconds, 0);
assert_eq!(nanoseconds, 0);
}
#[test]
fn test_conversion() {
let d = Duration::MIN;
let core_d: core::time::Duration = d.into();
assert_eq!(core_d, core::time::Duration::ZERO);
let d = Duration::MAX;
let core_d: core::time::Duration = d.into();
assert_eq!(core_d, core::time::Duration::from_secs(103407943680000));
}
}