use super::super::common::{get_tai_utc_offset, next_calendar_day};
use super::{ToTAI, ToUTC};
use crate::julian::JulianDate;
use crate::scales::{TAI, UTC};
use crate::{TimeError, TimeResult};
use celestial_core::constants::{MJD_ZERO_POINT, SECONDS_PER_DAY_F64};
impl ToTAI for UTC {
fn to_tai(&self) -> TimeResult<TAI> {
utc_to_tai(self.to_julian_date())
}
}
impl ToUTC for TAI {
fn to_utc(&self) -> TimeResult<UTC> {
tai_to_utc(self.to_julian_date())
}
}
impl ToUTC for UTC {
fn to_utc(&self) -> TimeResult<UTC> {
Ok(*self)
}
}
pub fn utc_to_tai(utc_jd: JulianDate) -> TimeResult<TAI> {
let (utc_int, utc_frac, big1) = if utc_jd.jd1().abs() >= utc_jd.jd2().abs() {
(utc_jd.jd1(), utc_jd.jd2(), true)
} else {
(utc_jd.jd2(), utc_jd.jd1(), false)
};
let (year, month, day, mut day_fraction) = julian_to_calendar(utc_int, utc_frac)?;
let offset_0h = get_tai_utc_offset(year, month, day, 0.0);
let offset_12h = get_tai_utc_offset(year, month, day, 0.5);
let (next_year, next_month, next_day) = next_calendar_day(year, month, day)?;
let offset_24h = get_tai_utc_offset(next_year, next_month, next_day, 0.0);
let drift_rate = 2.0 * (offset_12h - offset_0h);
let leap_amount = offset_24h - (offset_0h + drift_rate);
day_fraction *= (SECONDS_PER_DAY_F64 + leap_amount) / SECONDS_PER_DAY_F64;
day_fraction *= (SECONDS_PER_DAY_F64 + drift_rate) / SECONDS_PER_DAY_F64;
let (z1, z2) = calendar_to_julian(year, month, day);
let mut tai_frac = z1 - utc_int;
tai_frac += z2;
tai_frac += day_fraction + offset_0h / SECONDS_PER_DAY_F64;
let (tai_jd1, tai_jd2) = if big1 {
(utc_int, tai_frac)
} else {
(tai_frac, utc_int)
};
Ok(TAI::from_julian_date(JulianDate::new(tai_jd1, tai_jd2)))
}
pub fn tai_to_utc(tai_jd: JulianDate) -> TimeResult<UTC> {
const TAI_TO_UTC_ITERATIONS: usize = 3;
let (tai_int, tai_frac, big1) = if tai_jd.jd1().abs() >= tai_jd.jd2().abs() {
(tai_jd.jd1(), tai_jd.jd2(), true)
} else {
(tai_jd.jd2(), tai_jd.jd1(), false)
};
let utc_int = tai_int;
let mut utc_frac = tai_frac;
for _ in 0..TAI_TO_UTC_ITERATIONS {
let guess_tai = utc_to_tai_jd(utc_int, utc_frac)?;
utc_frac += tai_int - guess_tai.jd1();
utc_frac += tai_frac - guess_tai.jd2();
}
let (utc_jd1, utc_jd2) = if big1 {
(utc_int, utc_frac)
} else {
(utc_frac, utc_int)
};
Ok(UTC::from_julian_date(JulianDate::new(utc_jd1, utc_jd2)))
}
fn utc_to_tai_jd(utc_int: f64, utc_frac: f64) -> TimeResult<JulianDate> {
let utc = UTC::from_julian_date(JulianDate::new(utc_int, utc_frac));
let tai = utc.to_tai()?;
Ok(tai.to_julian_date())
}
pub fn julian_to_calendar(jd1: f64, jd2: f64) -> TimeResult<(i32, i32, i32, f64)> {
let dj = jd1 + jd2;
const DJMIN: f64 = -68569.5;
const DJMAX: f64 = 1e9;
if !(DJMIN..=DJMAX).contains(&dj) {
return Err(TimeError::ConversionError(format!(
"Julian Date {} out of valid range [{}, {}]",
dj, DJMIN, DJMAX
)));
}
fn nearest_int(a: f64) -> f64 {
if a.abs() < 0.5 {
0.0
} else if a < 0.0 {
libm::ceil(a - 0.5)
} else {
libm::floor(a + 0.5)
}
}
let day_int_1 = nearest_int(jd1);
let frac_1 = jd1 - day_int_1;
let mut jd = day_int_1 as i64;
let day_int_2 = nearest_int(jd2);
let frac_2 = jd2 - day_int_2;
jd += day_int_2 as i64;
let mut sum = 0.5;
let mut correction = 0.0;
let fractions = [frac_1, frac_2];
for frac in fractions.iter() {
let temp = sum + frac;
correction += if sum.abs() >= frac.abs() {
(sum - temp) + frac
} else {
(frac - temp) + sum
};
sum = temp;
if sum >= 1.0 {
jd += 1;
sum -= 1.0;
}
}
let mut fraction = sum + correction;
correction = fraction - sum;
if fraction < 0.0 {
fraction = sum + 1.0;
correction += (1.0 - fraction) + sum;
sum = fraction;
fraction = sum + correction;
correction = fraction - sum;
jd -= 1
}
#[allow(clippy::excessive_precision)]
const DBL_EPSILON: f64 = 2.220_446_049_250_313_1e-16;
if (fraction - 1.0) >= -DBL_EPSILON / 4.0 {
let temp = sum - 1.0;
correction += (sum - temp) - 1.0;
sum = temp;
fraction = sum + correction;
if (-DBL_EPSILON / 2.0) < fraction {
jd += 1;
fraction = if fraction > 0.0 { fraction } else { 0.0 };
}
}
let mut l = jd + 68569_i64;
let n = (4_i64 * l) / 146097_i64;
l -= (146097_i64 * n + 3_i64) / 4_i64;
let i = (4000_i64 * (l + 1_i64)) / 1461001_i64;
l -= (1461_i64 * i) / 4_i64 - 31_i64;
let k = (80_i64 * l) / 2447_i64;
let day = (l - (2447_i64 * k) / 80_i64) as i32;
let l_final = k / 11_i64;
let month = (k + 2_i64 - 12_i64 * l_final) as i32;
let year = (100_i64 * (n - 49_i64) + i + l_final) as i32;
Ok((year, month, day, fraction))
}
pub fn calendar_to_julian(year: i32, month: i32, day: i32) -> (f64, f64) {
let my = (month - 14) / 12;
let iypmy = year + my;
let modified_jd = ((1461 * (iypmy + 4800)) / 4 + (367 * (month - 2 - 12 * my)) / 12
- (3 * ((iypmy + 4900) / 100)) / 4
+ day
- 2432076) as f64;
(MJD_ZERO_POINT, modified_jd)
}
#[cfg(test)]
mod tests {
use super::*;
use celestial_core::constants::J2000_JD;
#[test]
fn test_identity_conversions() {
let utc = UTC::from_julian_date(JulianDate::new(J2000_JD, 0.5));
let result = utc.to_utc().unwrap();
assert_eq!(utc.to_julian_date().jd1(), result.to_julian_date().jd1());
assert_eq!(utc.to_julian_date().jd2(), result.to_julian_date().jd2());
}
#[test]
fn test_utc_tai_leap_second_offset() {
assert_eq!(get_tai_utc_offset(1972, 1, 1, 0.0), 10.0); assert_eq!(get_tai_utc_offset(1980, 1, 1, 0.0), 19.0);
assert_eq!(get_tai_utc_offset(1999, 1, 1, 0.0), 32.0);
assert_eq!(get_tai_utc_offset(2017, 1, 1, 0.0), 37.0);
let offset_1970 = get_tai_utc_offset(1970, 6, 15, 0.5);
assert!(offset_1970 > 0.0 && offset_1970 < 15.0);
assert_eq!(get_tai_utc_offset(1959, 12, 31, 0.0), 0.0); assert_eq!(get_tai_utc_offset(2000, 1, 1, 1.5), 0.0); assert_eq!(get_tai_utc_offset(2000, 1, 1, -0.5), 0.0); assert_eq!(get_tai_utc_offset(1960, 0, 1, 0.0), 0.0); }
#[test]
fn test_utc_tai_round_trip_precision() {
const TOLERANCE: f64 = 1e-14;
let test_cases: &[(f64, f64)] = &[
(J2000_JD, 0.123456789),
(J2000_JD, 0.0),
(J2000_JD, 0.999999),
(0.5, J2000_JD), (0.1, celestial_core::constants::J2000_JD), ];
for &(jd1, jd2) in test_cases {
let utc = UTC::from_julian_date(JulianDate::new(jd1, jd2));
let tai = utc.to_tai().unwrap();
let utc_back = tai.to_utc().unwrap();
let diff_utc =
(utc.to_julian_date().to_f64() - utc_back.to_julian_date().to_f64()).abs();
assert!(
diff_utc < TOLERANCE,
"UTC->TAI->UTC failed for ({}, {}): diff={:.2e}",
jd1,
jd2,
diff_utc
);
let tai = TAI::from_julian_date(JulianDate::new(jd1, jd2));
let utc = tai.to_utc().unwrap();
let tai_back = utc.to_tai().unwrap();
let diff_tai =
(tai.to_julian_date().to_f64() - tai_back.to_julian_date().to_f64()).abs();
assert!(
diff_tai < TOLERANCE,
"TAI->UTC->TAI failed for ({}, {}): diff={:.2e}",
jd1,
jd2,
diff_tai
);
}
}
#[test]
fn test_calendar_helper_functions() {
assert_eq!(next_calendar_day(2000, 1, 31).unwrap(), (2000, 2, 1));
assert_eq!(next_calendar_day(2000, 12, 31).unwrap(), (2001, 1, 1));
assert_eq!(next_calendar_day(2000, 2, 29).unwrap(), (2000, 3, 1));
assert_eq!(next_calendar_day(2000, 2, 28).unwrap(), (2000, 2, 29));
assert_eq!(next_calendar_day(1900, 2, 28).unwrap(), (1900, 3, 1));
assert_eq!(next_calendar_day(2000, 4, 30).unwrap(), (2000, 5, 1));
assert_eq!(next_calendar_day(2000, 6, 30).unwrap(), (2000, 7, 1));
assert_eq!(next_calendar_day(2000, 9, 30).unwrap(), (2000, 10, 1));
assert_eq!(next_calendar_day(2000, 11, 30).unwrap(), (2000, 12, 1));
assert!(next_calendar_day(2000, 0, 1).is_err());
assert!(next_calendar_day(2000, 13, 1).is_err());
assert!(next_calendar_day(2000, -1, 1).is_err());
assert!(julian_to_calendar(1e10, 0.0).is_err());
assert!(julian_to_calendar(-1e6, 0.0).is_err());
let (y, m, d, frac) =
julian_to_calendar(celestial_core::constants::J2000_JD, -0.6).unwrap();
assert!(y > 0 && (1..=12).contains(&m) && (1..=31).contains(&d) && frac >= 0.0);
let (y, m, d, frac) = julian_to_calendar(2451544.6, 0.2).unwrap();
assert!(y > 0 && (1..=12).contains(&m) && (1..=31).contains(&d));
assert!(frac >= 0.0 && frac <= 1.0);
let (y, m, d, frac) = julian_to_calendar(2451544.75, 0.75).unwrap();
assert!(y > 0 && (1..=12).contains(&m) && (1..=31).contains(&d));
assert!(frac >= 0.0 && frac <= 1.0);
}
}