use super::super::common::get_tai_utc_offset; use super::ut1_tai::{ToTAIWithOffset, ToUT1WithOffset};
use super::utc_tai::{calendar_to_julian, julian_to_calendar};
use super::{ToTAI, ToUTC};
use crate::julian::JulianDate;
use crate::scales::{UT1, UTC};
use crate::TimeResult;
use celestial_core::constants::SECONDS_PER_DAY_F64;
pub trait ToUT1WithDUT1 {
fn to_ut1_with_dut1(&self, dut1_seconds: f64) -> TimeResult<UT1>;
}
pub trait ToUTCWithDUT1 {
fn to_utc_with_dut1(&self, dut1_seconds: f64) -> TimeResult<UTC>;
}
impl ToUT1WithDUT1 for UTC {
fn to_ut1_with_dut1(&self, dut1_seconds: f64) -> TimeResult<UT1> {
let tai = self.to_tai()?;
let utc_jd = self.to_julian_date();
let (year, month, day, day_fraction) = julian_to_calendar(utc_jd.jd1(), utc_jd.jd2())?;
let tai_utc_seconds = get_tai_utc_offset(year, month, day, day_fraction);
let ut1_tai_offset = dut1_seconds - tai_utc_seconds;
tai.to_ut1_with_offset(ut1_tai_offset)
}
}
fn adjust_dut1_for_leap_second(jd_big: f64, jd_small: f64, dut1: f64) -> TimeResult<f64> {
let mut duts = dut1;
let mut prev_offset = 0.0;
for i in -1..=3 {
let jd_frac = jd_small + i as f64;
let (year, month, day, _) = julian_to_calendar(jd_big, jd_frac)?;
let curr_offset = get_tai_utc_offset(year, month, day, 0.0);
if i == -1 {
prev_offset = curr_offset;
continue;
}
let delta = curr_offset - prev_offset;
if delta.abs() < 0.5 {
prev_offset = curr_offset;
continue;
}
if delta * duts >= 0.0 {
duts -= delta;
}
let (leap_d1, leap_d2) = calendar_to_julian(year, month, day);
let time_past_leap =
(jd_big - leap_d1) + (jd_small - (leap_d2 - 1.0 + duts / SECONDS_PER_DAY_F64));
if time_past_leap > 0.0 {
let fraction =
(time_past_leap * SECONDS_PER_DAY_F64 / (SECONDS_PER_DAY_F64 + delta)).min(1.0);
duts += delta * fraction;
}
break;
}
Ok(duts)
}
impl ToUTCWithDUT1 for UT1 {
fn to_utc_with_dut1(&self, dut1_seconds: f64) -> TimeResult<UTC> {
let ut1_jd = self.to_julian_date();
let (big, small, big_first) = if ut1_jd.jd1().abs() >= ut1_jd.jd2().abs() {
(ut1_jd.jd1(), ut1_jd.jd2(), true)
} else {
(ut1_jd.jd2(), ut1_jd.jd1(), false)
};
let adjusted_dut1 = adjust_dut1_for_leap_second(big, small, dut1_seconds)?;
let small_corrected = small - adjusted_dut1 / SECONDS_PER_DAY_F64;
let (utc_jd1, utc_jd2) = if big_first {
(big, small_corrected)
} else {
(small_corrected, big)
};
Ok(UTC::from_julian_date(JulianDate::new(utc_jd1, utc_jd2)))
}
}
pub trait ToUTCViaTAI {
fn to_utc_via_tai_with_dut1(&self, dut1_seconds: f64) -> TimeResult<UTC>;
}
impl ToUTCViaTAI for UT1 {
fn to_utc_via_tai_with_dut1(&self, dut1_seconds: f64) -> TimeResult<UTC> {
let ut1_jd = self.to_julian_date();
let (year, month, day, day_fraction) = julian_to_calendar(ut1_jd.jd1(), ut1_jd.jd2())?;
let tai_utc_seconds = get_tai_utc_offset(year, month, day, day_fraction);
let ut1_tai_offset = dut1_seconds - tai_utc_seconds;
let tai = self.to_tai_with_offset(ut1_tai_offset)?;
tai.to_utc()
}
}
#[cfg(test)]
mod tests {
use super::super::{ToUT1, ToUTC};
use super::*;
use celestial_core::constants::J2000_JD;
#[test]
fn test_identity_conversions() {
let ut1 = UT1::from_julian_date(JulianDate::new(J2000_JD, 0.5));
let identity_ut1 = ut1.to_ut1().unwrap();
assert_eq!(
ut1.to_julian_date().jd1(),
identity_ut1.to_julian_date().jd1()
);
assert_eq!(
ut1.to_julian_date().jd2(),
identity_ut1.to_julian_date().jd2()
);
let utc = UTC::from_julian_date(JulianDate::new(J2000_JD, 0.5));
let identity_utc = utc.to_utc().unwrap();
assert_eq!(
utc.to_julian_date().jd1(),
identity_utc.to_julian_date().jd1()
);
assert_eq!(
utc.to_julian_date().jd2(),
identity_utc.to_julian_date().jd2()
);
}
#[test]
fn test_dut1_offset_relationship() {
let dut1_values = [-0.9, 0.0, 0.9];
for dut1 in dut1_values {
let utc = UTC::from_julian_date(JulianDate::new(J2000_JD, 0.0));
let ut1 = utc.to_ut1_with_dut1(dut1).unwrap();
let ut1_jd = ut1.to_julian_date().to_f64();
let diff_seconds = (ut1_jd - J2000_JD) * SECONDS_PER_DAY_F64;
assert!(
diff_seconds > -1.0 && diff_seconds < 1.0,
"DUT1={}: UT1-UTC difference should be within 1 second: {} seconds",
dut1,
diff_seconds
);
let ut1_reverse = UT1::from_julian_date(JulianDate::new(J2000_JD, 0.0));
let utc_reverse = ut1_reverse.to_utc_with_dut1(dut1).unwrap();
let utc_jd = utc_reverse.to_julian_date().to_f64();
let reverse_diff = (J2000_JD - utc_jd) * SECONDS_PER_DAY_F64;
assert!(
(reverse_diff - dut1).abs() < 0.1,
"DUT1={}: UTC should be behind UT1 by ~DUT1: {} seconds",
dut1,
reverse_diff
);
}
let ut1_normal = UT1::from_julian_date(JulianDate::new(J2000_JD, 0.5));
let utc_normal = ut1_normal.to_utc_with_dut1(0.3).unwrap();
assert!(
utc_normal.to_julian_date().jd1().abs() > utc_normal.to_julian_date().jd2().abs(),
"Should preserve larger JD1 component"
);
let ut1_flipped = UT1::from_julian_date(JulianDate::new(0.1, J2000_JD));
let utc_flipped = ut1_flipped.to_utc_with_dut1(0.3).unwrap();
assert!(
utc_flipped.to_julian_date().jd2().abs() > utc_flipped.to_julian_date().jd1().abs(),
"Should preserve larger JD2 component"
);
}
#[test]
fn test_utc_ut1_round_trip_precision() {
let tolerance = 1e-14;
let jd_splits = [
(J2000_JD, 0.123456789),
(J2000_JD, 0.5),
(0.1, J2000_JD),
(J2000_JD, 0.999999999),
(J2000_JD, 0.25),
];
let dut1_values = [-0.9, 0.0, 0.9];
for (jd1, jd2) in jd_splits {
for dut1 in dut1_values {
let original_utc = UTC::from_julian_date(JulianDate::new(jd1, jd2));
let ut1 = original_utc.to_ut1_with_dut1(dut1).unwrap();
let round_trip_utc = ut1.to_utc_with_dut1(dut1).unwrap();
let diff = (original_utc.to_julian_date().to_f64()
- round_trip_utc.to_julian_date().to_f64())
.abs();
assert!(
diff < tolerance,
"UTC->UT1->UTC round trip (jd1={}, jd2={}, dut1={}): {:.2e} days exceeds {:.0e}",
jd1,
jd2,
dut1,
diff,
tolerance
);
let original_ut1 = UT1::from_julian_date(JulianDate::new(jd1, jd2));
let utc = original_ut1.to_utc_with_dut1(dut1).unwrap();
let round_trip_ut1 = utc.to_ut1_with_dut1(dut1).unwrap();
let diff_reverse = (original_ut1.to_julian_date().to_f64()
- round_trip_ut1.to_julian_date().to_f64())
.abs();
assert!(
diff_reverse < tolerance,
"UT1->UTC->UT1 round trip (jd1={}, jd2={}, dut1={}): {:.2e} days exceeds {:.0e}",
jd1,
jd2,
dut1,
diff_reverse,
tolerance
);
}
}
}
#[test]
fn test_leap_second_boundary_handling() {
let leap_dates = [
(2441499.5, "1972-07-01"),
(2441683.5, "1973-01-01"),
(2442048.5, "1974-01-01"),
];
for (jd, description) in leap_dates {
let ut1_at_leap = UT1::from_julian_date(JulianDate::new(jd, 0.0));
let utc = ut1_at_leap.to_utc_with_dut1(0.0).unwrap();
assert!(
utc.to_julian_date().to_f64() > 0.0,
"{}: Should produce valid UTC at leap second",
description
);
let ut1_after_leap = UT1::from_julian_date(JulianDate::new(jd, 0.001));
let utc_after = ut1_after_leap.to_utc_with_dut1(0.0).unwrap();
assert!(
utc_after.to_julian_date().to_f64() > jd,
"{}: UTC should be after leap second start",
description
);
let utc_direct = ut1_at_leap.to_utc_with_dut1(0.0).unwrap();
let utc_via_tai = ut1_at_leap.to_utc_via_tai_with_dut1(0.0).unwrap();
let diff = (utc_direct.to_julian_date().to_f64()
- utc_via_tai.to_julian_date().to_f64())
.abs();
assert!(
diff < 1e-14,
"{}: Direct vs TAI-intermediate should match: {:.2e} days",
description,
diff
);
}
}
}