apsis-rs/crates/physics/apsis-leap/src/time.rs

93 lines
3.6 KiB
Rust

//! Conversions between UTC and TAI expressed on the Modified Julian Date
//! scale.
use crate::table::{
LEAP_SECONDS, TABLE_COVERS_FROM_MJD, TAI_MINUS_UTC_AFTER_LAST_ENTRY, mjd_from_ymd,
};
/// The Modified Julian Date of the Unix epoch, 1970-01-01T00:00:00.
///
/// MJD carries no time scale of its own, so this constant is the same
/// number whether the instant is read in UTC, TAI, or any other scale.
pub const MJD_AT_UNIX_EPOCH: f64 = 40_587.0;
/// The number of seconds in a day.
const SECONDS_PER_DAY: f64 = 86_400.0;
/// Returns the value of `TAI - UTC` in seconds at the given UTC Modified
/// Julian Date.
///
/// The offset is a step function: it changes at 00:00:00 UTC on the
/// effective date of each leap second and is constant in between. A query
/// exactly on an effective date returns the new value.
///
/// Returns `None` when the instant precedes 1972-01-01, before which UTC
/// used fractional offsets and this table does not apply.
pub fn tai_minus_utc_at_mjd(utc_mjd: f64) -> Option<f64> {
if !utc_mjd.is_finite() || utc_mjd < TABLE_COVERS_FROM_MJD {
return None;
}
let mut offset = TAI_MINUS_UTC_AFTER_LAST_ENTRY;
for entry in LEAP_SECONDS {
if utc_mjd >= entry.effective_mjd() {
offset = entry.tai_minus_utc;
} else {
break;
}
}
Some(offset)
}
/// Converts a UTC Modified Julian Date to the corresponding TAI Modified
/// Julian Date.
///
/// The conversion adds the offset in force at that instant. Because the
/// offset is a step function evaluated on the UTC side, the conversion is
/// defined for every UTC instant; the inverse conversion is not, because a
/// leap second makes some TAI instants correspond to no UTC instant.
///
/// Returns `None` when the instant precedes 1972-01-01.
pub fn tai_mjd_from_utc_mjd(utc_mjd: f64) -> Option<f64> {
let offset = tai_minus_utc_at_mjd(utc_mjd)?;
Some(utc_mjd + offset / SECONDS_PER_DAY)
}
/// Converts a TAI Modified Julian Date to the corresponding UTC Modified
/// Julian Date.
///
/// Each candidate offset from the table is tried in turn. A candidate is
/// accepted when the offset in force at the resulting UTC instant is the
/// same offset that produced it. At most one offset can satisfy that
/// condition for a given input, so the search is unambiguous.
///
/// Returns `None` in two cases. The first is an instant before the start of
/// the table, where UTC carried fractional offsets that this table does not
/// describe. The second is an instant that falls strictly inside an
/// inserted leap second. Such an instant has no UTC representation, because
/// the UTC clock labels that interval `23:59:60`, a value that does not
/// exist on the continuous Modified Julian Date scale.
pub fn utc_mjd_from_tai_mjd(tai_mjd: f64) -> Option<f64> {
if !tai_mjd.is_finite() {
return None;
}
for entry in LEAP_SECONDS {
let offset = entry.tai_minus_utc;
let candidate = tai_mjd - offset / SECONDS_PER_DAY;
if tai_minus_utc_at_mjd(candidate) == Some(offset) {
return Some(candidate);
}
}
None
}
/// Converts a proleptic Gregorian calendar date at 00:00:00 to a Modified
/// Julian Date.
///
/// This is a convenience re-export of the calendar conversion used to place
/// the leap second table entries. It is provided so that callers do not
/// have to reproduce the conversion, and because the calibration scripts in
/// `validation/` need the same function on both sides of a comparison.
pub const fn calendar_mjd(year: i32, month: u32, day: u32) -> f64 {
mjd_from_ymd(year, month, day)
}