use super::ToTT;
use crate::{
constants::FAIRHD,
julian::JulianDate,
scales::{TDB, TT},
};
use crate::{TimeError, TimeResult};
use celestial_core::constants::{
DAYS_PER_JULIAN_MILLENNIUM, DEG_TO_RAD, J2000_JD, SECONDS_PER_DAY_F64, TWOPI,
};
use celestial_core::math::fmod;
use celestial_core::Location;
fn greenwich_location() -> Location {
Location::from_degrees(51.477928, 0.0, 46.0).expect("Greenwich coordinates should be valid")
}
const DEFAULT_UT1_OFFSET_SECONDS: f64 = 0.0;
pub fn compute_tdb_tt_offset(
date_jd: &JulianDate,
ut1_fraction: f64,
location: &Location,
) -> TimeResult<f64> {
let (u, v) = location.to_geocentric_km()?;
let dtr = calculate_tdb_tt_difference(
date_jd.jd1(),
date_jd.jd2(),
ut1_fraction,
location.longitude,
u,
v,
);
Ok(dtr)
}
pub trait ToTDB {
fn to_tdb_greenwich(&self) -> TimeResult<TDB>;
fn to_tdb_with_location(&self, location: &Location) -> TimeResult<TDB>;
fn to_tdb_with_location_and_ut1_offset(
&self,
location: &Location,
ut1_offset_seconds: f64,
) -> TimeResult<TDB>;
fn to_tdb_with_offset(&self, dtr_seconds: f64) -> TimeResult<TDB>;
}
impl ToTDB for TDB {
fn to_tdb_greenwich(&self) -> TimeResult<TDB> {
Ok(*self)
}
fn to_tdb_with_location(&self, _location: &Location) -> TimeResult<TDB> {
Ok(*self)
}
fn to_tdb_with_location_and_ut1_offset(
&self,
_location: &Location,
_ut1_offset_seconds: f64,
) -> TimeResult<TDB> {
Ok(*self)
}
fn to_tdb_with_offset(&self, _dtr_seconds: f64) -> TimeResult<TDB> {
Ok(*self)
}
}
impl ToTDB for TT {
fn to_tdb_greenwich(&self) -> TimeResult<TDB> {
let location = greenwich_location();
self.to_tdb_with_location_and_ut1_offset(&location, DEFAULT_UT1_OFFSET_SECONDS)
}
fn to_tdb_with_location(&self, location: &Location) -> TimeResult<TDB> {
self.to_tdb_with_location_and_ut1_offset(location, DEFAULT_UT1_OFFSET_SECONDS)
}
fn to_tdb_with_location_and_ut1_offset(
&self,
location: &Location,
ut1_offset_seconds: f64,
) -> TimeResult<TDB> {
let tt_jd = self.to_julian_date();
let tt_f64 = tt_jd.to_f64();
let ut1_fraction = ((tt_f64 - libm::trunc(tt_f64)) * SECONDS_PER_DAY_F64
+ ut1_offset_seconds)
/ SECONDS_PER_DAY_F64;
let ut1_fraction = ut1_fraction - libm::floor(ut1_fraction);
let dtr = compute_tdb_tt_offset(&tt_jd, ut1_fraction, location)?;
self.to_tdb_with_offset(dtr)
}
fn to_tdb_with_offset(&self, dtr_seconds: f64) -> TimeResult<TDB> {
let tt_jd = self.to_julian_date();
let dtr_days = dtr_seconds / celestial_core::constants::SECONDS_PER_DAY_F64;
let (tdb_jd1, tdb_jd2) = if tt_jd.jd1().abs() > tt_jd.jd2().abs() {
(tt_jd.jd1(), tt_jd.jd2() + dtr_days)
} else {
(tt_jd.jd1() + dtr_days, tt_jd.jd2())
};
let tdb_jd = JulianDate::new(tdb_jd1, tdb_jd2);
Ok(TDB::from_julian_date(tdb_jd))
}
}
impl ToTT for TDB {
fn to_tt(&self) -> TimeResult<TT> {
Err(TimeError::ConversionError(
"TDB→TT conversion requires observer location. \
Use to_tt_greenwich() for Greenwich or to_tt_with_location() for other locations."
.to_string(),
))
}
}
pub trait ToTTFromTDB {
fn to_tt_greenwich(&self) -> TimeResult<TT>;
fn to_tt_with_location(&self, location: &Location) -> TimeResult<TT>;
fn to_tt_with_location_and_ut1_offset(
&self,
location: &Location,
ut1_offset_seconds: f64,
) -> TimeResult<TT>;
fn to_tt_with_offset(&self, dtr_seconds: f64) -> TimeResult<TT>;
}
impl ToTTFromTDB for TDB {
fn to_tt_greenwich(&self) -> TimeResult<TT> {
let location = greenwich_location();
self.to_tt_with_location_and_ut1_offset(&location, DEFAULT_UT1_OFFSET_SECONDS)
}
fn to_tt_with_location(&self, location: &Location) -> TimeResult<TT> {
self.to_tt_with_location_and_ut1_offset(location, DEFAULT_UT1_OFFSET_SECONDS)
}
fn to_tt_with_location_and_ut1_offset(
&self,
location: &Location,
ut1_offset_seconds: f64,
) -> TimeResult<TT> {
let tdb_jd = self.to_julian_date();
let tdb_f64 = tdb_jd.to_f64();
let ut1_fraction_approx = ((tdb_f64 - libm::trunc(tdb_f64)) * SECONDS_PER_DAY_F64
+ ut1_offset_seconds)
/ SECONDS_PER_DAY_F64;
let ut1_fraction_approx = ut1_fraction_approx - libm::floor(ut1_fraction_approx);
let dtr_approx = compute_tdb_tt_offset(&tdb_jd, ut1_fraction_approx, location)?;
let tt_approx = self.to_tt_with_offset(dtr_approx)?;
let tt_jd_approx = tt_approx.to_julian_date();
let tt_jd_approx_f64 = tt_jd_approx.jd1() + tt_jd_approx.jd2();
let ut1_fraction = ((tt_jd_approx_f64 - libm::trunc(tt_jd_approx_f64))
* SECONDS_PER_DAY_F64
+ ut1_offset_seconds)
/ SECONDS_PER_DAY_F64;
let ut1_fraction = ut1_fraction - libm::floor(ut1_fraction);
let dtr_refined = compute_tdb_tt_offset(&tt_jd_approx, ut1_fraction, location)?;
let tt_refined = self.to_tt_with_offset(dtr_refined)?;
let tt_jd_refined = tt_refined.to_julian_date();
let tt_jd_refined_f64 = tt_jd_refined.jd1() + tt_jd_refined.jd2();
let ut1_fraction_final = ((tt_jd_refined_f64 - libm::trunc(tt_jd_refined_f64))
* SECONDS_PER_DAY_F64
+ ut1_offset_seconds)
/ SECONDS_PER_DAY_F64;
let ut1_fraction_final = ut1_fraction_final - libm::floor(ut1_fraction_final);
let dtr_final = compute_tdb_tt_offset(&tt_jd_refined, ut1_fraction_final, location)?;
self.to_tt_with_offset(dtr_final)
}
fn to_tt_with_offset(&self, dtr_seconds: f64) -> TimeResult<TT> {
let tdb_jd = self.to_julian_date();
let dtr_days = dtr_seconds / celestial_core::constants::SECONDS_PER_DAY_F64;
let (tt_jd1, tt_jd2) = if tdb_jd.jd1().abs() > tdb_jd.jd2().abs() {
(tdb_jd.jd1(), tdb_jd.jd2() - dtr_days)
} else {
(tdb_jd.jd1() - dtr_days, tdb_jd.jd2())
};
let tt_jd = JulianDate::new(tt_jd1, tt_jd2);
Ok(TT::from_julian_date(tt_jd))
}
}
fn calculate_tdb_tt_difference(date1: f64, date2: f64, ut: f64, elong: f64, u: f64, v: f64) -> f64 {
let t = ((date1 - J2000_JD) + date2) / DAYS_PER_JULIAN_MILLENNIUM;
let tsol = fmod(ut, 1.0) * TWOPI + elong;
let w = t / 3600.0;
let elsun = fmod(280.46645683 + 1296027711.03429 * w, 360.0) * DEG_TO_RAD;
let emsun = fmod(357.52910918 + 1295965810.481 * w, 360.0) * DEG_TO_RAD;
let d = fmod(297.85019547 + 16029616012.090 * w, 360.0) * DEG_TO_RAD;
let elj = fmod(34.35151874 + 109306899.89453 * w, 360.0) * DEG_TO_RAD;
let els = fmod(50.07744430 + 44046398.47038 * w, 360.0) * DEG_TO_RAD;
let wt = 0.00029e-10 * u * libm::sin(tsol + elsun - els)
+ 0.00100e-10 * u * libm::sin(tsol - 2.0 * emsun)
+ 0.00133e-10 * u * libm::sin(tsol - d)
+ 0.00133e-10 * u * libm::sin(tsol + elsun - elj)
- 0.00229e-10 * u * libm::sin(tsol + 2.0 * elsun + emsun)
- 0.02200e-10 * v * libm::cos(elsun + emsun)
+ 0.05312e-10 * u * libm::sin(tsol - emsun)
- 0.13677e-10 * u * libm::sin(tsol + 2.0 * elsun)
- 1.31840e-10 * v * libm::cos(elsun)
+ 3.17679e-10 * u * libm::sin(tsol);
let mut w0 = 0.0;
for j in (0..474).rev() {
w0 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
}
let mut w1 = 0.0;
for j in (474..679).rev() {
w1 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
}
let mut w2 = 0.0;
for j in (679..764).rev() {
if FAIRHD[j][0] != 0.0 {
w2 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
}
}
let mut w3 = 0.0;
for j in (764..784).rev() {
if FAIRHD[j][0] != 0.0 {
w3 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
}
}
let mut w4 = 0.0;
for j in (784..787).rev() {
if FAIRHD[j][0] != 0.0 {
w4 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
}
}
let wf = t * (t * (t * (t * w4 + w3) + w2) + w1) + w0;
let wj = 0.00065e-6 * libm::sin(6069.776754 * t + 4.021194)
+ 0.00033e-6 * libm::sin(213.299095 * t + 5.543132)
- 0.00196e-6 * libm::sin(6208.294251 * t + 5.696701)
- 0.00173e-6 * libm::sin(74.781599 * t + 2.435900)
+ 0.03638e-6 * t * t;
wt + wf + wj
}
#[cfg(test)]
mod tests {
use super::*;
use celestial_core::constants::{DAYS_PER_JULIAN_CENTURY, J2000_JD};
#[test]
fn test_identity_conversions() {
let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
let location = Location::from_degrees(45.0, 90.0, 100.0).unwrap();
let via_greenwich = tdb.to_tdb_greenwich().unwrap();
assert_eq!(
tdb.to_julian_date().jd1(),
via_greenwich.to_julian_date().jd1(),
"TDB→TDB via Greenwich should preserve JD1"
);
assert_eq!(
tdb.to_julian_date().jd2(),
via_greenwich.to_julian_date().jd2(),
"TDB→TDB via Greenwich should preserve JD2"
);
let via_location = tdb.to_tdb_with_location(&location).unwrap();
assert_eq!(
tdb.to_julian_date().jd1(),
via_location.to_julian_date().jd1(),
"TDB→TDB via location should preserve JD1"
);
assert_eq!(
tdb.to_julian_date().jd2(),
via_location.to_julian_date().jd2(),
"TDB→TDB via location should preserve JD2"
);
let via_ut1_offset = tdb
.to_tdb_with_location_and_ut1_offset(&location, 0.3)
.unwrap();
assert_eq!(
tdb.to_julian_date().jd1(),
via_ut1_offset.to_julian_date().jd1(),
"TDB→TDB via UT1 offset should preserve JD1"
);
assert_eq!(
tdb.to_julian_date().jd2(),
via_ut1_offset.to_julian_date().jd2(),
"TDB→TDB via UT1 offset should preserve JD2"
);
let via_offset = tdb.to_tdb_with_offset(0.001).unwrap();
assert_eq!(
tdb.to_julian_date().jd1(),
via_offset.to_julian_date().jd1(),
"TDB→TDB via offset should preserve JD1"
);
assert_eq!(
tdb.to_julian_date().jd2(),
via_offset.to_julian_date().jd2(),
"TDB→TDB via offset should preserve JD2"
);
}
#[test]
fn test_tt_tdb_offset_verification() {
let test_cases = [
(J2000_JD, "J2000.0", greenwich_location()),
(
J2000_JD - DAYS_PER_JULIAN_CENTURY,
"1900",
greenwich_location(),
),
(J2000_JD + 18262.5, "2050", greenwich_location()),
(
J2000_JD + DAYS_PER_JULIAN_CENTURY,
"2100",
greenwich_location(),
),
(
J2000_JD,
"J2000 Tokyo",
Location::from_degrees(35.6762, 139.6503, 40.0).unwrap(),
),
(
J2000_JD,
"J2000 Sydney",
Location::from_degrees(-33.8688, 151.2093, 58.0).unwrap(),
),
];
for (jd, description, location) in test_cases {
let tt = TT::from_julian_date(JulianDate::new(jd, 0.0));
let tdb = tt.to_tdb_with_location(&location).unwrap();
let diff_seconds = (tdb.to_julian_date().to_f64() - tt.to_julian_date().to_f64())
* SECONDS_PER_DAY_F64;
assert!(
diff_seconds.abs() < 0.002,
"{}: TT→TDB offset should be < 2ms, got {:.6} seconds",
description,
diff_seconds
);
let tdb = TDB::from_julian_date(JulianDate::new(jd, 0.0));
let tt = tdb.to_tt_with_location(&location).unwrap();
let diff_seconds = (tdb.to_julian_date().to_f64() - tt.to_julian_date().to_f64())
* SECONDS_PER_DAY_F64;
assert!(
diff_seconds.abs() < 0.002,
"{}: TDB→TT offset should be < 2ms, got {:.6} seconds",
description,
diff_seconds
);
}
}
#[test]
fn test_tt_tdb_round_trip_precision() {
const TOLERANCE_DAYS: f64 = 1e-11;
let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345];
for jd2 in test_jd2_values {
let original_tt = TT::from_julian_date(JulianDate::new(J2000_JD, jd2));
let tdb = original_tt.to_tdb_greenwich().unwrap();
let round_trip_tt = tdb.to_tt_greenwich().unwrap();
let jd1_diff =
(original_tt.to_julian_date().jd1() - round_trip_tt.to_julian_date().jd1()).abs();
let jd2_diff =
(original_tt.to_julian_date().jd2() - round_trip_tt.to_julian_date().jd2()).abs();
assert!(
jd1_diff < TOLERANCE_DAYS,
"TT→TDB→TT JD1 diff {} exceeds tolerance for jd2={}",
jd1_diff,
jd2
);
assert!(
jd2_diff < TOLERANCE_DAYS,
"TT→TDB→TT JD2 diff {} exceeds tolerance for jd2={}",
jd2_diff,
jd2
);
let original_tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, jd2));
let tt = original_tdb.to_tt_greenwich().unwrap();
let round_trip_tdb = tt.to_tdb_greenwich().unwrap();
let jd1_diff =
(original_tdb.to_julian_date().jd1() - round_trip_tdb.to_julian_date().jd1()).abs();
let jd2_diff =
(original_tdb.to_julian_date().jd2() - round_trip_tdb.to_julian_date().jd2()).abs();
assert!(
jd1_diff < TOLERANCE_DAYS,
"TDB→TT→TDB JD1 diff {} exceeds tolerance for jd2={}",
jd1_diff,
jd2
);
assert!(
jd2_diff < TOLERANCE_DAYS,
"TDB→TT→TDB JD2 diff {} exceeds tolerance for jd2={}",
jd2_diff,
jd2
);
}
let alt_tt = TT::from_julian_date(JulianDate::new(0.5, J2000_JD));
let alt_tdb = alt_tt.to_tdb_greenwich().unwrap();
let alt_round_trip = alt_tdb.to_tt_greenwich().unwrap();
let jd1_diff =
(alt_tt.to_julian_date().jd1() - alt_round_trip.to_julian_date().jd1()).abs();
let jd2_diff =
(alt_tt.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();
assert!(
jd1_diff < TOLERANCE_DAYS,
"Alternate split TT→TDB→TT JD1 diff {} exceeds tolerance",
jd1_diff
);
assert!(
jd2_diff < TOLERANCE_DAYS,
"Alternate split TT→TDB→TT JD2 diff {} exceeds tolerance",
jd2_diff
);
}
#[test]
fn test_api_equivalence() {
let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.123456));
let tdb_greenwich = tt.to_tdb_greenwich().unwrap();
let tdb_explicit = tt.to_tdb_with_location(&greenwich_location()).unwrap();
assert_eq!(
tdb_greenwich.to_julian_date().jd1(),
tdb_explicit.to_julian_date().jd1(),
"TT: to_tdb_greenwich() should match to_tdb_with_location(greenwich) JD1"
);
assert_eq!(
tdb_greenwich.to_julian_date().jd2(),
tdb_explicit.to_julian_date().jd2(),
"TT: to_tdb_greenwich() should match to_tdb_with_location(greenwich) JD2"
);
let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.987654));
let tt_greenwich = tdb.to_tt_greenwich().unwrap();
let tt_explicit = tdb.to_tt_with_location(&greenwich_location()).unwrap();
assert_eq!(
tt_greenwich.to_julian_date().jd1(),
tt_explicit.to_julian_date().jd1(),
"TDB: to_tt_greenwich() should match to_tt_with_location(greenwich) JD1"
);
assert_eq!(
tt_greenwich.to_julian_date().jd2(),
tt_explicit.to_julian_date().jd2(),
"TDB: to_tt_greenwich() should match to_tt_with_location(greenwich) JD2"
);
}
#[test]
fn test_tdb_to_tt_requires_location() {
let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
let result = tdb.to_tt();
assert!(result.is_err(), "TDB.to_tt() should return error");
match result {
Err(TimeError::ConversionError(msg)) => {
assert!(
msg.contains("location"),
"Error message should mention location requirement: {}",
msg
);
assert!(
msg.contains("greenwich") || msg.contains("Greenwich"),
"Error message should mention Greenwich option: {}",
msg
);
}
_ => panic!("Expected ConversionError, got {:?}", result),
}
}
}