use crate::astro::eop::{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");
JulianDate::new(ut1.to::<tempoch::JD>().raw().value())
}
#[inline]
pub fn jd_utc_from_tt(jd_tt: JulianDate) -> JulianDate {
let jd_tt: tempoch::JulianDate<TT> = jd_tt;
let ctx = TimeContext::with_builtin_eop();
let ut1 = jd_tt
.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 {
break;
};
utc_jd = ut1_jd - eop.ut1_minus_utc.to::<Day>().value();
}
JulianDate::new(utc_jd)
}
#[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;
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 {
let jd_utc = jd_utc_from_tt(jd_tt);
let eop = IersEop::new().eop_at(jd_utc);
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 = 2451545.0;
fn jd() -> JulianDate {
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().jd_value() - jd_ut1.jd_value()) * 86400.0;
assert!(
diff_sec > 50.0 && diff_sec < 80.0,
"ΔT at J2000 expected ~63s, got {diff_sec}s"
);
}
#[test]
fn jd_ut1_eop_zero_dut1_returns_utc() {
let eop = EopValues::default(); let jd_eop = jd_ut1_from_tt_eop(jd(), &eop);
let jd_tt_t: tempoch::JulianDate<TT> = jd();
let ctx = TimeContext::with_builtin_eop();
let bundled_ut1 = jd_tt_t.to_with::<UT1>(&ctx).expect("bundled UT1");
let bundled_ut1_jd = bundled_ut1.to::<tempoch::JD>().raw().value();
let bundled_dut1 = ctx
.ut1_minus_utc(qtty::Day::new(bundled_ut1_jd - 2_400_000.5))
.map(|s| s.value())
.unwrap_or(0.0);
let expected_utc_jd = bundled_ut1_jd - bundled_dut1 / 86_400.0;
assert!(
(jd_eop.jd_value() - expected_utc_jd).abs() < 1e-12,
"with dut1 = 0, UT1 must equal UTC, got {} vs UTC {}",
jd_eop.jd_value(),
expected_utc_jd
);
let jd_dt = jd_ut1_from_tt(jd());
let diff_sec = (jd_eop.jd_value() - jd_dt.jd_value()).abs() * 86_400.0;
assert!(
diff_sec > 0.05,
"EOP(dut1=0) and ΔT models should differ measurably, 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_tt_t: tempoch::JulianDate<TT> = jd();
let ctx = TimeContext::with_builtin_eop();
let bundled_ut1 = jd_tt_t.to_with::<UT1>(&ctx).expect("bundled UT1");
let bundled_ut1_jd = bundled_ut1.to::<tempoch::JD>().raw().value();
let bundled_dut1 = ctx
.ut1_minus_utc(qtty::Day::new(bundled_ut1_jd - 2_400_000.5))
.map(|s| s.value())
.unwrap_or(0.0);
let expected_utc_jd = bundled_ut1_jd - bundled_dut1 / 86_400.0;
let recovered_dut1_s = (jd_eop.jd_value() - expected_utc_jd) * 86_400.0;
assert!(
(recovered_dut1_s - dut1_s).abs() < 1e-4,
"UT1-UTC should equal the supplied dUT1 ({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.jd_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(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() {
let eop = EopValues::default();
let gmst_eop = gmst_from_tt_eop(jd(), &eop);
let gmst_dt = gmst_from_tt(jd());
assert!(
gmst_eop.value() >= 0.0 && gmst_eop.value() < TAU,
"EOP-driven GMST out of [0, 2π): {}",
gmst_eop.value()
);
let diff = (gmst_eop.value() - gmst_dt.value()).abs();
let diff = diff.min((TAU - diff).abs());
assert!(
diff > 1e-6,
"EOP(dut1=0) and ΔT-based GMST should differ by ~ω·dUT1, 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_builtin_eop_path() {
let jd_val = jd();
let g1 = gmst_default(jd_val);
let jd_utc = jd_utc_from_tt(jd_val);
let eop = IersEop::new().eop_at(jd_utc);
let g2 = gmst_from_tt_eop(jd_val, &eop);
assert!((g1.value() - g2.value()).abs() < 1e-15);
}
#[test]
fn tt_to_utc_conversion_is_not_identity() {
let jd_utc = jd_utc_from_tt(jd());
let diff_sec = (jd().jd_value() - jd_utc.jd_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"
);
}
}