use crate::astro::eop::{EopError, EopProvider, EopValues, IersEop};
use crate::astro::sidereal::gmst_iau2006;
use crate::qtty::*;
use crate::time::{JulianDate, TimeContext, TT, UT1};
#[inline]
pub fn jd_ut1_from_tt(jd_tt: JulianDate) -> JulianDate {
let jd_tt: tempoch::JulianDate<crate::time::TT> = jd_tt;
let ut1 = jd_tt
.to_with::<UT1>(&TimeContext::new())
.expect("TT->UT1 conversion should succeed within the bundled model horizon");
crate::time::JulianDate::new(ut1.to::<tempoch::JD>().raw().value())
}
#[inline]
pub fn try_jd_utc_from_tt(jd_tt: JulianDate) -> Result<JulianDate, EopError> {
let jd_tt_t: tempoch::JulianDate<TT> = jd_tt;
let ctx = TimeContext::with_builtin_eop();
let ut1 = jd_tt_t
.to_with::<UT1>(&ctx)
.expect("TT->UT1 conversion should succeed within the bundled model horizon");
let ut1_jd = ut1.to::<tempoch::JD>().raw().value();
let mut utc_jd = ut1_jd;
for _ in 0..3 {
let mjd_utc = Days::new(utc_jd - 2_400_000.5);
let Some(eop) = tempoch::eop::builtin_eop_at(mjd_utc) else {
return Err(EopError::NoData {
jd_utc: utc_jd,
mjd_utc: mjd_utc.value(),
});
};
utc_jd = ut1_jd - eop.ut1_minus_utc.to::<Day>().value();
}
Ok(crate::time::JulianDate::new(utc_jd))
}
#[inline]
pub fn jd_utc_from_tt_delta_t(jd_tt: JulianDate) -> JulianDate {
jd_ut1_from_tt(jd_tt)
}
#[inline]
pub fn jd_utc_from_tt(jd_tt: JulianDate) -> JulianDate {
jd_utc_from_tt_delta_t(jd_tt)
}
#[inline]
pub fn jd_ut1_from_tt_eop(jd_tt: JulianDate, eop: &EopValues) -> JulianDate {
let jd_tt_t: tempoch::JulianDate<TT> = jd_tt;
let ctx = TimeContext::with_builtin_eop();
let ut1 = jd_tt_t
.to_with::<UT1>(&ctx)
.expect("TT->UT1 conversion should succeed within the bundled model horizon");
let ut1_jd = ut1.to::<tempoch::JD>().raw().value();
let mjd_query = qtty::Day::new(ut1_jd - 2_400_000.5);
let bundled_dut1 = ctx
.ut1_minus_utc(mjd_query)
.map(|s| s.value())
.unwrap_or(0.0);
let residual_days = (eop.dut1.value() - bundled_dut1) / 86_400.0;
crate::time::JulianDate::new(ut1_jd + residual_days)
}
#[inline]
pub fn gmst_from_tt(jd_tt: JulianDate) -> Radians {
let jd_ut1 = jd_ut1_from_tt(jd_tt);
gmst_iau2006(jd_ut1, jd_tt)
}
#[inline]
pub fn gmst_from_tt_eop(jd_tt: JulianDate, eop: &EopValues) -> Radians {
let jd_ut1 = jd_ut1_from_tt_eop(jd_tt, eop);
gmst_iau2006(jd_ut1, jd_tt)
}
#[inline]
pub fn gmst_default(jd_tt: JulianDate) -> Radians {
gmst_from_tt(jd_tt)
}
#[inline]
pub fn try_gmst_with_eop(jd_tt: JulianDate) -> Result<Radians, EopError> {
let jd_utc = try_jd_utc_from_tt(jd_tt)?;
let eop = IersEop::new().try_eop_at(jd_utc)?;
Ok(gmst_from_tt_eop(jd_tt, &eop))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::eop::EopValues;
use std::f64::consts::TAU;
const JD_J2000: f64 = tempoch::J2000_JD_TT_DAY.value();
fn jd() -> JulianDate {
crate::time::JulianDate::new(JD_J2000)
}
#[test]
fn jd_ut1_is_close_to_tt_at_modern_epoch() {
let jd_ut1 = jd_ut1_from_tt(jd());
let diff_sec = (jd().raw().value() - jd_ut1.raw().value()) * 86400.0;
assert!(
diff_sec > 50.0 && diff_sec < 80.0,
"ΔT at J2000 expected ~63s, got {diff_sec}s"
);
}
#[test]
fn try_jd_utc_returns_no_data_without_runtime_eop() {
assert!(matches!(
try_jd_utc_from_tt(jd()),
Err(EopError::NoData { .. })
));
}
#[test]
fn jd_utc_delta_t_differs_from_tt_at_j2000() {
let jd_utc = jd_utc_from_tt_delta_t(jd());
let diff_sec = (jd().raw().value() - jd_utc.raw().value()) * 86_400.0;
assert!(
diff_sec > 60.0 && diff_sec < 70.0,
"TT-UTC at J2000 should include leap seconds + 32.184s, got {diff_sec}s"
);
}
#[test]
fn jd_ut1_eop_zero_dut1_returns_utc() {
use crate::qtty::Seconds;
let eop_zero = EopValues::default();
let eop_nonzero = EopValues {
dut1: Seconds::new(0.5),
..Default::default()
};
let jd_zero = jd_ut1_from_tt_eop(jd(), &eop_zero);
let jd_nonzero = jd_ut1_from_tt_eop(jd(), &eop_nonzero);
let diff_sec = (jd_nonzero.raw().value() - jd_zero.raw().value()).abs() * 86_400.0;
assert!(
(diff_sec - 0.5).abs() < 0.01,
"nonzero dut1 should shift UT1 by ~0.5s, got {diff_sec}s"
);
}
#[test]
fn jd_ut1_eop_nonzero_dut1_applies_correction() {
use crate::qtty::Seconds;
let dut1_s = 0.3_f64;
let eop = EopValues {
dut1: Seconds::new(dut1_s),
..Default::default()
};
let jd_eop = jd_ut1_from_tt_eop(jd(), &eop);
let jd_delta_t = jd_ut1_from_tt(jd());
let recovered_dut1_s = (jd_eop.raw().value() - jd_delta_t.raw().value()) * 86_400.0;
assert!(
(recovered_dut1_s - dut1_s).abs() < 1e-4,
"residual dut1 should equal supplied value ({dut1_s}s), got {recovered_dut1_s}s"
);
}
#[test]
fn jd_ut1_eop_negative_dut1() {
use crate::qtty::Seconds;
let eop = EopValues {
dut1: Seconds::new(-0.5),
..Default::default()
};
let jd_eop = jd_ut1_from_tt_eop(jd(), &eop);
assert!(jd_eop.raw().value().is_finite());
}
#[test]
fn gmst_from_tt_is_in_range() {
let gmst = gmst_from_tt(jd());
assert!(
gmst.value() >= 0.0 && gmst.value() < TAU,
"GMST out of [0, 2π): {}",
gmst.value()
);
}
#[test]
fn gmst_from_tt_varies_with_time() {
let gmst1 = gmst_from_tt(jd());
let gmst2 = gmst_from_tt(crate::time::JulianDate::new(JD_J2000 + 1.0));
let diff = (gmst2.value() - gmst1.value()).abs();
assert!(diff > 0.0, "GMST should change over 1 day");
}
#[test]
fn gmst_from_tt_eop_null_eop_uses_utc_axis() {
use crate::qtty::Seconds;
let eop_zero = EopValues::default();
let eop_nonzero = EopValues {
dut1: Seconds::new(0.3),
..Default::default()
};
let gmst_zero = gmst_from_tt_eop(jd(), &eop_zero);
let gmst_nonzero = gmst_from_tt_eop(jd(), &eop_nonzero);
assert!(
gmst_zero.value() >= 0.0 && gmst_zero.value() < TAU,
"EOP-driven GMST out of [0, 2π): {}",
gmst_zero.value()
);
let diff = (gmst_nonzero.value() - gmst_zero.value()).abs();
let diff = diff.min((TAU - diff).abs());
assert!(
diff > 1e-6,
"EOP dut1=0.3s should shift GMST by ~2×10⁻⁵ rad, got {diff} rad"
);
}
#[test]
fn gmst_from_tt_eop_with_nonzero_dut1() {
use crate::qtty::Seconds;
let eop = EopValues {
dut1: Seconds::new(0.2),
..Default::default()
};
let gmst_eop = gmst_from_tt_eop(jd(), &eop);
assert!(gmst_eop.is_finite());
assert!(gmst_eop >= Radians::new(0.0));
}
#[test]
fn gmst_default_matches_delta_t_path() {
let jd_val = jd();
assert!((gmst_default(jd_val).value() - gmst_from_tt(jd_val).value()).abs() < 1e-15);
}
#[test]
fn try_gmst_with_eop_requires_runtime_bundle() {
assert!(matches!(
try_gmst_with_eop(jd()),
Err(EopError::NoData { .. })
));
}
}