use core::ops::{Add, Sub};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimeScale {
Tai,
Gps,
Tt,
Utc,
}
const NS_PER_SEC: i128 = 1_000_000_000;
const SEC_PER_DAY: i64 = 86_400;
const TT_MINUS_TAI_NS: i128 = 32_184_000_000;
const TAI_MINUS_GPS_NS: i128 = 19 * NS_PER_SEC;
const GPS_EPOCH_UNIX: i64 = 315_964_800;
const UNIX_JD: f64 = 2_440_587.5;
const LEAP_TABLE: [(i64, i64, i64, i64); 28] = [
(1972, 1, 1, 10),
(1972, 7, 1, 11),
(1973, 1, 1, 12),
(1974, 1, 1, 13),
(1975, 1, 1, 14),
(1976, 1, 1, 15),
(1977, 1, 1, 16),
(1978, 1, 1, 17),
(1979, 1, 1, 18),
(1980, 1, 1, 19),
(1981, 7, 1, 20),
(1982, 7, 1, 21),
(1983, 7, 1, 22),
(1985, 7, 1, 23),
(1988, 1, 1, 24),
(1990, 1, 1, 25),
(1991, 1, 1, 26),
(1992, 7, 1, 27),
(1993, 7, 1, 28),
(1994, 7, 1, 29),
(1996, 1, 1, 30),
(1997, 7, 1, 31),
(1999, 1, 1, 32),
(2006, 1, 1, 33),
(2009, 1, 1, 34),
(2012, 7, 1, 35),
(2015, 7, 1, 36),
(2017, 1, 1, 37),
];
const fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = (if y >= 0 { y } else { y - 399 }) / 400;
let yoe = y - era * 400; let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719_468
}
const fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719_468;
let era = (if z >= 0 { z } else { z - 146_096 }) / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; (if m <= 2 { y + 1 } else { y }, m, d)
}
fn leap_secs_at_unix(unix_s: i64) -> i64 {
let mut leap = LEAP_TABLE[0].3;
for (y, m, d, l) in LEAP_TABLE {
if unix_s >= days_from_civil(y, m, d) * SEC_PER_DAY {
leap = l;
}
}
leap
}
fn leap_secs_at_tai(tai_ns: i128) -> i64 {
let mut leap = LEAP_TABLE[0].3;
for (y, m, d, l) in LEAP_TABLE {
let tai_transition = (days_from_civil(y, m, d) * SEC_PER_DAY + l) as i128 * NS_PER_SEC;
if tai_ns >= tai_transition {
leap = l;
}
}
leap
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Duration {
nanos: i128,
}
impl Duration {
pub const ZERO: Duration = Duration { nanos: 0 };
pub const SECOND: Duration = Duration { nanos: NS_PER_SEC };
pub const MINUTE: Duration = Duration { nanos: 60 * NS_PER_SEC };
pub const HOUR: Duration = Duration {
nanos: 3_600 * NS_PER_SEC,
};
pub const DAY: Duration = Duration {
nanos: SEC_PER_DAY as i128 * NS_PER_SEC,
};
pub const WEEK: Duration = Duration {
nanos: 7 * SEC_PER_DAY as i128 * NS_PER_SEC,
};
pub const JULIAN_YEAR: Duration = Duration {
nanos: 31_557_600 * NS_PER_SEC,
};
#[inline]
pub const fn from_nanos(nanos: i128) -> Self {
Duration { nanos }
}
#[inline]
pub const fn from_micros(micros: i128) -> Self {
Duration { nanos: micros * 1_000 }
}
#[inline]
pub const fn from_millis(millis: i128) -> Self {
Duration {
nanos: millis * 1_000_000,
}
}
#[inline]
pub fn from_seconds(seconds: f64) -> Self {
Duration {
nanos: (seconds * 1e9).round() as i128,
}
}
#[inline]
pub fn from_minutes(minutes: f64) -> Self {
Duration::from_seconds(minutes * 60.0)
}
#[inline]
pub fn from_hours(hours: f64) -> Self {
Duration::from_seconds(hours * 3_600.0)
}
#[inline]
pub fn from_days(days: f64) -> Self {
Duration::from_seconds(days * SEC_PER_DAY as f64)
}
#[inline]
pub fn from_julian_years(years: f64) -> Self {
Duration::from_seconds(years * 31_557_600.0)
}
#[inline]
pub const fn total_nanoseconds(self) -> i128 {
self.nanos
}
#[inline]
pub fn as_seconds(self) -> f64 {
self.nanos as f64 / 1e9
}
#[inline]
pub fn as_minutes(self) -> f64 {
self.as_seconds() / 60.0
}
#[inline]
pub fn as_hours(self) -> f64 {
self.as_seconds() / 3_600.0
}
#[inline]
pub fn as_days(self) -> f64 {
self.as_seconds() / SEC_PER_DAY as f64
}
#[inline]
pub fn as_julian_years(self) -> f64 {
self.as_seconds() / 31_557_600.0
}
#[inline]
pub const fn abs(self) -> Self {
Duration {
nanos: self.nanos.abs(),
}
}
#[inline]
pub const fn signum(self) -> i32 {
self.nanos.signum() as i32
}
#[inline]
pub const fn is_zero(self) -> bool {
self.nanos == 0
}
}
impl Add for Duration {
type Output = Duration;
#[inline]
fn add(self, o: Duration) -> Duration {
Duration {
nanos: self.nanos + o.nanos,
}
}
}
impl Sub for Duration {
type Output = Duration;
#[inline]
fn sub(self, o: Duration) -> Duration {
Duration {
nanos: self.nanos - o.nanos,
}
}
}
impl core::ops::Neg for Duration {
type Output = Duration;
#[inline]
fn neg(self) -> Duration {
Duration { nanos: -self.nanos }
}
}
impl core::ops::Mul<f64> for Duration {
type Output = Duration;
#[inline]
fn mul(self, k: f64) -> Duration {
Duration {
nanos: (self.nanos as f64 * k).round() as i128,
}
}
}
impl core::ops::Div<f64> for Duration {
type Output = Duration;
#[inline]
fn div(self, k: f64) -> Duration {
Duration {
nanos: (self.nanos as f64 / k).round() as i128,
}
}
}
impl core::ops::Div<Duration> for Duration {
type Output = f64;
#[inline]
fn div(self, o: Duration) -> f64 {
self.nanos as f64 / o.nanos as f64
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Epoch {
tai_ns: i128,
}
impl Epoch {
pub const J2000: Epoch = Epoch {
tai_ns: (days_from_civil(2000, 1, 1) * SEC_PER_DAY + 43_200) as i128 * NS_PER_SEC - TT_MINUS_TAI_NS,
};
pub const UNIX_EPOCH: Epoch = Epoch {
tai_ns: LEAP_TABLE[0].3 as i128 * NS_PER_SEC, };
pub const GPS_EPOCH: Epoch = Epoch {
tai_ns: (GPS_EPOCH_UNIX as i128 + 19) * NS_PER_SEC,
};
pub fn from_scale_nanos(scale: TimeScale, nanos: i128) -> Self {
let tai_ns = match scale {
TimeScale::Tai => nanos,
TimeScale::Tt => nanos - TT_MINUS_TAI_NS,
TimeScale::Gps => nanos + TAI_MINUS_GPS_NS,
TimeScale::Utc => {
let unix_s = (nanos.div_euclid(NS_PER_SEC)) as i64;
nanos + leap_secs_at_unix(unix_s) as i128 * NS_PER_SEC
}
};
Epoch { tai_ns }
}
#[inline]
pub fn from_scale_seconds(scale: TimeScale, seconds: f64) -> Self {
Epoch::from_scale_nanos(scale, (seconds * 1e9).round() as i128)
}
#[inline]
pub fn from_unix_seconds(seconds: f64) -> Self {
Epoch::from_scale_seconds(TimeScale::Utc, seconds)
}
#[inline]
pub fn from_unix_nanos(nanos: i128) -> Self {
Epoch::from_scale_nanos(TimeScale::Utc, nanos)
}
#[inline]
pub fn from_gps_seconds(seconds: f64) -> Self {
Epoch::from_scale_seconds(TimeScale::Gps, seconds + GPS_EPOCH_UNIX as f64)
}
#[inline]
pub fn from_gps_week_seconds(week: u32, tow_seconds: f64) -> Self {
Epoch::from_gps_seconds(week as f64 * 604_800.0 + tow_seconds)
}
pub fn from_gregorian(
scale: TimeScale,
year: i64,
month: i64,
day: i64,
hour: i64,
minute: i64,
seconds: f64,
) -> Self {
let whole =
days_from_civil(year, month, day) * SEC_PER_DAY + hour * 3_600 + minute * 60 + seconds.trunc() as i64;
let frac_ns = ((seconds - seconds.trunc()) * 1e9).round() as i128;
Epoch::from_scale_nanos(scale, whole as i128 * NS_PER_SEC + frac_ns)
}
#[inline]
pub fn from_gregorian_utc(year: i64, month: i64, day: i64, hour: i64, minute: i64, seconds: f64) -> Self {
Epoch::from_gregorian(TimeScale::Utc, year, month, day, hour, minute, seconds)
}
#[inline]
pub fn from_julian_date(scale: TimeScale, jd: f64) -> Self {
Epoch::from_scale_seconds(scale, (jd - UNIX_JD) * SEC_PER_DAY as f64)
}
#[inline]
pub fn from_mjd(scale: TimeScale, mjd: f64) -> Self {
Epoch::from_julian_date(scale, mjd + 2_400_000.5)
}
pub fn from_decimal_year(year: f64) -> Self {
let y = year.floor() as i64;
let start = Epoch::from_gregorian_utc(y, 1, 1, 0, 0, 0.0);
let next = Epoch::from_gregorian_utc(y + 1, 1, 1, 0, 0, 0.0);
start + (next - start) * (year - y as f64)
}
pub fn now() -> Self {
let d = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before 1970");
Epoch::from_unix_nanos(d.as_nanos() as i128)
}
#[inline]
pub const fn duration_since(self, other: Epoch) -> Duration {
Duration {
nanos: self.tai_ns - other.tai_ns,
}
}
#[inline]
pub fn leap_seconds(self) -> f64 {
leap_secs_at_tai(self.tai_ns) as f64
}
#[inline]
pub fn to_scale_nanos(self, scale: TimeScale) -> i128 {
match scale {
TimeScale::Tai => self.tai_ns,
TimeScale::Tt => self.tai_ns + TT_MINUS_TAI_NS,
TimeScale::Gps => self.tai_ns - TAI_MINUS_GPS_NS,
TimeScale::Utc => self.tai_ns - leap_secs_at_tai(self.tai_ns) as i128 * NS_PER_SEC,
}
}
#[inline]
pub fn to_scale_seconds(self, scale: TimeScale) -> f64 {
self.to_scale_nanos(scale) as f64 / 1e9
}
#[inline]
pub fn to_unix_seconds(self) -> f64 {
self.to_scale_seconds(TimeScale::Utc)
}
#[inline]
pub fn gps_seconds(self) -> f64 {
self.to_scale_seconds(TimeScale::Gps) - GPS_EPOCH_UNIX as f64
}
pub fn gps_week_seconds(self) -> (u32, f64) {
let g = self.gps_seconds();
let week = (g / 604_800.0).floor();
((week as u32), g - week * 604_800.0)
}
#[inline]
pub fn julian_date(self, scale: TimeScale) -> f64 {
UNIX_JD + self.to_scale_nanos(scale) as f64 / (1e9 * SEC_PER_DAY as f64)
}
#[inline]
pub fn mjd(self, scale: TimeScale) -> f64 {
self.julian_date(scale) - 2_400_000.5
}
pub fn decimal_year(self) -> f64 {
let (y, ..) = self.to_gregorian_utc();
let start = Epoch::from_gregorian_utc(y, 1, 1, 0, 0, 0.0);
let next = Epoch::from_gregorian_utc(y + 1, 1, 1, 0, 0, 0.0);
y as f64 + (self - start) / (next - start)
}
pub fn to_gregorian(self, scale: TimeScale) -> (i64, u8, u8, u8, u8, f64) {
let ns = self.to_scale_nanos(scale);
let total_secs = ns.div_euclid(NS_PER_SEC) as i64;
let frac = (ns.rem_euclid(NS_PER_SEC)) as f64 / 1e9;
let days = total_secs.div_euclid(SEC_PER_DAY);
let sod = total_secs.rem_euclid(SEC_PER_DAY);
let (y, m, d) = civil_from_days(days);
let hour = sod / 3_600;
let minute = (sod % 3_600) / 60;
let second = sod % 60;
(y, m as u8, d as u8, hour as u8, minute as u8, second as f64 + frac)
}
#[inline]
pub fn to_gregorian_utc(self) -> (i64, u8, u8, u8, u8, f64) {
self.to_gregorian(TimeScale::Utc)
}
pub fn earth_rotation_angle_rad(self) -> f64 {
let tu = self.julian_date(TimeScale::Utc) - 2_451_545.0;
let turns = 0.779_057_273_264_0 + 1.002_737_811_911_354_6 * tu;
turns.rem_euclid(1.0) * core::f64::consts::TAU
}
}
impl Add<Duration> for Epoch {
type Output = Epoch;
#[inline]
fn add(self, d: Duration) -> Epoch {
Epoch {
tai_ns: self.tai_ns + d.total_nanoseconds(),
}
}
}
impl Sub<Duration> for Epoch {
type Output = Epoch;
#[inline]
fn sub(self, d: Duration) -> Epoch {
Epoch {
tai_ns: self.tai_ns - d.total_nanoseconds(),
}
}
}
impl Sub<Epoch> for Epoch {
type Output = Duration;
#[inline]
fn sub(self, o: Epoch) -> Duration {
self.duration_since(o)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimeWindow {
pub start: Option<Epoch>,
pub end: Option<Epoch>,
}
impl TimeWindow {
#[inline]
pub const fn new(start: Option<Epoch>, end: Option<Epoch>) -> Self {
TimeWindow { start, end }
}
#[inline]
pub const fn between(start: Epoch, end: Epoch) -> Self {
TimeWindow::new(Some(start), Some(end))
}
#[inline]
pub fn for_duration(start: Epoch, duration: Duration) -> Self {
TimeWindow::between(start, start + duration)
}
#[inline]
pub const fn from(start: Epoch) -> Self {
TimeWindow::new(Some(start), None)
}
#[inline]
pub const fn until(end: Epoch) -> Self {
TimeWindow::new(None, Some(end))
}
#[inline]
pub const fn unbounded() -> Self {
TimeWindow::new(None, None)
}
#[inline]
pub fn contains(&self, epoch: Epoch) -> bool {
self.start.is_none_or(|s| epoch >= s) && self.end.is_none_or(|e| epoch < e)
}
#[inline]
pub fn clamp(&self, epoch: Epoch) -> Epoch {
let mut e = epoch;
if let Some(s) = self.start {
if e < s {
e = s;
}
}
if let Some(end) = self.end {
if e > end {
e = end;
}
}
e
}
#[inline]
pub fn duration(&self) -> Option<Duration> {
match (self.start, self.end) {
(Some(s), Some(e)) => Some(e - s),
_ => None,
}
}
pub fn intersect(&self, other: &TimeWindow) -> Option<TimeWindow> {
let start = max_opt(self.start, other.start, true);
let end = max_opt(self.end, other.end, false);
if let (Some(s), Some(e)) = (start, end) {
if s >= e {
return None;
}
}
Some(TimeWindow::new(start, end))
}
#[inline]
pub fn overlaps(&self, other: &TimeWindow) -> bool {
self.intersect(other).is_some()
}
}
fn max_opt(a: Option<Epoch>, b: Option<Epoch>, take_later: bool) -> Option<Epoch> {
match (a, b) {
(Some(a), Some(b)) => Some(if (a > b) == take_later { a } else { b }),
(x, None) | (None, x) => x,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn time_window_contains_clamp_and_intersect() {
let t0 = Epoch::from_unix_seconds(1_000_000.0);
let w = TimeWindow::for_duration(t0, Duration::from_hours(2.0));
assert!(w.contains(t0) && w.contains(t0 + Duration::from_hours(1.0)));
assert!(!w.contains(t0 + Duration::from_hours(2.0))); assert!(!w.contains(t0 - Duration::SECOND));
assert_eq!(w.clamp(t0 - Duration::HOUR), t0);
assert_eq!(w.clamp(t0 + Duration::from_hours(5.0)), t0 + Duration::from_hours(2.0));
assert_eq!(w.clamp(t0 + Duration::HOUR), t0 + Duration::HOUR);
let later = TimeWindow::from(t0 + Duration::HOUR);
assert_eq!(w.intersect(&later).unwrap().duration(), Some(Duration::HOUR));
assert!(w.intersect(&TimeWindow::from(t0 + Duration::from_hours(3.0))).is_none());
assert!(TimeWindow::unbounded().contains(t0));
assert_eq!(TimeWindow::unbounded().intersect(&w), Some(w));
}
#[test]
fn civil_day_algorithm_round_trips() {
for (y, m, d) in [
(1970, 1, 1),
(2000, 1, 1),
(2026, 9, 14),
(1972, 6, 30),
(1600, 12, 31),
(2400, 2, 29),
] {
let z = days_from_civil(y, m, d);
assert_eq!(civil_from_days(z), (y, m, d), "{y}-{m}-{d}");
}
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(days_from_civil(2000, 1, 1), 10_957);
assert_eq!(days_from_civil(1980, 1, 6) * SEC_PER_DAY, GPS_EPOCH_UNIX);
}
#[test]
fn leap_seconds_step_correctly() {
assert_eq!(Epoch::from_gregorian_utc(1971, 1, 1, 0, 0, 0.0).leap_seconds(), 10.0);
assert_eq!(Epoch::from_gregorian_utc(1972, 1, 1, 0, 0, 0.0).leap_seconds(), 10.0);
assert_eq!(Epoch::from_gregorian_utc(1972, 7, 1, 0, 0, 0.0).leap_seconds(), 11.0);
assert_eq!(Epoch::from_gregorian_utc(2000, 1, 1, 0, 0, 0.0).leap_seconds(), 32.0);
assert_eq!(Epoch::from_gregorian_utc(2017, 1, 1, 0, 0, 0.0).leap_seconds(), 37.0);
assert_eq!(Epoch::from_gregorian_utc(2026, 1, 1, 0, 0, 0.0).leap_seconds(), 37.0);
assert_eq!(
(Epoch::from_gregorian_utc(1972, 7, 1, 0, 0, 0.0) - Duration::SECOND).leap_seconds(),
10.0
);
}
#[test]
fn scale_offsets_are_exact_to_the_nanosecond() {
let e = Epoch::from_gregorian_utc(2020, 6, 15, 12, 0, 0.0);
assert_eq!(
e.to_scale_nanos(TimeScale::Tt) - e.to_scale_nanos(TimeScale::Tai),
32_184_000_000
);
assert_eq!(
e.to_scale_nanos(TimeScale::Tai) - e.to_scale_nanos(TimeScale::Gps),
19_000_000_000
);
assert_eq!(
e.to_scale_nanos(TimeScale::Tai) - e.to_scale_nanos(TimeScale::Utc),
37_000_000_000
);
assert_eq!(
e.to_scale_nanos(TimeScale::Gps) - e.to_scale_nanos(TimeScale::Utc),
18_000_000_000
);
}
#[test]
fn j2000_is_consistent_everywhere() {
let j = Epoch::J2000;
assert!(close(j.julian_date(TimeScale::Tt), 2_451_545.0, 1e-9));
assert!(close(j.mjd(TimeScale::Tt), 51_544.5, 1e-9));
let (y, mo, d, h, mi, s) = j.to_gregorian_utc();
assert_eq!((y, mo, d, h, mi), (2000, 1, 1, 11, 58));
assert!(close(s, 55.816, 1e-6), "{s}");
assert_eq!(j, Epoch::from_gregorian(TimeScale::Tt, 2000, 1, 1, 12, 0, 0.0));
}
#[test]
fn unix_and_gps_reference_values() {
let noon2000 = Epoch::from_gregorian_utc(2000, 1, 1, 12, 0, 0.0);
assert!(close(noon2000.to_unix_seconds(), 946_728_000.0, 1e-6));
assert!(close(noon2000.julian_date(TimeScale::Utc), 2_451_545.0, 1e-9));
assert_eq!(Epoch::from_unix_seconds(0.0), Epoch::UNIX_EPOCH);
assert_eq!(Epoch::GPS_EPOCH, Epoch::from_gregorian_utc(1980, 1, 6, 0, 0, 0.0));
assert!(close(Epoch::GPS_EPOCH.gps_seconds(), 0.0, 1e-9));
let (w, tow) = Epoch::GPS_EPOCH.gps_week_seconds();
assert_eq!((w, tow), (0, 0.0));
let (w, _) = Epoch::from_gregorian_utc(2020, 6, 15, 0, 0, 0.0).gps_week_seconds();
assert_eq!(w, 2110);
}
#[test]
fn round_trips_hold_to_the_nanosecond() {
let e = Epoch::from_gregorian_utc(2026, 9, 14, 17, 33, 12.123_456_789);
let (y, mo, d, h, mi, s) = e.to_gregorian_utc();
assert_eq!((y, mo, d, h, mi), (2026, 9, 14, 17, 33));
assert!(close(s, 12.123_456_789, 1e-9), "{s}");
let later = e + Duration::from_nanos(1);
assert!(later > e && (later - e) == Duration::from_nanos(1));
for scale in [TimeScale::Tai, TimeScale::Gps, TimeScale::Tt, TimeScale::Utc] {
let (y, mo, d, h, mi, s) = e.to_gregorian(scale);
let back = Epoch::from_gregorian(scale, y, mo as i64, d as i64, h as i64, mi as i64, s);
assert!((back - e).abs() <= Duration::from_nanos(1), "{scale:?}");
}
}
#[test]
fn decimal_year_uses_the_calendar_convention() {
let e = Epoch::from_decimal_year(2020.0);
assert_eq!(e, Epoch::from_gregorian_utc(2020, 1, 1, 0, 0, 0.0));
assert!(close(e.decimal_year(), 2020.0, 1e-9));
assert!(close(Epoch::from_decimal_year(2020.5).decimal_year(), 2020.5, 1e-9));
let mid = Epoch::from_decimal_year(2000.5);
let (_, mo, d, ..) = mid.to_gregorian_utc();
assert_eq!((mo, d), (7, 2)); }
#[test]
fn era_matches_reference() {
let era = Epoch::from_julian_date(TimeScale::Utc, 2_451_545.0).earth_rotation_angle_rad();
assert!(close(era, 0.779_057_273_264_0 * core::f64::consts::TAU, 1e-12), "{era}");
assert!((0.0..core::f64::consts::TAU).contains(&era));
}
#[test]
fn durations_convert_and_compose() {
assert_eq!(Duration::from_days(1.0), Duration::HOUR * 24.0);
assert!(close(Duration::JULIAN_YEAR.as_days(), 365.25, 1e-9));
assert!(close(Duration::from_hours(3.0) / Duration::from_hours(2.0), 1.5, 1e-12));
assert_eq!(Duration::from_seconds(1.0).total_nanoseconds(), 1_000_000_000);
assert_eq!((-Duration::SECOND).signum(), -1);
assert_eq!(Duration::from_millis(1_500).as_seconds(), 1.5);
}
}