use crate::constants::TT_TAI_OFFSET;
use crate::julian::JulianDate;
use crate::scales::{TCB, TDB};
use crate::TimeResult;
use celestial_core::constants::{MJD_ZERO_POINT, SECONDS_PER_DAY_F64};
const TCB_RATE: f64 = 1.550519768e-8;
const MJD_1977: f64 = 43144.0;
const TDB_OFFSET: f64 = -6.55e-5;
const T77TD: f64 = MJD_ZERO_POINT + MJD_1977;
const T77TF: f64 = TT_TAI_OFFSET / SECONDS_PER_DAY_F64;
const TDB0: f64 = TDB_OFFSET / SECONDS_PER_DAY_F64;
const TCB_RATE_RATIO: f64 = TCB_RATE / (1.0 - TCB_RATE);
pub trait TcbToTdb {
fn tcb_to_tdb(&self) -> TimeResult<TDB>;
}
pub trait TdbToTcb {
fn tdb_to_tcb(&self) -> TimeResult<TCB>;
}
impl TcbToTdb for TCB {
fn tcb_to_tdb(&self) -> TimeResult<TDB> {
let tcb_jd = self.to_julian_date();
let (tcb1, tcb2) = (tcb_jd.jd1(), tcb_jd.jd2());
let (big, small) = if tcb1.abs() > tcb2.abs() {
(tcb1, tcb2)
} else {
(tcb2, tcb1)
};
let d = big - T77TD;
let corrected = small + TDB0 - (d + (small - T77TF)) * TCB_RATE;
let (tdb1, tdb2) = if tcb1.abs() > tcb2.abs() {
(big, corrected)
} else {
(corrected, big)
};
Ok(TDB::from_julian_date(JulianDate::new(tdb1, tdb2)))
}
}
impl TdbToTcb for TDB {
fn tdb_to_tcb(&self) -> TimeResult<TCB> {
let tdb_jd = self.to_julian_date();
let (tdb1, tdb2) = (tdb_jd.jd1(), tdb_jd.jd2());
let (big, small) = if tdb1.abs() > tdb2.abs() {
(tdb1, tdb2)
} else {
(tdb2, tdb1)
};
let d = T77TD - big;
let f = small - TDB0;
let corrected = f - (d - (f - T77TF)) * TCB_RATE_RATIO;
let (tcb1, tcb2) = if tdb1.abs() > tdb2.abs() {
(big, corrected)
} else {
(corrected, big)
};
Ok(TCB::from_julian_date(JulianDate::new(tcb1, tcb2)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use celestial_core::constants::J2000_JD;
#[test]
fn test_tcb_tdb_relationship() {
let tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
let tdb = tcb.tcb_to_tdb().unwrap();
let tcb_jd = tcb.to_julian_date();
let tdb_jd = tdb.to_julian_date();
assert!(
tdb_jd.to_f64() < tcb_jd.to_f64(),
"TDB should be behind TCB at J2000"
);
let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
let tcb = tdb.tdb_to_tcb().unwrap();
let tdb_jd = tdb.to_julian_date();
let tcb_jd = tcb.to_julian_date();
assert!(
tcb_jd.to_f64() > tdb_jd.to_f64(),
"TCB should be ahead of TDB at J2000"
);
}
#[test]
fn test_tcb_tdb_round_trip_precision() {
const TOLERANCE_DAYS: f64 = 1e-14;
let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
for jd2 in test_jd2_values {
let original_tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, jd2));
let tdb = original_tcb.tcb_to_tdb().unwrap();
let round_trip_tcb = tdb.tdb_to_tcb().unwrap();
assert_eq!(
original_tcb.to_julian_date().jd1(),
round_trip_tcb.to_julian_date().jd1(),
"TCB->TDB->TCB JD1 must be exact for jd2={}",
jd2
);
let jd2_diff =
(original_tcb.to_julian_date().jd2() - round_trip_tcb.to_julian_date().jd2()).abs();
assert!(
jd2_diff <= TOLERANCE_DAYS,
"TCB->TDB->TCB JD2 diff {} exceeds tolerance {} for jd2={}",
jd2_diff,
TOLERANCE_DAYS,
jd2
);
let original_tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, jd2));
let tcb = original_tdb.tdb_to_tcb().unwrap();
let round_trip_tdb = tcb.tcb_to_tdb().unwrap();
assert_eq!(
original_tdb.to_julian_date().jd1(),
round_trip_tdb.to_julian_date().jd1(),
"TDB->TCB->TDB JD1 must be exact for jd2={}",
jd2
);
let jd2_diff =
(original_tdb.to_julian_date().jd2() - round_trip_tdb.to_julian_date().jd2()).abs();
assert!(
jd2_diff <= TOLERANCE_DAYS,
"TDB->TCB->TDB JD2 diff {} exceeds tolerance {} for jd2={}",
jd2_diff,
TOLERANCE_DAYS,
jd2
);
}
let alt_tcb = TCB::from_julian_date(JulianDate::new(0.5, J2000_JD));
let alt_tdb = alt_tcb.tcb_to_tdb().unwrap();
let alt_round_trip = alt_tdb.tdb_to_tcb().unwrap();
assert_eq!(
alt_tcb.to_julian_date().jd1(),
alt_round_trip.to_julian_date().jd1(),
"Alternate split TCB->TDB->TCB JD1 must be exact"
);
let jd2_diff =
(alt_tcb.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();
assert!(
jd2_diff <= TOLERANCE_DAYS,
"Alternate split TCB->TDB->TCB JD2 diff {} exceeds tolerance {}",
jd2_diff,
TOLERANCE_DAYS
);
}
}