pub const JD_J2000: f64 = 2_451_545.0;
pub const MJD_OFFSET: f64 = 2_400_000.5;
pub const TT_MINUS_TAI: f64 = 32.184;
pub const SECONDS_PER_DAY: f64 = 86_400.0;
#[allow(clippy::excessive_precision)]
pub const ERA_TURNS_AT_J2000: f64 = 0.7790572732640;
#[allow(clippy::excessive_precision)]
pub const ERA_TURNS_PER_UT1_DAY: f64 = 1.00273781191135448;
pub fn julian_date(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: f64) -> f64 {
let (y, m) = if month <= 2 {
(year - 1, month as i32 + 12)
} else {
(year, month as i32)
};
let a = (y as f64 / 100.0).floor();
let b = 2.0 - a + (a / 4.0).floor();
let day_fraction =
day as f64 + (hour as f64 * 3600.0 + minute as f64 * 60.0 + second) / SECONDS_PER_DAY;
(365.25 * (y as f64 + 4716.0)).floor() + (30.6001 * (m as f64 + 1.0)).floor() + day_fraction + b
- 1524.5
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CivilTime {
pub year: i32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
pub second: f64,
}
pub fn civil_from_jd(jd: f64) -> CivilTime {
let jd2 = jd + 0.5;
let z = jd2.floor();
let f = jd2 - z;
let a = if z < 2_299_161.0 {
z
} else {
let alpha = ((z - 1_867_216.25) / 36_524.25).floor();
z + 1.0 + alpha - (alpha / 4.0).floor()
};
let b = a + 1524.0;
let c = ((b - 122.1) / 365.25).floor();
let d = (365.25 * c).floor();
let e = ((b - d) / 30.6001).floor();
let day_with_frac = b - d - (30.6001 * e).floor() + f;
let day = day_with_frac.floor();
let month = if e < 14.0 { e - 1.0 } else { e - 13.0 };
let year = if month > 2.0 { c - 4716.0 } else { c - 4715.0 };
let mut secs = (day_with_frac - day) * SECONDS_PER_DAY;
if secs < 0.0 {
secs = 0.0;
}
let hour = (secs / 3600.0).floor();
secs -= hour * 3600.0;
let minute = (secs / 60.0).floor();
secs -= minute * 60.0;
CivilTime {
year: year as i32,
month: month as u32,
day: day as u32,
hour: hour as u32,
minute: minute as u32,
second: secs,
}
}
pub fn mjd_from_jd(jd: f64) -> f64 {
jd - MJD_OFFSET
}
const LEAP_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_minus_utc(jd_utc: f64) -> f64 {
let mut secs = LEAP_TABLE[0].3;
for &(y, m, d, s) in LEAP_TABLE {
let jd_entry = julian_date(y, m, d, 0, 0, 0.0);
if jd_utc >= jd_entry {
secs = s;
} else {
break;
}
}
secs
}
pub fn utc_to_tai(jd_utc: f64) -> f64 {
jd_utc + tai_minus_utc(jd_utc) / SECONDS_PER_DAY
}
pub fn tai_to_tt(jd_tai: f64) -> f64 {
jd_tai + TT_MINUS_TAI / SECONDS_PER_DAY
}
pub fn utc_to_tt(jd_utc: f64) -> f64 {
tai_to_tt(utc_to_tai(jd_utc))
}
pub fn utc_to_ut1(jd_utc: f64, dut1_seconds: f64) -> f64 {
jd_utc + dut1_seconds / SECONDS_PER_DAY
}
pub fn earth_rotation_angle(jd_ut1: f64) -> f64 {
let tu = jd_ut1 - JD_J2000;
let turns = ERA_TURNS_AT_J2000 + ERA_TURNS_PER_UT1_DAY * tu;
let frac = turns - turns.floor(); 2.0 * std::f64::consts::PI * frac
}
#[cfg(test)]
mod tests {
use super::*;
const EPS_JD: f64 = 1e-9;
#[test]
fn j2000_epoch_is_2451545() {
assert!((julian_date(2000, 1, 1, 12, 0, 0.0) - JD_J2000).abs() < EPS_JD);
assert!((julian_date(2000, 1, 1, 0, 0, 0.0) - 2_451_544.5).abs() < EPS_JD);
}
#[test]
fn known_julian_dates() {
assert!((julian_date(1957, 10, 4, 19, 26, 24.0) - 2_436_116.31).abs() < 1e-2); assert!((julian_date(2000, 1, 1, 0, 0, 0.0) - 2_451_544.5).abs() < EPS_JD);
assert!((julian_date(1999, 1, 1, 0, 0, 0.0) - 2_451_179.5).abs() < EPS_JD);
assert!((julian_date(1987, 1, 27, 0, 0, 0.0) - 2_446_822.5).abs() < EPS_JD);
assert!(mjd_from_jd(julian_date(1858, 11, 17, 0, 0, 0.0)).abs() < EPS_JD);
}
#[test]
fn julian_date_round_trips_through_civil() {
for &(y, mo, d, h, mi, s) in &[
(2000, 1, 1, 12, 0, 0.0),
(2026, 6, 2, 17, 45, 30.0),
(1972, 1, 1, 0, 0, 0.0),
(1999, 12, 31, 23, 59, 59.0),
(2024, 2, 29, 6, 30, 0.0), ] {
let jd = julian_date(y, mo, d, h, mi, s);
let c = civil_from_jd(jd);
assert_eq!(
(c.year, c.month, c.day, c.hour, c.minute),
(y, mo, d, h, mi)
);
assert!(
(c.second - s).abs() < 1e-3,
"second mismatch for {y}-{mo}-{d}: {} vs {s}",
c.second
);
}
}
#[test]
fn leap_seconds_match_iers_history() {
let at = |y, m, d| tai_minus_utc(julian_date(y, m, d, 0, 0, 0.0));
assert_eq!(at(1972, 1, 1), 10.0);
assert_eq!(at(1999, 1, 1), 32.0);
assert_eq!(at(2006, 1, 1), 33.0);
assert_eq!(at(2009, 1, 1), 34.0);
assert_eq!(at(2015, 7, 1), 36.0);
assert_eq!(at(2017, 1, 1), 37.0);
assert_eq!(at(2026, 6, 2), 37.0); assert_eq!(at(2016, 12, 31), 36.0);
assert_eq!(at(1998, 6, 1), 31.0);
assert_eq!(at(1970, 1, 1), 10.0);
}
#[test]
fn tt_minus_utc_is_leap_plus_offset() {
let jd_utc = julian_date(2020, 1, 1, 0, 0, 0.0);
let tt = utc_to_tt(jd_utc);
let delta_s = (tt - jd_utc) * SECONDS_PER_DAY;
assert!(
(delta_s - (37.0 + 32.184)).abs() < 1e-4,
"TT-UTC = {delta_s}"
);
let jd_tai = utc_to_tai(jd_utc);
assert!(((tai_to_tt(jd_tai) - jd_tai) * SECONDS_PER_DAY - 32.184).abs() < 1e-4);
}
#[test]
fn ut1_applies_dut1() {
let jd_utc = julian_date(2020, 1, 1, 0, 0, 0.0);
let dut1 = -0.1772; let jd_ut1 = utc_to_ut1(jd_utc, dut1);
assert!(((jd_ut1 - jd_utc) * SECONDS_PER_DAY - dut1).abs() < 1e-4);
}
#[test]
fn era_at_j2000_matches_iau_value() {
let era = earth_rotation_angle(JD_J2000);
let expect = 2.0 * std::f64::consts::PI * ERA_TURNS_AT_J2000;
assert!((era - expect).abs() < 1e-12, "ERA(J2000) = {era}");
let deg = era.to_degrees();
assert!((deg - 280.4606).abs() < 1e-3, "ERA(J2000) = {deg} deg");
}
#[test]
fn era_advances_one_sidereal_turn_per_ut1_day() {
let jd = JD_J2000 + 100.0;
let d0 = earth_rotation_angle(jd);
let d1 = earth_rotation_angle(jd + 1.0);
let two_pi = 2.0 * std::f64::consts::PI;
let mut delta = d1 - d0;
if delta < 0.0 {
delta += two_pi; }
let expect = two_pi * ERA_TURNS_PER_UT1_DAY.fract();
assert!(
(delta - expect).abs() < 1e-9,
"daily ERA advance = {delta}, want {expect}"
);
assert!((0.0..two_pi).contains(&d0) && (0.0..two_pi).contains(&d1));
}
}