use chrono::{DateTime, Utc, NaiveDate};
const TT_TAI_SECONDS: f64 = 32.184;
static LEAP_SECOND_TABLE: &[(i32, u32, u32, f64)] = &[
(1972, 1, 1, 10.0), (1972, 7, 1, 11.0),
(1973, 1, 1, 12.0),
(1974, 1, 1, 13.0),
(1975, 1, 1, 14.0),
(1976, 1, 1, 15.0),
(1977, 1, 1, 16.0),
(1978, 1, 1, 17.0),
(1979, 1, 1, 18.0),
(1980, 1, 1, 19.0),
(1981, 7, 1, 20.0),
(1982, 7, 1, 21.0),
(1983, 7, 1, 22.0),
(1985, 7, 1, 23.0),
(1988, 1, 1, 24.0),
(1990, 1, 1, 25.0),
(1991, 1, 1, 26.0),
(1992, 7, 1, 27.0),
(1993, 7, 1, 28.0),
(1994, 7, 1, 29.0),
(1996, 1, 1, 30.0),
(1997, 7, 1, 31.0),
(1999, 1, 1, 32.0),
(2006, 1, 1, 33.0),
(2009, 1, 1, 34.0),
(2012, 7, 1, 35.0),
(2015, 7, 1, 36.0),
(2017, 1, 1, 37.0), ];
pub fn tai_utc_offset_for_date(date: NaiveDate) -> f64 {
let mut current_offset = 10.0;
for &(year, month, day, offset) in LEAP_SECOND_TABLE {
let leap_date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
if date >= leap_date {
current_offset = offset;
} else {
break;
}
}
current_offset
}
pub fn tai_utc_offset() -> f64 {
let now = Utc::now();
tai_utc_offset_for_datetime(now)
}
pub fn tai_utc_offset_for_datetime(datetime: DateTime<Utc>) -> f64 {
tai_utc_offset_for_date(datetime.date_naive())
}
pub fn tt_utc_offset_seconds() -> f64 {
tai_utc_offset() + TT_TAI_SECONDS
}
pub fn tt_utc_offset_jd() -> f64 {
tt_utc_offset_seconds() / 86400.0
}
pub fn utc_to_tt_jd(jd_utc: f64) -> f64 {
jd_utc + tt_utc_offset_jd()
}
pub fn utc_to_tt_jd_for_date(jd_utc: f64) -> f64 {
let days_since_j2000 = jd_utc - 2451545.0;
let j2000_date = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
let target_date = j2000_date + chrono::Duration::days(days_since_j2000.round() as i64);
let tai_utc = tai_utc_offset_for_date(target_date);
let tt_utc_seconds = tai_utc + TT_TAI_SECONDS;
let tt_utc_jd = tt_utc_seconds / 86400.0;
jd_utc + tt_utc_jd
}
pub fn tt_to_utc_jd(jd_tt: f64) -> f64 {
jd_tt - tt_utc_offset_jd()
}
pub fn split_jd_for_erfa(jd: f64) -> (f64, f64) {
let jd1 = jd.floor() + 0.5; let jd2 = jd - jd1; (jd1, jd2)
}
pub fn check_time_offset_accuracy(hardcoded_seconds: f64) -> f64 {
tt_utc_offset_seconds() - hardcoded_seconds
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tai_utc_offset_current() {
let offset = tai_utc_offset();
assert_eq!(offset, 37.0, "TAI-UTC offset should be 37 seconds as of 2025");
}
#[test]
fn test_tt_tai_constant() {
assert_eq!(TT_TAI_SECONDS, 32.184, "TT-TAI must be exactly 32.184 seconds");
}
#[test]
fn test_tt_utc_offset_calculation() {
let tt_utc = tt_utc_offset_seconds();
let expected = tai_utc_offset() + TT_TAI_SECONDS;
assert_eq!(tt_utc, expected, "TT-UTC should equal TAI-UTC + 32.184");
assert_eq!(tt_utc, 69.184, "TT-UTC should be 69.184 seconds as of 2025");
}
#[test]
fn test_utc_tt_conversion_roundtrip() {
let jd_utc = 2460888.5;
let jd_tt = utc_to_tt_jd(jd_utc);
let jd_utc_back = tt_to_utc_jd(jd_tt);
assert!((jd_utc - jd_utc_back).abs() < 1e-12,
"Round-trip conversion should preserve precision");
let diff_seconds = (jd_tt - jd_utc) * 86400.0;
let expected_offset = tt_utc_offset_seconds();
println!("JD UTC: {:.9}", jd_utc);
println!("JD TT: {:.9}", jd_tt);
println!("Diff seconds: {:.9}", diff_seconds);
println!("Expected offset: {:.9}", expected_offset);
println!("Error: {:.2e} seconds", (diff_seconds - expected_offset).abs());
assert!((diff_seconds - expected_offset).abs() < 0.0002,
"JD difference should match TT-UTC offset within 0.2ms: got {:.9} expected {:.9}",
diff_seconds, expected_offset);
}
#[test]
fn test_jd_splitting() {
let jd = 2460888.75;
let (jd1, jd2) = split_jd_for_erfa(jd);
assert!((jd - (jd1 + jd2)).abs() < 1e-15, "JD splitting should be exact");
assert!((jd1 % 1.0 - 0.5).abs() < 1e-15, "jd1 should end in .5");
}
#[test]
fn test_hardcoded_offset_accuracy() {
let error = check_time_offset_accuracy(69.184);
assert!(error.abs() < 0.001,
"Current hardcoded 69.184 should be within 1ms of computed value, got {} seconds error", error);
}
#[test]
fn test_outdated_offset_detection() {
let error = check_time_offset_accuracy(67.0);
assert!(error.abs() > 2.0,
"Should detect when hardcoded value is significantly outdated");
}
#[test]
fn test_leap_second_table_lookup() {
let date_1971 = NaiveDate::from_ymd_opt(1971, 12, 31).unwrap();
assert_eq!(tai_utc_offset_for_date(date_1971), 10.0);
let date_1972_jun = NaiveDate::from_ymd_opt(1972, 6, 30).unwrap();
assert_eq!(tai_utc_offset_for_date(date_1972_jun), 10.0);
let date_1972_jul = NaiveDate::from_ymd_opt(1972, 7, 1).unwrap();
assert_eq!(tai_utc_offset_for_date(date_1972_jul), 11.0);
let date_2016 = NaiveDate::from_ymd_opt(2016, 12, 31).unwrap();
assert_eq!(tai_utc_offset_for_date(date_2016), 36.0);
let date_2017 = NaiveDate::from_ymd_opt(2017, 1, 1).unwrap();
assert_eq!(tai_utc_offset_for_date(date_2017), 37.0);
let date_2025 = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
assert_eq!(tai_utc_offset_for_date(date_2025), 37.0);
}
#[test]
fn test_datetime_based_conversion() {
use chrono::TimeZone;
let dt_j2000 = Utc.with_ymd_and_hms(2000, 1, 1, 12, 0, 0).unwrap();
let offset_j2000 = tai_utc_offset_for_datetime(dt_j2000);
assert_eq!(offset_j2000, 32.0, "TAI-UTC at J2000.0 should be 32 seconds");
let dt_2025 = Utc.with_ymd_and_hms(2025, 6, 15, 0, 0, 0).unwrap();
let offset_2025 = tai_utc_offset_for_datetime(dt_2025);
assert_eq!(offset_2025, 37.0, "TAI-UTC in 2025 should be 37 seconds");
}
#[test]
fn test_historical_jd_conversion_accuracy() {
let jd_j2000_utc = 2451545.0;
let jd_j2000_tt = utc_to_tt_jd_for_date(jd_j2000_utc);
let expected_offset_seconds = 32.0 + 32.184;
let expected_offset_jd = expected_offset_seconds / 86400.0;
let expected_jd_tt = jd_j2000_utc + expected_offset_jd;
assert!((jd_j2000_tt - expected_jd_tt).abs() < 1e-10,
"J2000.0 conversion should use correct leap second value: got {:.9}, expected {:.9}",
jd_j2000_tt, expected_jd_tt);
}
}