use core::{
fmt,
ops::{Add, Neg, Sub},
};
use supernovas_ffi::{novas_timescale, novas_timescale::NOVAS_TT};
use crate::{
error::{Error, Result},
unit,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Interval {
seconds: f64,
timescale: novas_timescale,
}
impl Interval {
pub fn from_seconds(seconds: f64, timescale: novas_timescale) -> Result<Self> {
if !seconds.is_finite() {
return Err(Error::NotFinite);
}
Ok(Interval { seconds, timescale })
}
pub fn from_millis(ms: f64) -> Result<Self> {
Self::from_seconds(ms * unit::MS, NOVAS_TT)
}
pub fn from_minutes(min: f64) -> Result<Self> {
Self::from_seconds(min * unit::MIN, NOVAS_TT)
}
pub fn from_hours(hours: f64) -> Result<Self> {
Self::from_seconds(hours * unit::HOUR, NOVAS_TT)
}
pub fn from_days(days: f64) -> Result<Self> {
Self::from_seconds(days * unit::DAY, NOVAS_TT)
}
pub fn from_julian_years(years: f64) -> Result<Self> {
Self::from_seconds(years * unit::JULIAN_YEAR, NOVAS_TT)
}
pub fn from_julian_centuries(cy: f64) -> Result<Self> {
Self::from_seconds(cy * unit::JULIAN_CENTURY, NOVAS_TT)
}
pub fn timescale(self) -> novas_timescale {
self.timescale
}
pub fn millis(self) -> f64 {
self.seconds / unit::MS
}
pub fn seconds(self) -> f64 {
self.seconds
}
pub fn minutes(self) -> f64 {
self.seconds / unit::MIN
}
pub fn hours(self) -> f64 {
self.seconds / unit::HOUR
}
pub fn days(self) -> f64 {
self.seconds / unit::DAY
}
pub fn weeks(self) -> f64 {
self.seconds / unit::WEEK
}
pub fn years(self) -> f64 {
self.seconds / unit::YEAR
}
pub fn julian_years(self) -> f64 {
self.seconds / unit::JULIAN_YEAR
}
pub fn julian_centuries(self) -> f64 {
self.seconds / unit::JULIAN_CENTURY
}
}
impl Add for Interval {
type Output = Interval;
fn add(self, rhs: Interval) -> Interval {
Interval {
seconds: self.seconds + rhs.seconds,
timescale: self.timescale,
}
}
}
impl Sub for Interval {
type Output = Interval;
fn sub(self, rhs: Interval) -> Interval {
Interval {
seconds: self.seconds - rhs.seconds,
timescale: self.timescale,
}
}
}
impl Neg for Interval {
type Output = Interval;
fn neg(self) -> Interval {
Interval {
seconds: -self.seconds,
timescale: self.timescale,
}
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision().unwrap_or(3);
let d = self.seconds.abs();
let (value, unit_label) = if d < unit::NS {
(self.seconds / unit::PS, "ps")
} else if d < unit::US {
(self.seconds / unit::NS, "ns")
} else if d < unit::MS {
(self.seconds / unit::US, "μs")
} else if d < 1.0 {
(self.seconds / unit::MS, "ms")
} else if d < unit::MIN {
(self.seconds, "s")
} else if d < unit::HOUR {
(self.seconds / unit::MIN, "min")
} else if d < unit::DAY {
(self.seconds / unit::HOUR, "h")
} else if d < unit::JULIAN_YEAR {
(self.seconds / unit::DAY, "d")
} else {
(self.seconds / unit::JULIAN_YEAR, "yr")
};
write!(f, "{value:.decimals$} {unit_label}")
}
}
impl approx::AbsDiffEq for Interval {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
unit::US
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
(self.seconds - other.seconds).abs() <= epsilon
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_finite() {
assert!(matches!(
Interval::from_hours(f64::NAN),
Err(Error::NotFinite)
));
}
#[test]
fn round_trip_units() {
let one_hour = Interval::from_hours(1.0).unwrap();
assert!((one_hour.seconds() - 3600.0).abs() < 1e-12);
assert!((one_hour.minutes() - 60.0).abs() < 1e-12);
assert!((one_hour.millis() - 3_600_000.0).abs() < 1e-9);
assert!((one_hour.days() - 1.0 / 24.0).abs() < 1e-15);
}
#[test]
fn arithmetic() {
let a = Interval::from_hours(2.0).unwrap();
let b = Interval::from_minutes(30.0).unwrap();
assert!(((a + b).hours() - 2.5).abs() < 1e-12);
assert!(((a - b).hours() - 1.5).abs() < 1e-12);
assert!(((-a).seconds() - -7200.0).abs() < 1e-12);
}
}