use crate::constants::{
ASEC2RAD, DAY_S, GREGORIAN_START, J2000, TAU, TT_MINUS_TAI, TT_MINUS_TAI_S,
};
use chrono::{self, DateTime, Datelike, Duration, Timelike, Utc};
use nalgebra::Matrix3;
use std::fmt;
use std::ops::{Add, Sub};
use std::sync::{Arc, OnceLock};
use thiserror::Error;
pub mod delta_t;
#[cfg(feature = "python-tests")]
mod python_tests;
#[derive(Debug, Error)]
pub enum TimeError {
#[error("Invalid time format: {0}")]
InvalidFormat(String),
#[error("Time out of range: {0}")]
OutOfRange(String),
#[error("Parsing error: {0}")]
ParseError(String),
#[error("Calendar error: {0}")]
CalendarError(String),
#[error("Leap second data not available")]
LeapSecondDataUnavailable,
}
pub type Result<T> = std::result::Result<T, TimeError>;
#[derive(Debug, Clone, PartialEq)]
pub struct CalendarTuple {
pub year: i32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
pub second: f64,
}
#[derive(Debug, Clone)]
pub struct Timescale(Arc<TimescaleInner>);
#[derive(Debug, Clone)]
struct TimescaleInner {
delta_t_table: Option<(Vec<f64>, Vec<f64>)>,
delta_t_spline: delta_t::DeltaT,
leap_dates: Vec<f64>,
leap_offsets: Vec<i32>,
leap_utc: Option<Vec<f64>>,
leap_tai: Option<Vec<f64>>,
julian_calendar_cutoff: Option<i32>,
polar_motion_table: Option<(Vec<f64>, Vec<f64>, Vec<f64>)>,
}
impl Default for Timescale {
fn default() -> Self {
let mut inner = TimescaleInner {
delta_t_table: None,
delta_t_spline: delta_t::DeltaT::new(),
leap_dates: Vec::new(),
leap_offsets: Vec::new(),
leap_utc: None,
leap_tai: None,
julian_calendar_cutoff: Some(GREGORIAN_START),
polar_motion_table: None,
};
inner.init_basic_leap_seconds();
Timescale(Arc::new(inner))
}
}
impl Timescale {
pub fn new(
delta_t_table: Option<(Vec<f64>, Vec<f64>)>,
leap_dates: Vec<f64>,
leap_offsets: Vec<i32>,
julian_calendar_cutoff: Option<i32>,
) -> Self {
let mut inner = TimescaleInner {
delta_t_table,
delta_t_spline: delta_t::DeltaT::new(),
leap_dates,
leap_offsets,
leap_utc: None,
leap_tai: None,
julian_calendar_cutoff,
polar_motion_table: None,
};
inner.init_leap_second_tables();
Timescale(Arc::new(inner))
}
pub fn set_polar_motion_table(
&mut self,
tt_jd: Vec<f64>,
x_arcsec: Vec<f64>,
y_arcsec: Vec<f64>,
) {
Arc::make_mut(&mut self.0).polar_motion_table = Some((tt_jd, x_arcsec, y_arcsec));
}
pub fn has_polar_motion(&self) -> bool {
self.0.polar_motion_table.is_some()
}
}
impl TimescaleInner {
fn init_basic_leap_seconds(&mut self) {
self.leap_dates.push(2441317.5);
self.leap_offsets.push(10);
self.leap_dates.push(2441499.5); self.leap_offsets.push(11);
self.leap_dates.push(2441683.5); self.leap_offsets.push(12);
self.leap_dates.push(2442048.5); self.leap_offsets.push(13);
self.leap_dates.push(2442413.5); self.leap_offsets.push(14);
self.leap_dates.push(2442778.5); self.leap_offsets.push(15);
self.leap_dates.push(2443144.5); self.leap_offsets.push(16);
self.leap_dates.push(2443509.5); self.leap_offsets.push(17);
self.leap_dates.push(2443874.5); self.leap_offsets.push(18);
self.leap_dates.push(2444239.5); self.leap_offsets.push(19);
self.leap_dates.push(2444786.5); self.leap_offsets.push(20);
self.leap_dates.push(2445151.5); self.leap_offsets.push(21);
self.leap_dates.push(2445516.5); self.leap_offsets.push(22);
self.leap_dates.push(2446247.5); self.leap_offsets.push(23);
self.leap_dates.push(2447161.5); self.leap_offsets.push(24);
self.leap_dates.push(2447892.5); self.leap_offsets.push(25);
self.leap_dates.push(2448257.5); self.leap_offsets.push(26);
self.leap_dates.push(2448804.5); self.leap_offsets.push(27);
self.leap_dates.push(2449169.5); self.leap_offsets.push(28);
self.leap_dates.push(2449534.5); self.leap_offsets.push(29);
self.leap_dates.push(2450083.5); self.leap_offsets.push(30);
self.leap_dates.push(2450630.5); self.leap_offsets.push(31);
self.leap_dates.push(2451179.5); self.leap_offsets.push(32);
self.leap_dates.push(2453736.5); self.leap_offsets.push(33);
self.leap_dates.push(2454832.5); self.leap_offsets.push(34);
self.leap_dates.push(2456109.5); self.leap_offsets.push(35);
self.leap_dates.push(2457204.5); self.leap_offsets.push(36);
self.leap_dates.push(2457754.5); self.leap_offsets.push(37);
self.init_leap_second_tables();
}
fn init_leap_second_tables(&mut self) {
if self.leap_dates.is_empty() || self.leap_offsets.is_empty() {
return;
}
let mut leap_utc = Vec::new();
let mut leap_tai = Vec::new();
for i in 0..self.leap_dates.len() {
let date = self.leap_dates[i];
let offset = self.leap_offsets[i] as f64;
leap_utc.push(date * DAY_S - 1.0);
leap_utc.push(date * DAY_S);
if i > 0 {
leap_tai.push(date * DAY_S - 1.0 + self.leap_offsets[i - 1] as f64);
} else {
leap_tai.push(date * DAY_S - 1.0);
}
leap_tai.push(date * DAY_S + offset);
}
self.leap_utc = Some(leap_utc);
self.leap_tai = Some(leap_tai);
}
}
impl Timescale {
pub fn now(&self) -> Time {
self.from_datetime(Utc::now())
}
pub fn from_datetime(&self, dt: DateTime<Utc>) -> Time {
let calendar_tuple = (
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second() as f64 + dt.nanosecond() as f64 / 1_000_000_000.0,
);
self.utc(calendar_tuple)
}
pub fn utc<T: Into<CalendarInput>>(&self, date: T) -> Time {
let input = date.into();
let (adjusted_input, is_leap_second) = Self::detect_leap_second(input);
let jd = self.calendar_to_jd(&adjusted_input);
let (tai_jd, tai_fraction) = self.utc_to_tai(jd);
let tai_fraction = if is_leap_second {
tai_fraction + 1.0 / DAY_S
} else {
tai_fraction
};
let tt_fraction = tai_fraction + TT_MINUS_TAI;
let mut time = Time {
ts: self.clone(),
whole: tai_jd,
tt_fraction,
tai_fraction: Some(tai_fraction),
ut1_fraction: OnceLock::new(),
tdb_fraction: OnceLock::new(),
delta_t: OnceLock::new(),
shape: None,
leap_second: is_leap_second,
};
time.set_utc_tuple(adjusted_input);
time
}
fn detect_leap_second(input: CalendarInput) -> (CalendarInput, bool) {
match &input {
CalendarInput::Tuple(y, m, d, h, mi, s) => {
if *s >= 60.0 {
(CalendarInput::Tuple(*y, *m, *d, *h, *mi, s - 1.0), true)
} else {
(input, false)
}
}
CalendarInput::CalendarTuple(cal) => {
if cal.second >= 60.0 {
(
CalendarInput::CalendarTuple(CalendarTuple {
year: cal.year,
month: cal.month,
day: cal.day,
hour: cal.hour,
minute: cal.minute,
second: cal.second - 1.0,
}),
true,
)
} else {
(input, false)
}
}
}
}
fn utc_to_tai(&self, jd: f64) -> (f64, f64) {
let whole = jd.floor();
let fraction = jd - whole;
let seconds = whole * DAY_S;
let seconds_fraction = fraction * DAY_S;
let tai_seconds =
seconds + seconds_fraction + self.get_leap_offset(seconds + seconds_fraction);
let tai_jd = tai_seconds / DAY_S;
let tai_whole = tai_jd.floor();
let tai_fraction = tai_jd - tai_whole;
(tai_whole, tai_fraction)
}
fn get_leap_offset(&self, utc_seconds: f64) -> f64 {
if let (Some(leap_utc), Some(leap_tai)) = (&self.0.leap_utc, &self.0.leap_tai) {
if leap_utc.is_empty() || leap_tai.is_empty() {
return 0.0;
}
match leap_utc.binary_search_by(|&time| time.partial_cmp(&utc_seconds).unwrap()) {
Ok(index) => {
leap_tai[index] - leap_utc[index]
}
Err(index) => {
if index == 0 {
0.0 } else if index >= leap_utc.len() {
leap_tai[leap_tai.len() - 1] - leap_utc[leap_utc.len() - 1]
} else {
leap_tai[index - 1] - leap_utc[index - 1]
}
}
}
} else {
0.0
}
}
pub fn tai<T: Into<CalendarInput>>(&self, date: T) -> Time {
let input = date.into();
let (whole, fraction) = self.calendar_to_jd_with_fraction(&input);
let tt_fraction = fraction + TT_MINUS_TAI;
Time {
ts: self.clone(),
whole,
tt_fraction,
tai_fraction: Some(fraction),
ut1_fraction: OnceLock::new(),
tdb_fraction: OnceLock::new(),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
}
}
pub fn tai_jd(&self, jd: f64, fraction: Option<f64>) -> Time {
let (whole, frac) = if let Some(f) = fraction {
(jd, f)
} else {
let whole = jd.floor();
(whole, jd - whole)
};
Time {
ts: self.clone(),
whole,
tt_fraction: frac + TT_MINUS_TAI,
tai_fraction: Some(frac),
ut1_fraction: OnceLock::new(),
tdb_fraction: OnceLock::new(),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
}
}
pub fn tt<T: Into<CalendarInput>>(&self, date: T) -> Time {
let input = date.into();
let (whole, fraction) = self.calendar_to_jd_with_fraction(&input);
Time {
ts: self.clone(),
whole,
tt_fraction: fraction,
tai_fraction: Some(fraction - TT_MINUS_TAI),
ut1_fraction: OnceLock::new(),
tdb_fraction: OnceLock::new(),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
}
}
pub fn tdb<T: Into<CalendarInput>>(&self, date: T) -> Time {
let input = date.into();
let (whole, tdb_frac) = self.calendar_to_jd_with_fraction(&input);
let jd_approx = whole + tdb_frac;
let t = (jd_approx - J2000) / 36525.0;
let tdb_minus_tt_days = (0.001657 * f64::sin(628.3076 * t + 6.2401)
+ 0.000022 * f64::sin(575.3385 * t + 4.2970)
+ 0.000014 * f64::sin(1256.6152 * t + 6.1969)
+ 0.000005 * f64::sin(606.9777 * t + 4.0212)
+ 0.000005 * f64::sin(52.9691 * t + 0.4444)
+ 0.000002 * f64::sin(21.3299 * t + 5.5431)
+ 0.000010 * t * f64::sin(628.3076 * t + 4.2490))
/ DAY_S;
let tt_frac = tdb_frac - tdb_minus_tt_days;
Time {
ts: self.clone(),
whole,
tt_fraction: tt_frac,
tai_fraction: Some(tt_frac - TT_MINUS_TAI),
ut1_fraction: OnceLock::new(),
tdb_fraction: once_with(tdb_frac),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
}
}
pub fn tt_jd(&self, jd: f64, fraction: Option<f64>) -> Time {
let (whole, frac) = if let Some(f) = fraction {
(jd, f)
} else {
let whole = jd.floor();
(whole, jd - whole)
};
Time {
ts: self.clone(),
whole,
tt_fraction: frac,
tai_fraction: Some(frac - TT_MINUS_TAI),
ut1_fraction: OnceLock::new(),
tdb_fraction: OnceLock::new(),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
}
}
pub fn tdb_jd(&self, jd: f64) -> Time {
let whole = jd.floor();
let tdb_frac = jd - whole;
let t = (jd - J2000) / 36525.0;
let tdb_minus_tt_days = (0.001657 * f64::sin(628.3076 * t + 6.2401)
+ 0.000022 * f64::sin(575.3385 * t + 4.2970)
+ 0.000014 * f64::sin(1256.6152 * t + 6.1969)
+ 0.000005 * f64::sin(606.9777 * t + 4.0212)
+ 0.000005 * f64::sin(52.9691 * t + 0.4444)
+ 0.000002 * f64::sin(21.3299 * t + 5.5431)
+ 0.000010 * t * f64::sin(628.3076 * t + 4.2490))
/ DAY_S;
let tt_frac = tdb_frac - tdb_minus_tt_days;
Time {
ts: self.clone(),
whole,
tt_fraction: tt_frac,
tai_fraction: Some(tt_frac - TT_MINUS_TAI),
ut1_fraction: OnceLock::new(),
tdb_fraction: once_with(tdb_frac),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
}
}
pub fn j(&self, year: f64) -> Time {
let tt = year * 365.25 + 1_721_045.0;
self.tt_jd(tt, None)
}
pub fn ut1<T: Into<CalendarInput>>(&self, date: T) -> Time {
let input = date.into();
let jd = self.calendar_to_jd(&input);
let ut1 = jd;
let tt_approx = ut1;
let delta_t_approx = self.delta_t(tt_approx);
let tt_better = ut1 + delta_t_approx / DAY_S;
let delta_t_better = self.delta_t(tt_better);
let delta_t_days = delta_t_better / DAY_S;
let whole = ut1.floor();
let ut1_fraction = ut1 - whole;
let tt_fraction = ut1_fraction + delta_t_days;
Time {
ts: self.clone(),
whole,
tt_fraction,
tai_fraction: Some(tt_fraction - TT_MINUS_TAI),
ut1_fraction: once_with(ut1_fraction),
tdb_fraction: OnceLock::new(),
delta_t: once_with(delta_t_better),
shape: None,
leap_second: false,
}
}
pub fn ut1_jd(&self, jd: f64) -> Time {
let ut1 = jd;
let tt_approx = ut1;
let delta_t_approx = self.delta_t(tt_approx);
let tt_better = ut1 + delta_t_approx / DAY_S;
let delta_t_better = self.delta_t(tt_better);
let delta_t_days = delta_t_better / DAY_S;
let whole = ut1.floor();
let ut1_fraction = ut1 - whole;
let tt_fraction = ut1_fraction + delta_t_days;
Time {
ts: self.clone(),
whole,
tt_fraction,
tai_fraction: Some(tt_fraction - TT_MINUS_TAI),
ut1_fraction: once_with(ut1_fraction),
tdb_fraction: OnceLock::new(),
delta_t: once_with(delta_t_better),
shape: None,
leap_second: false,
}
}
pub fn delta_t(&self, tt: f64) -> f64 {
if let Some((table_tt, table_delta_t)) = &self.0.delta_t_table {
Self::interpolate(tt, table_tt, table_delta_t, f64::NAN, f64::NAN)
} else {
self.0.delta_t_spline.compute(tt)
}
}
pub fn interpolate(
x: f64,
x_values: &[f64],
y_values: &[f64],
extrapolate_low: f64,
extrapolate_high: f64,
) -> f64 {
if x_values.is_empty() || y_values.is_empty() || x_values.len() != y_values.len() {
return f64::NAN;
}
match x_values.binary_search_by(|&val| val.partial_cmp(&x).unwrap()) {
Ok(i) => y_values[i], Err(i) => {
if i == 0 {
if extrapolate_low.is_nan() {
y_values[0]
} else {
extrapolate_low
}
} else if i >= x_values.len() {
if extrapolate_high.is_nan() {
y_values[y_values.len() - 1]
} else {
extrapolate_high
}
} else {
let x0 = x_values[i - 1];
let x1 = x_values[i];
let y0 = y_values[i - 1];
let y1 = y_values[i];
let t = (x - x0) / (x1 - x0);
y0 + t * (y1 - y0)
}
}
}
}
pub fn calendar_to_jd(&self, input: &CalendarInput) -> f64 {
let (whole, fraction) = self.calendar_to_jd_with_fraction(input);
whole + fraction
}
pub fn calendar_to_jd_with_fraction(&self, input: &CalendarInput) -> (f64, f64) {
let (year, month, day, hour, minute, second) = match input {
CalendarInput::Tuple(y, m, d, h, mi, s) => (*y, *m, *d, *h, *mi, *s),
CalendarInput::CalendarTuple(cal) => (
cal.year, cal.month, cal.day, cal.hour, cal.minute, cal.second,
),
};
let jd = self.julian_day(year, month, day);
let day_fraction = (hour as f64 + minute as f64 / 60.0 + second / 3600.0) / 24.0;
(jd as f64 - 0.5, day_fraction)
}
pub fn jd_to_calendar(&self, jd: f64) -> CalendarTuple {
let jd_plus_half = jd + 0.5;
let z = jd_plus_half.floor();
let f = jd_plus_half - z;
let (year, month, day) = self.julian_day_to_calendar_date(z as i32);
let seconds_in_day = f * DAY_S;
let hour = (seconds_in_day / 3600.0).floor() as u32;
let minute = ((seconds_in_day - hour as f64 * 3600.0) / 60.0).floor() as u32;
let second = seconds_in_day - hour as f64 * 3600.0 - minute as f64 * 60.0;
CalendarTuple {
year,
month,
day,
hour,
minute,
second,
}
}
fn normalize_month(year: i32, month: u32) -> (i32, u32) {
if (1..=12).contains(&month) {
return (year, month);
}
let month_0 = month as i32 - 1;
let year_offset = month_0.div_euclid(12);
let month_norm = month_0.rem_euclid(12);
(year + year_offset, (month_norm + 1) as u32)
}
pub fn julian_day(&self, year: i32, month: u32, day: u32) -> i32 {
let (adjusted_year, adjusted_month) = Self::normalize_month(year, month);
let janfeb = adjusted_month <= 2;
let g = adjusted_year + 4716 - if janfeb { 1 } else { 0 };
let f = (adjusted_month + 9) % 12;
let e = 1461 * g / 4 + day as i32 - 1402;
let mut j = e + (153 * f as i32 + 2) / 5;
let use_gregorian = match self.0.julian_calendar_cutoff {
Some(cutoff) => j >= cutoff,
None => true, };
if use_gregorian {
j += 38 - (g + 184) / 100 * 3 / 4;
}
j
}
pub fn julian_day_to_calendar_date(&self, jd: i32) -> (i32, u32, u32) {
let use_gregorian = match self.0.julian_calendar_cutoff {
Some(cutoff) => jd >= cutoff,
None => true, };
let mut f = jd + 1401;
if use_gregorian {
f += (4 * jd + 274277) / 146097 * 3 / 4 - 38;
}
let e = 4 * f + 3;
let g = (e % 1461) / 4;
let h = 5 * g + 2;
let day = (h % 153) / 5 + 1;
let month = ((h / 153) + 2) % 12 + 1;
let year = e / 1461 - 4716 + (12 + 2 - month) / 12;
(year, month as u32, day as u32)
}
pub fn linspace(&self, t0: &Time, t1: &Time, num: usize) -> Vec<Time> {
if num < 2 {
return vec![t0.clone()];
}
let whole0 = t0.whole;
let frac0 = t0.tt_fraction;
let whole1 = t1.whole;
let frac1 = t1.tt_fraction;
let mut result = Vec::with_capacity(num);
for i in 0..num {
let t = i as f64 / (num - 1) as f64;
let whole = whole0 + t * (whole1 - whole0);
let fraction = frac0 + t * (frac1 - frac0);
result.push(Time {
ts: self.clone(),
whole,
tt_fraction: fraction,
tai_fraction: Some(fraction - TT_MINUS_TAI),
ut1_fraction: OnceLock::new(),
tdb_fraction: OnceLock::new(),
delta_t: OnceLock::new(),
shape: None,
leap_second: false,
});
}
result
}
}
#[derive(Debug, Clone)]
pub enum CalendarInput {
Tuple(i32, u32, u32, u32, u32, f64),
CalendarTuple(CalendarTuple),
}
impl From<(i32, u32, u32, u32, u32, f64)> for CalendarInput {
fn from(tuple: (i32, u32, u32, u32, u32, f64)) -> Self {
CalendarInput::Tuple(tuple.0, tuple.1, tuple.2, tuple.3, tuple.4, tuple.5)
}
}
impl From<CalendarTuple> for CalendarInput {
fn from(cal: CalendarTuple) -> Self {
CalendarInput::CalendarTuple(cal)
}
}
impl From<(i32, u32, u32)> for CalendarInput {
fn from(date: (i32, u32, u32)) -> Self {
CalendarInput::Tuple(date.0, date.1, date.2, 0, 0, 0.0)
}
}
fn once_with(value: f64) -> OnceLock<f64> {
let lock = OnceLock::new();
let _ = lock.set(value);
lock
}
#[derive(Debug, Clone)]
pub struct Time {
ts: Timescale,
whole: f64,
tt_fraction: f64,
tai_fraction: Option<f64>,
ut1_fraction: OnceLock<f64>,
tdb_fraction: OnceLock<f64>,
delta_t: OnceLock<f64>,
shape: Option<Vec<usize>>,
leap_second: bool,
}
impl Time {
pub fn new(utc: DateTime<Utc>) -> Self {
let ts = Timescale::default();
ts.from_datetime(utc)
}
pub fn now() -> Self {
let ts = Timescale::default();
ts.now()
}
pub fn utc_datetime(&self) -> Result<DateTime<Utc>> {
let cal = self.utc_calendar()?;
let naive = chrono::NaiveDate::from_ymd_opt(cal.year, cal.month, cal.day)
.and_then(|date| {
let hour = cal.hour;
let minute = cal.minute;
let second = cal.second as u32;
let nano = ((cal.second - second as f64) * 1_000_000_000.0) as u32;
date.and_hms_nano_opt(hour, minute, second, nano)
})
.ok_or_else(|| TimeError::CalendarError("Invalid calendar date".into()))?;
Ok(DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
}
pub fn utc_datetime_and_leap_second(&self) -> Result<(CalendarTuple, bool)> {
let mut cal = self.utc_calendar()?;
if self.leap_second {
cal.second += 1.0; }
Ok((cal, self.leap_second))
}
pub fn is_leap_second(&self) -> bool {
self.leap_second
}
pub fn utc_calendar(&self) -> Result<CalendarTuple> {
if let Some((year, month, day, hour, minute, second)) = self.get_utc_tuple() {
return Ok(CalendarTuple {
year,
month,
day,
hour,
minute,
second,
});
}
self.tai_to_utc_calendar()
}
fn tai_to_utc_calendar(&self) -> Result<CalendarTuple> {
let tai_jd = self.tai();
if let Some(leap_tai) = &self.ts.0.leap_tai {
if let Some(leap_utc) = &self.ts.0.leap_utc {
let tai_seconds = tai_jd * DAY_S;
let offset = match leap_tai
.binary_search_by(|&time| time.partial_cmp(&tai_seconds).unwrap())
{
Ok(index) => leap_tai[index] - leap_utc[index],
Err(index) => {
if index == 0 {
0.0
} else if index >= leap_tai.len() {
leap_tai[leap_tai.len() - 1] - leap_utc[leap_utc.len() - 1]
} else {
leap_tai[index - 1] - leap_utc[index - 1]
}
}
};
let utc_seconds = tai_seconds - offset;
let utc_jd = utc_seconds / DAY_S;
Ok(self.ts.jd_to_calendar(utc_jd))
} else {
Err(TimeError::LeapSecondDataUnavailable)
}
} else {
Err(TimeError::LeapSecondDataUnavailable)
}
}
fn set_utc_tuple(&mut self, _input: CalendarInput) {
}
fn get_utc_tuple(&self) -> Option<(i32, u32, u32, u32, u32, f64)> {
None
}
pub fn utc_iso(&self, delimiter: char, places: usize) -> Result<String> {
let cal = self.utc_calendar()?;
if places > 0 {
let second_int = cal.second.floor() as u32;
let fraction = cal.second - second_int as f64;
let fraction_str = format!("{:.*}", places, fraction)
.chars()
.skip(2)
.collect::<String>();
Ok(format!(
"{:04}-{:02}-{:02}{}{:02}:{:02}:{:02}.{}Z",
cal.year,
cal.month,
cal.day,
delimiter,
cal.hour,
cal.minute,
second_int,
fraction_str
))
} else {
Ok(format!(
"{:04}-{:02}-{:02}{}{:02}:{:02}:{:02}Z",
cal.year, cal.month, cal.day, delimiter, cal.hour, cal.minute, cal.second as u32
))
}
}
pub fn utc_strftime(&self, format: &str) -> Result<String> {
let cal = self.utc_calendar()?;
Ok(Self::strftime_cal(&cal, format))
}
pub fn tt_strftime(&self, format: &str) -> String {
let cal = self.tt_calendar();
Self::strftime_cal(&cal, format)
}
pub fn tdb_strftime(&self, format: &str) -> String {
let cal = self.tdb_calendar();
Self::strftime_cal(&cal, format)
}
pub fn tai_strftime(&self, format: &str) -> String {
let cal = self.tai_calendar();
Self::strftime_cal(&cal, format)
}
pub fn ut1_strftime(&self, format: &str) -> String {
let cal = self.ut1_calendar();
Self::strftime_cal(&cal, format)
}
fn strftime_cal(cal: &CalendarTuple, format: &str) -> String {
let second_int = cal.second.floor() as u32;
let microseconds = ((cal.second - second_int as f64) * 1_000_000.0).round() as u32;
let day_of_year = Self::day_of_year(cal.year, cal.month, cal.day);
let mut result = String::with_capacity(format.len() * 2);
let mut chars = format.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('Y') => result.push_str(&format!("{:04}", cal.year)),
Some('m') => result.push_str(&format!("{:02}", cal.month)),
Some('d') => result.push_str(&format!("{:02}", cal.day)),
Some('H') => result.push_str(&format!("{:02}", cal.hour)),
Some('M') => result.push_str(&format!("{:02}", cal.minute)),
Some('S') => result.push_str(&format!("{:02}", second_int)),
Some('f') => result.push_str(&format!("{:06}", microseconds)),
Some('j') => result.push_str(&format!("{:03}", day_of_year)),
Some('%') => result.push('%'),
Some(other) => {
result.push('%');
result.push(other);
}
None => result.push('%'),
}
} else {
result.push(c);
}
}
result
}
fn day_of_year(year: i32, month: u32, day: u32) -> u32 {
let is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
let days_in_months = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut doy: u32 = day;
for m in 1..month {
doy += days_in_months[m as usize];
}
if is_leap && month > 2 {
doy += 1;
}
doy
}
pub fn tai(&self) -> f64 {
if let Some(tai_fraction) = self.tai_fraction {
self.whole + tai_fraction
} else {
self.whole + self.tt_fraction - TT_MINUS_TAI
}
}
pub fn tt(&self) -> f64 {
self.whole + self.tt_fraction
}
pub fn j(&self) -> f64 {
(self.whole - 1_721_045.0 + self.tt_fraction) / 365.25
}
pub fn tdb(&self) -> f64 {
let tdb_frac = *self.tdb_fraction.get_or_init(|| {
let tt = self.tt();
let tdb_correction = self.tdb_minus_tt(tt) / DAY_S;
self.tt_fraction + tdb_correction
});
self.whole + tdb_frac
}
fn tdb_minus_tt(&self, jd_tdb: f64) -> f64 {
let t = (jd_tdb - J2000) / 36525.0;
0.001657 * f64::sin(628.3076 * t + 6.2401)
+ 0.000022 * f64::sin(575.3385 * t + 4.2970)
+ 0.000014 * f64::sin(1256.6152 * t + 6.1969)
+ 0.000005 * f64::sin(606.9777 * t + 4.0212)
+ 0.000005 * f64::sin(52.9691 * t + 0.4444)
+ 0.000002 * f64::sin(21.3299 * t + 5.5431)
+ 0.000010 * t * f64::sin(628.3076 * t + 4.2490)
}
pub fn ut1(&self) -> f64 {
let ut1_frac = *self
.ut1_fraction
.get_or_init(|| self.tt_fraction - self.delta_t() / DAY_S);
self.whole + ut1_frac
}
pub fn delta_t(&self) -> f64 {
*self.delta_t.get_or_init(|| self.ts.delta_t(self.tt()))
}
pub fn dut1(&self) -> f64 {
TT_MINUS_TAI_S + self.leap_seconds() - self.delta_t()
}
pub fn leap_seconds(&self) -> f64 {
let tt_jd = self.tt();
let leap_dates = &self.ts.0.leap_dates;
let leap_offsets = &self.ts.0.leap_offsets;
if leap_dates.is_empty() {
return 0.0;
}
match leap_dates.binary_search_by(|d| d.partial_cmp(&tt_jd).unwrap()) {
Ok(i) => leap_offsets[i] as f64,
Err(i) => {
if i == 0 {
0.0
} else {
leap_offsets[i - 1] as f64
}
}
}
}
pub fn gmst(&self) -> f64 {
let ut1_jd = self.ut1();
let ut1_whole = ut1_jd.floor();
let ut1_frac = ut1_jd - ut1_whole;
let tdb_centuries = (self.tdb() - J2000) / 36525.0;
crate::earthlib::sidereal_time(ut1_whole, ut1_frac, tdb_centuries)
}
pub fn gast(&self) -> f64 {
let tt = self.tt();
let (d_psi, _d_eps) = crate::nutationlib::iau2000a_nutation(tt);
let mean_obliq = crate::nutationlib::mean_obliquity(self.tdb());
let c_terms = crate::nutationlib::equation_of_the_equinoxes_complementary_terms(tt);
let eq_eq = d_psi * mean_obliq.cos() + c_terms;
(self.gmst() + eq_eq / TAU * 24.0).rem_euclid(24.0)
}
pub fn m_matrix(&self) -> Matrix3<f64> {
let b = *crate::framelib::ICRS_TO_J2000;
let p = crate::precessionlib::compute_precession(self.tdb());
let mean_obliq = crate::nutationlib::mean_obliquity(self.tdb());
let (d_psi, d_eps) = crate::nutationlib::iau2000a_nutation(self.tt());
let n = crate::nutationlib::build_nutation_matrix(mean_obliq, d_psi, d_eps);
n * p * b
}
pub fn mt_matrix(&self) -> Matrix3<f64> {
self.m_matrix().transpose()
}
pub fn polar_motion_angles(&self) -> (f64, f64, f64) {
if let Some((tt_dates, x_vals, y_vals)) = &self.ts.0.polar_motion_table {
let s_prime = -47.0e-6 * (self.tdb() - J2000) / 36525.0;
let tt = self.tt();
let x = linear_interpolate(tt_dates, x_vals, tt);
let y = linear_interpolate(tt_dates, y_vals, tt);
(s_prime, x, y)
} else {
(0.0, 0.0, 0.0)
}
}
pub fn polar_motion_matrix(&self) -> Matrix3<f64> {
let (s_prime, x_p, y_p) = self.polar_motion_angles();
let sp = -s_prime * ASEC2RAD;
let xp = x_p * ASEC2RAD;
let yp = y_p * ASEC2RAD;
rot_x(yp) * rot_y(xp) * rot_z(sp)
}
pub fn c_matrix(&self) -> Matrix3<f64> {
let angle = -self.gast() * TAU / 24.0;
let (s, c) = angle.sin_cos();
#[rustfmt::skip]
let r = Matrix3::new(
c, -s, 0.0,
s, c, 0.0,
0.0, 0.0, 1.0,
);
let base = r * self.m_matrix();
if self.ts.0.polar_motion_table.is_some() {
self.polar_motion_matrix() * base
} else {
base
}
}
pub fn ct_matrix(&self) -> Matrix3<f64> {
self.c_matrix().transpose()
}
pub fn tt_seconds_from_j2000(&self) -> f64 {
(self.tt() - J2000) * DAY_S
}
pub fn jd(&self) -> f64 {
self.tt()
}
pub fn tai_calendar(&self) -> CalendarTuple {
self.ts.jd_to_calendar(self.tai())
}
pub fn tt_calendar(&self) -> CalendarTuple {
self.ts.jd_to_calendar(self.tt())
}
pub fn tdb_calendar(&self) -> CalendarTuple {
self.ts.jd_to_calendar(self.tdb())
}
pub fn ut1_calendar(&self) -> CalendarTuple {
self.ts.jd_to_calendar(self.ut1())
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.utc_calendar() {
Ok(cal) => write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:06.3} UTC (ΔT: {:.2}s)",
cal.year,
cal.month,
cal.day,
cal.hour,
cal.minute,
cal.second,
self.delta_t()
),
Err(_) => write!(f, "<Time tt={:.6}>", self.tt()),
}
}
}
impl Add<f64> for Time {
type Output = Time;
fn add(self, days: f64) -> Self::Output {
let whole_days = days.floor();
let fraction = days - whole_days;
Time {
ts: self.ts.clone(),
whole: self.whole + whole_days,
tt_fraction: self.tt_fraction + fraction,
tai_fraction: self.tai_fraction.map(|f| f + fraction),
ut1_fraction: self
.ut1_fraction
.get()
.map_or_else(OnceLock::new, |f| once_with(*f + fraction)),
tdb_fraction: self
.tdb_fraction
.get()
.map_or_else(OnceLock::new, |f| once_with(*f + fraction)),
delta_t: OnceLock::new(), shape: self.shape,
leap_second: false,
}
}
}
impl Add<Duration> for Time {
type Output = Time;
fn add(self, duration: Duration) -> Self::Output {
let days = duration.num_days() as f64;
let remaining_nanos = (duration.num_nanoseconds().unwrap_or(0) % 86_400_000_000_000) as f64;
let days_fraction = remaining_nanos / 86_400_000_000_000.0;
self + (days + days_fraction)
}
}
impl Sub<f64> for Time {
type Output = Time;
fn sub(self, days: f64) -> Self::Output {
let whole_days = days.floor();
let fraction = days - whole_days;
Time {
ts: self.ts.clone(),
whole: self.whole - whole_days,
tt_fraction: self.tt_fraction - fraction,
tai_fraction: self.tai_fraction.map(|f| f - fraction),
ut1_fraction: self
.ut1_fraction
.get()
.map_or_else(OnceLock::new, |f| once_with(*f - fraction)),
tdb_fraction: self
.tdb_fraction
.get()
.map_or_else(OnceLock::new, |f| once_with(*f - fraction)),
delta_t: OnceLock::new(), shape: self.shape,
leap_second: false,
}
}
}
impl Sub<Time> for Time {
type Output = f64;
fn sub(self, other: Time) -> Self::Output {
(self.whole - other.whole) + (self.tt_fraction - other.tt_fraction)
}
}
impl Sub<Duration> for Time {
type Output = Time;
fn sub(self, duration: Duration) -> Self::Output {
let days = duration.num_days() as f64;
let remaining_nanos = (duration.num_nanoseconds().unwrap_or(0) % 86_400_000_000_000) as f64;
let days_fraction = remaining_nanos / 86_400_000_000_000.0;
self - (days + days_fraction)
}
}
impl PartialEq for Time {
fn eq(&self, other: &Self) -> bool {
(self.whole == other.whole) && (self.tt_fraction == other.tt_fraction)
}
}
impl PartialOrd for Time {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let self_tt = self.whole + self.tt_fraction;
let other_tt = other.whole + other.tt_fraction;
self_tt.partial_cmp(&other_tt)
}
}
impl From<DateTime<Utc>> for Time {
fn from(dt: DateTime<Utc>) -> Self {
Self::new(dt)
}
}
fn rot_x(angle: f64) -> Matrix3<f64> {
let (s, c) = angle.sin_cos();
Matrix3::new(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c)
}
fn rot_y(angle: f64) -> Matrix3<f64> {
let (s, c) = angle.sin_cos();
Matrix3::new(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c)
}
fn rot_z(angle: f64) -> Matrix3<f64> {
let (s, c) = angle.sin_cos();
Matrix3::new(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0)
}
fn linear_interpolate(xs: &[f64], ys: &[f64], x: f64) -> f64 {
if xs.is_empty() {
return 0.0;
}
if x <= xs[0] {
return ys[0];
}
if x >= xs[xs.len() - 1] {
return ys[ys.len() - 1];
}
let idx = xs.partition_point(|&v| v < x);
if idx == 0 {
return ys[0];
}
let i = idx - 1;
let frac = (x - xs[i]) / (xs[i + 1] - xs[i]);
ys[i] + frac * (ys[i + 1] - ys[i])
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use chrono::TimeZone;
#[test]
#[test]
fn test_time_and_timescale_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Time>();
assert_send_sync::<Timescale>();
}
#[test]
fn test_time_lazy_caches_are_consistent_across_threads() {
let ts = Timescale::default();
let t = std::sync::Arc::new(ts.tt_jd(2_451_545.0, None));
let mut handles = Vec::new();
for _ in 0..8 {
let t = std::sync::Arc::clone(&t);
handles.push(std::thread::spawn(move || (t.tdb(), t.ut1(), t.delta_t())));
}
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let first = results[0];
for r in &results[1..] {
assert_eq!(*r, first);
}
}
fn test_time_shares_timescale_via_arc_after_clone() {
let ts = Timescale::default();
let t1 = ts.tt_jd(2_451_545.0, None);
let t2 = ts.tt_jd(2_451_546.0, None);
let t3 = t1.clone();
let p_ts = Arc::as_ptr(&ts.0);
assert_eq!(p_ts, Arc::as_ptr(&t1.ts.0));
assert_eq!(p_ts, Arc::as_ptr(&t2.ts.0));
assert_eq!(p_ts, Arc::as_ptr(&t3.ts.0));
assert!(
std::mem::size_of::<Time>() <= 200,
"Time grew to {} bytes — review whether the new field belongs inline",
std::mem::size_of::<Time>()
);
}
#[test]
fn test_set_polar_motion_table_does_not_disturb_prior_times() {
let mut ts = Timescale::default();
let before = ts.tt_jd(2_451_545.0, None);
assert!(!ts.has_polar_motion());
assert!(before.ts.0.polar_motion_table.is_none());
ts.set_polar_motion_table(vec![2_451_545.0], vec![0.05], vec![0.30]);
assert!(ts.has_polar_motion());
assert!(before.ts.0.polar_motion_table.is_none());
}
#[test]
fn test_time_creation() {
let ts = Timescale::default();
let j2020 = J2000 + 20.0 * 365.25; let time = ts.tt_jd(j2020, None);
let delta_t = time.delta_t();
assert!(
delta_t > 60.0 && delta_t < 80.0,
"delta_t(2020) = {delta_t}"
);
}
#[test]
fn test_tt_seconds_from_j2000() {
let ts = Timescale::default();
let j2000_time = ts.tt_jd(J2000, None);
assert_relative_eq!(j2000_time.tt_seconds_from_j2000(), 0.0, epsilon = 1e-10);
let one_day_later = ts.tt_jd(J2000 + 1.0, None);
assert_relative_eq!(
one_day_later.tt_seconds_from_j2000(),
DAY_S,
epsilon = 1e-10
);
}
#[test]
fn test_julian_date() {
let date = Utc.with_ymd_and_hms(2000, 1, 1, 12, 0, 0).unwrap();
let time = Time::new(date);
let expected = J2000;
let result = time.jd();
assert_relative_eq!(result, expected, max_relative = 1e-5); }
#[test]
fn test_time_scales() {
let ts = Timescale::default();
let t_j2000 = ts.tt_jd(J2000, None);
assert_relative_eq!(t_j2000.tt(), J2000, epsilon = 1e-10);
let tai_j2000 = t_j2000.tai();
assert_relative_eq!(
J2000 - tai_j2000,
TT_MINUS_TAI,
epsilon = 1e-8 );
let delta_t = t_j2000.delta_t();
let ut1_j2000 = t_j2000.ut1();
assert_relative_eq!(J2000 - ut1_j2000, delta_t / DAY_S, epsilon = 1e-8);
}
#[test]
fn test_calendar_conversions() {
let ts = Timescale::default();
assert_relative_eq!(J2000, 2451545.0, epsilon = 1e-10);
let test_jd = 2455000.5;
let time = ts.tt_jd(test_jd, None);
assert_relative_eq!(time.tt(), test_jd, epsilon = 1e-10);
let jd_test = 2452345.5; let jd_result = ts.tt_jd(jd_test, None).tt();
assert_relative_eq!(jd_result, jd_test, epsilon = 1e-10);
}
#[test]
fn test_time_math() {
let ts = Timescale::default();
let t1 = ts.tt_jd(J2000, None);
let t2 = t1.clone() + 1.0;
assert_relative_eq!(t2.tt(), J2000 + 1.0, epsilon = 1e-10);
let days_diff = t2 - t1;
assert_relative_eq!(days_diff, 1.0, epsilon = 1e-10);
}
#[test]
fn test_delta_t_spline() {
let ts = Timescale::default();
let tt_2000 = 2_451_545.0; let delta_t_2000 = ts.delta_t(tt_2000);
assert_relative_eq!(delta_t_2000, 63.83, epsilon = 1.0);
let tt_1970 = 2_451_545.0 - 30.0 * 365.25;
let delta_t_1970 = ts.delta_t(tt_1970);
assert!(
delta_t_1970 > 30.0 && delta_t_1970 < 50.0,
"delta_t(1970) = {delta_t_1970}"
);
let tt_1800 = 2_451_545.0 - 200.0 * 365.25;
let delta_t_1800 = ts.delta_t(tt_1800);
assert!(
delta_t_1800 > 10.0 && delta_t_1800 < 20.0,
"delta_t(1800) = {delta_t_1800}"
);
}
#[test]
fn test_from_datetime() {
let dt = Utc.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap();
let time: Time = dt.into();
let delta_t = time.delta_t();
assert!(
delta_t > 60.0 && delta_t < 80.0,
"delta_t(2020) = {delta_t}"
);
}
#[test]
fn test_leap_second_detection() {
let ts = Timescale::default();
let t_normal = ts.utc((2016, 12, 31, 23, 59, 59.0));
assert!(!t_normal.is_leap_second());
let t_leap = ts.utc((2016, 12, 31, 23, 59, 60.0));
assert!(t_leap.is_leap_second());
let diff_days = t_leap.tt() - t_normal.tt();
let diff_seconds = diff_days * DAY_S;
assert_relative_eq!(diff_seconds, 1.0, epsilon = 0.01);
}
#[test]
fn test_leap_seconds_from_table() {
let ts = Timescale::default();
let t_early = ts.tt_jd(2440000.0, None);
assert_eq!(t_early.leap_seconds(), 0.0);
let t_1972 = ts.tt_jd(2441400.0, None);
assert_eq!(t_1972.leap_seconds(), 10.0);
let t_j2000 = ts.tt_jd(J2000, None);
assert_eq!(t_j2000.leap_seconds(), 32.0);
let t_2024 = ts.tt_jd(2460310.5, None);
assert_eq!(t_2024.leap_seconds(), 37.0);
}
#[test]
fn test_tdb_calendar_constructor() {
let ts = Timescale::default();
let t_tdb = ts.tdb((2020, 6, 21, 12, 0, 0.0));
let t_tt = ts.tt((2020, 6, 21, 12, 0, 0.0));
assert!(t_tdb.tdb_fraction.get().is_some());
let tt_diff = (t_tdb.tt() - t_tt.tt()).abs() * DAY_S;
assert!(
tt_diff < 0.002,
"TDB and TT constructors' TT values should differ by < 2ms, got {tt_diff}s"
);
}
#[test]
fn test_utc_datetime_and_leap_second() {
let ts = Timescale::default();
let t = ts.utc((2020, 6, 15, 12, 30, 45.0));
let (_cal, is_leap) = t.utc_datetime_and_leap_second().unwrap();
assert!(!is_leap);
let t_leap = ts.utc((2016, 12, 31, 23, 59, 60.0));
let (_cal, is_leap) = t_leap.utc_datetime_and_leap_second().unwrap();
assert!(is_leap);
}
#[test]
fn test_tdb_constructor_basic() {
let ts = Timescale::default();
let t = ts.tdb((2024, 1, 1, 0, 0, 0.0));
assert!(t.tdb_fraction.get().is_some());
assert!(t.tdb() > 2460000.0);
}
#[test]
fn test_tdb_constructor_matches_tdb_jd() {
let ts = Timescale::default();
let t = ts.tdb_jd(J2000);
assert_relative_eq!(t.tdb(), J2000, epsilon = 1e-10);
let tt_diff = (t.tdb() - t.tt()).abs() * DAY_S;
assert!(tt_diff < 0.002, "TDB-TT should be < 2ms, got {tt_diff}s");
}
#[test]
fn test_tt_strftime_basic() {
let ts = Timescale::default();
let t = ts.tt_jd(J2000, None);
let cal = t.tt_calendar();
assert_eq!(cal.year, 2000);
let formatted = t.tt_strftime("%Y-%m-%d %H:%M:%S");
assert!(formatted.starts_with("2000-"));
}
#[test]
fn test_strftime_specifiers() {
let cal = CalendarTuple {
year: 2024,
month: 3,
day: 15,
hour: 14,
minute: 30,
second: 45.123456,
};
let result = Time::strftime_cal(&cal, "%Y-%m-%d %H:%M:%S.%f");
assert_eq!(result, "2024-03-15 14:30:45.123456");
let result2 = Time::strftime_cal(&cal, "%j");
assert_eq!(result2, "075");
let result3 = Time::strftime_cal(&cal, "%%");
assert_eq!(result3, "%");
}
#[test]
fn test_day_of_year() {
assert_eq!(Time::day_of_year(2024, 1, 1), 1);
assert_eq!(Time::day_of_year(2024, 2, 29), 60);
assert_eq!(Time::day_of_year(2024, 12, 31), 366);
assert_eq!(Time::day_of_year(2023, 12, 31), 365);
}
#[test]
fn test_per_scale_strftime() {
let ts = Timescale::default();
let t = ts.tt_jd(J2000, None);
let _tt = t.tt_strftime("%Y-%m-%d");
let _tai = t.tai_strftime("%Y-%m-%d");
let _tdb = t.tdb_strftime("%Y-%m-%d");
let _ut1 = t.ut1_strftime("%Y-%m-%d");
}
}