use core::{fmt, mem::MaybeUninit};
use supernovas_ffi::{
novas_set_split_time, novas_set_unix_time, novas_timescale,
novas_timescale::{NOVAS_TT, NOVAS_UTC},
novas_timespec,
};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy)]
pub struct Time(novas_timespec);
impl PartialEq for Time {
fn eq(&self, other: &Self) -> bool {
self.0.ijd_tt == other.0.ijd_tt
&& self.0.fjd_tt == other.0.fjd_tt
&& self.0.tt2tdb == other.0.tt2tdb
&& self.0.ut1_to_tt == other.0.ut1_to_tt
}
}
impl Time {
pub fn from_jd(scale: novas_timescale, jd: f64, leap_seconds: i32, dut1: f64) -> Result<Self> {
if !jd.is_finite() || !dut1.is_finite() {
return Err(Error::NotFinite);
}
let ijd = jd.floor() as i64;
let fjd = jd - ijd as f64;
Self::from_split_jd(scale, ijd, fjd, leap_seconds, dut1)
}
pub fn from_utc_jd(jd_utc: f64, leap_seconds: i32, dut1: f64) -> Result<Self> {
Self::from_jd(NOVAS_UTC, jd_utc, leap_seconds, dut1)
}
pub fn from_tt_jd(jd_tt: f64, leap_seconds: i32, dut1: f64) -> Result<Self> {
Self::from_jd(NOVAS_TT, jd_tt, leap_seconds, dut1)
}
pub fn from_split_jd(
scale: novas_timescale,
ijd: i64,
fjd: f64,
leap_seconds: i32,
dut1: f64,
) -> Result<Self> {
if !fjd.is_finite() || !dut1.is_finite() {
return Err(Error::NotFinite);
}
let mut ts = MaybeUninit::<novas_timespec>::zeroed();
let rc = unsafe {
novas_set_split_time(scale, ijd as _, fjd, leap_seconds, dut1, ts.as_mut_ptr())
};
if rc != 0 {
return Err(Error::Parse);
}
Ok(Time(unsafe { ts.assume_init() }))
}
pub fn from_unix(secs: i64, nanos: i64, leap_seconds: i32, dut1: f64) -> Result<Self> {
if !dut1.is_finite() {
return Err(Error::NotFinite);
}
let mut ts = MaybeUninit::<novas_timespec>::zeroed();
let rc = unsafe {
novas_set_unix_time(secs as _, nanos as _, leap_seconds, dut1, ts.as_mut_ptr())
};
if rc != 0 {
return Err(Error::Parse);
}
Ok(Time(unsafe { ts.assume_init() }))
}
pub fn tt_jd(self) -> f64 {
self.0.ijd_tt as f64 + self.0.fjd_tt
}
pub fn tt_split_jd(self) -> (i64, f64) {
#[allow(clippy::useless_conversion)]
(i64::from(self.0.ijd_tt), self.0.fjd_tt)
}
pub fn as_timespec(&self) -> &novas_timespec {
&self.0
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals = f.precision().unwrap_or(6);
write!(f, "JD {:.decimals$} TT", self.tt_jd())
}
}
impl approx::AbsDiffEq for Time {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
1e-6 / 86_400.0
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
let dij = (self.0.ijd_tt - other.0.ijd_tt) as f64;
let dfj = self.0.fjd_tt - other.0.fjd_tt;
(dij + dfj).abs() <= epsilon
}
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
const JD_J2000_TT: f64 = 2_451_545.0;
#[test]
fn rejects_non_finite() {
assert!(matches!(
Time::from_utc_jd(f64::NAN, 37, 0.0),
Err(Error::NotFinite)
));
assert!(matches!(
Time::from_utc_jd(2_451_545.0, 37, f64::INFINITY),
Err(Error::NotFinite)
));
}
#[test]
fn round_trip_tt_jd() {
let t = Time::from_tt_jd(JD_J2000_TT, 32, 0.0).unwrap();
assert!((t.tt_jd() - JD_J2000_TT).abs() < 1e-9);
}
#[test]
fn utc_and_tt_differ_by_tai_offset() {
let utc = Time::from_utc_jd(JD_J2000_TT, 32, 0.0).unwrap();
let tt = Time::from_tt_jd(JD_J2000_TT, 32, 0.0).unwrap();
let (_, fjd_utc) = utc.tt_split_jd();
let (_, fjd_tt) = tt.tt_split_jd();
let diff_seconds = (fjd_utc - fjd_tt) * 86_400.0;
assert!(
(diff_seconds - 64.184).abs() < 1e-12,
"expected TT - UTC = 64.184 s at J2000.0 with leap=32, got {diff_seconds}"
);
}
#[test]
fn split_jd_preserves_precision() {
let t = Time::from_tt_jd(JD_J2000_TT, 32, 0.0).unwrap();
let (ijd, fjd) = t.tt_split_jd();
assert!((ijd as f64 + fjd - JD_J2000_TT).abs() < 1e-12);
}
#[test]
fn unix_epoch_is_1970() {
let t = Time::from_unix(0, 0, 0, 0.0).unwrap();
let expected_tt = 2_440_587.5 + 32.184 / 86_400.0;
assert_abs_diff_eq!(t.tt_jd(), expected_tt, epsilon = 1e-9);
}
}