tempoch_core/scales.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Time-scale marker types.
5//!
6//! Each zero-sized type identifies a specific time scale and encodes how
7//! values in that scale relate to the canonical **Julian Date in TT**
8//! (Terrestrial Time).
9//!
10//! # Epoch counters
11//!
12//! | Marker | Description | Epoch (JD) |
13//! |--------|-------------|------------|
14//! | [`JD`] | Julian Date | 0.0 |
15//! | [`JDE`] | Julian Ephemeris Day | 0.0 |
16//! | [`MJD`] | Modified Julian Date | 2 400 000.5 |
17//! | [`UnixTime`] | Seconds since 1970-01-01 | 2 440 587.5 |
18//! | [`GPS`] | GPS Time (days) | 2 444 244.5 |
19//!
20//! # Physical / relativistic scales
21//!
22//! | Marker | Description |
23//! |--------|-------------|
24//! | [`TDB`] | Barycentric Dynamical Time (canonical) |
25//! | [`TT`] | Terrestrial Time |
26//! | [`TAI`] | International Atomic Time |
27//! | [`TCG`] | Geocentric Coordinate Time (IAU 2000) |
28//! | [`TCB`] | Barycentric Coordinate Time (IAU 2006) |
29
30use super::instant::TimeScale;
31use qtty::Days;
32
33// ---------------------------------------------------------------------------
34// Epoch counters
35// ---------------------------------------------------------------------------
36
37/// Julian Date — the identity scale.
38///
39/// `to_jd_tt(v) = v`, i.e. the quantity *is* a Julian Day number.
40#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
41pub struct JD;
42
43impl TimeScale for JD {
44 const LABEL: &'static str = "Julian Day:";
45
46 #[inline(always)]
47 fn to_jd_tt(value: Days) -> Days {
48 value
49 }
50
51 #[inline(always)]
52 fn from_jd_tt(jd_tt: Days) -> Days {
53 jd_tt
54 }
55}
56
57/// Julian Ephemeris Day — dynamic Julian day used by ephemerides.
58///
59/// Numerically this is an absolute Julian day on the TT axis in this crate.
60/// It is a semantic label for ephemeris formulas, not a UT→TT conversion.
61#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
62pub struct JDE;
63
64impl TimeScale for JDE {
65 const LABEL: &'static str = "JDE";
66
67 #[inline(always)]
68 fn to_jd_tt(value: Days) -> Days {
69 value
70 }
71
72 #[inline(always)]
73 fn from_jd_tt(jd_tt: Days) -> Days {
74 jd_tt
75 }
76}
77
78/// Modified Julian Date — JD minus 2 400 000.5.
79#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
80pub struct MJD;
81
82/// The constant offset between JD and MJD: `JD = MJD + MJD_EPOCH`.
83const MJD_EPOCH: Days = Days::new(2_400_000.5);
84
85impl TimeScale for MJD {
86 const LABEL: &'static str = "MJD";
87
88 #[inline(always)]
89 fn to_jd_tt(value: Days) -> Days {
90 value + MJD_EPOCH
91 }
92
93 #[inline(always)]
94 fn from_jd_tt(jd_tt: Days) -> Days {
95 jd_tt - MJD_EPOCH
96 }
97}
98
99// ---------------------------------------------------------------------------
100// Physical / relativistic scales
101// ---------------------------------------------------------------------------
102
103/// Barycentric Dynamical Time.
104///
105/// TDB differs from TT by a periodic correction of ≈1.7 ms amplitude
106/// (Fairhead & Bretagnon 1990). This implementation applies the four
107/// largest periodic terms automatically in `to_jd_tt` / `from_jd_tt`,
108/// achieving accuracy better than 30 μs for dates within ±10 000 years
109/// of J2000.
110///
111/// ## References
112/// * Fairhead & Bretagnon (1990), A&A 229, 240
113/// * USNO Circular 179, eq. 2.6
114/// * IAU 2006 Resolution B3
115#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
116pub struct TDB;
117
118/// Compute TDB − TT in days using the Fairhead & Bretagnon (1990) 4-term
119/// expression. Accuracy: better than 30 μs for |t| < 100 centuries.
120#[inline]
121pub(crate) fn tdb_minus_tt_days(jd_tt: Days) -> Days {
122 // Julian centuries from J2000.0 on the TT axis
123 let t = (jd_tt.value() - 2_451_545.0) / 36_525.0;
124
125 // Earth's mean anomaly (radians)
126 let m_e = (357.5291092 + 35999.0502909 * t).to_radians();
127 // Mean anomaly of Jupiter (radians)
128 let m_j = (246.4512 + 3035.2335 * t).to_radians();
129 // Mean elongation of the Moon from the Sun (radians)
130 let d = (297.8502042 + 445267.1115168 * t).to_radians();
131 // Mean longitude of lunar ascending node (radians)
132 let om = (125.0445550 - 1934.1362091 * t).to_radians();
133
134 // TDB − TT in seconds (Fairhead & Bretagnon largest terms):
135 let dt_sec = 0.001_657 * (m_e + 0.01671 * m_e.sin()).sin()
136 + 0.000_022 * (d - m_e).sin()
137 + 0.000_014 * (2.0 * d).sin()
138 + 0.000_005 * m_j.sin()
139 + 0.000_005 * om.sin();
140
141 Days::new(dt_sec / 86_400.0)
142}
143
144impl TimeScale for TDB {
145 const LABEL: &'static str = "TDB";
146
147 #[inline]
148 fn to_jd_tt(tdb_value: Days) -> Days {
149 // JD(TT) = JD(TDB) - (TDB - TT)
150 // First approximation: use tdb_value as TT to compute the correction.
151 // The correction is < 2 ms so one iteration is sufficient for f64.
152 tdb_value - tdb_minus_tt_days(tdb_value)
153 }
154
155 #[inline]
156 fn from_jd_tt(jd_tt: Days) -> Days {
157 // JD(TDB) = JD(TT) + (TDB - TT)
158 jd_tt + tdb_minus_tt_days(jd_tt)
159 }
160}
161
162/// Terrestrial Time — the basis for astronomical ephemerides.
163///
164/// Numerically identical to JD when the Julian Day number already represents
165/// TT (which is the convention used throughout this crate).
166#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
167pub struct TT;
168
169impl TimeScale for TT {
170 const LABEL: &'static str = "TT";
171
172 #[inline(always)]
173 fn to_jd_tt(value: Days) -> Days {
174 value // value is already JD(TT)
175 }
176
177 #[inline(always)]
178 fn from_jd_tt(jd_tt: Days) -> Days {
179 jd_tt
180 }
181}
182
183/// International Atomic Time.
184///
185/// `TT = TAI + 32.184 s`.
186#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
187pub struct TAI;
188
189/// `TT = TAI + 32.184 s` expressed in days.
190const TT_MINUS_TAI: Days = Days::new(32.184 / 86_400.0);
191
192impl TimeScale for TAI {
193 const LABEL: &'static str = "TAI";
194
195 #[inline(always)]
196 fn to_jd_tt(value: Days) -> Days {
197 // TAI → TT: add 32.184 s
198 value + TT_MINUS_TAI
199 }
200
201 #[inline(always)]
202 fn from_jd_tt(jd_tt: Days) -> Days {
203 // TT → TAI: subtract 32.184 s
204 jd_tt - TT_MINUS_TAI
205 }
206}
207
208// ---------------------------------------------------------------------------
209// Coordinate time scales (IAU 2000 / 2006)
210// ---------------------------------------------------------------------------
211
212/// Geocentric Coordinate Time — the coordinate time for the GCRS.
213///
214/// TCG is the proper time experienced by a clock at the geocenter, free from
215/// the gravitational time dilation of the Earth's potential.
216///
217/// The defining relation (IAU 2000 Resolution B1.9) is:
218///
219/// ```text
220/// dTT / dTCG = 1 − L_G
221/// ```
222///
223/// where **L_G = 6.969 290 134 × 10⁻¹⁰** is an IAU defining constant.
224/// Integrating:
225///
226/// ```text
227/// TT = TCG − L_G × (JD_TCG − T₀)
228/// ```
229///
230/// with **T₀ = 2 443 144.500 372 5** (JD of 1977 January 1.0 TAI in the TCG scale).
231///
232/// ## References
233/// * IAU 2000 Resolution B1.9
234/// * IERS Conventions (2010), §1.2
235#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
236pub struct TCG;
237
238/// IAU defining constant L_G (IAU 2000 Resolution B1.9).
239///
240/// Defines the rate difference between TT and TCG:
241/// `dTT/dTCG = 1 − L_G`.
242const L_G: f64 = 6.969_290_134e-10;
243
244/// TCG epoch T₀: JD(TCG) of 1977 January 1.0 TAI.
245const TCG_EPOCH_T0: f64 = 2_443_144.500_372_5;
246
247impl TimeScale for TCG {
248 const LABEL: &'static str = "TCG";
249
250 #[inline]
251 fn to_jd_tt(tcg_value: Days) -> Days {
252 // TT = TCG − L_G × (JD_TCG − T₀)
253 let jd_tcg = tcg_value.value();
254 Days::new(jd_tcg - L_G * (jd_tcg - TCG_EPOCH_T0))
255 }
256
257 #[inline]
258 fn from_jd_tt(jd_tt: Days) -> Days {
259 // JD_TCG = (JD_TT + L_G × T₀) / (1 − L_G)
260 // ≈ JD_TT + L_G × (JD_TT − T₀) (first-order, adequate for f64)
261 let tt = jd_tt.value();
262 Days::new(tt + L_G * (tt - TCG_EPOCH_T0) / (1.0 - L_G))
263 }
264}
265
266/// Barycentric Coordinate Time — the coordinate time for the BCRS.
267///
268/// TCB is the time coordinate complementary to barycentric spatial coordinates.
269/// It relates to TDB via a linear drift:
270///
271/// ```text
272/// TDB = TCB − L_B × (JD_TCB − T₀) + TDB₀
273/// ```
274///
275/// where:
276/// * **L_B = 1.550 519 768 × 10⁻⁸** (IAU 2006 Resolution B3, defining constant)
277/// * **TDB₀ = −6.55 × 10⁻⁵ s** (IAU 2006 Resolution B3)
278/// * **T₀ = 2 443 144.500 372 5** (JD of 1977 January 1.0 TAI)
279///
280/// Since TDB ≈ TT for route-through-JD(TT) purposes (the ≈1.7 ms periodic
281/// difference is handled separately), we use the TDB relation directly.
282///
283/// ## References
284/// * IAU 2006 Resolution B3
285/// * IERS Conventions (2010), §1.2
286#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
287pub struct TCB;
288
289/// IAU defining constant L_B (IAU 2006 Resolution B3).
290///
291/// Defines the rate difference between TDB and TCB:
292/// `TDB = TCB − L_B × (JD_TCB − T₀) + TDB₀`.
293const L_B: f64 = 1.550_519_768e-8;
294
295impl TimeScale for TCB {
296 const LABEL: &'static str = "TCB";
297
298 #[inline]
299 fn to_jd_tt(tcb_value: Days) -> Days {
300 // TDB = TCB − L_B × (JD_TCB − T₀)
301 // Treating TDB ≈ TT (periodic ≈1.7 ms difference handled separately).
302 // Matches SOFA iauTcbtdb.
303 let jd_tcb = tcb_value.value();
304 Days::new(jd_tcb - L_B * (jd_tcb - TCG_EPOCH_T0))
305 }
306
307 #[inline]
308 fn from_jd_tt(jd_tt: Days) -> Days {
309 // JD_TCB = JD_TDB + L_B × (JD_TDB − T₀)
310 // Matches SOFA iauTdbtcb.
311 let tt = jd_tt.value();
312 Days::new(tt + L_B * (tt - TCG_EPOCH_T0))
313 }
314}
315
316// ---------------------------------------------------------------------------
317// Navigation counters
318// ---------------------------------------------------------------------------
319
320/// GPS Time — continuous day count since 1980-01-06T00:00:00 UTC.
321///
322/// GPS time has a fixed offset from TAI: `GPS = TAI − 19 s`.
323#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
324pub struct GPS;
325
326/// JD(TT) of the GPS epoch (1980-01-06T00:00:00 UTC).
327/// GPS = TAI − 19s, and TT = TAI + 32.184s, so TT = GPS + 51.184s.
328/// GPS epoch in JD(TT): JD 2444244.5 + 51.184/86400.
329const GPS_EPOCH_JD_TT: Days = Days::new(2_444_244.5 + 51.184 / 86_400.0);
330
331impl TimeScale for GPS {
332 const LABEL: &'static str = "GPS";
333
334 #[inline(always)]
335 fn to_jd_tt(value: Days) -> Days {
336 value + GPS_EPOCH_JD_TT
337 }
338
339 #[inline(always)]
340 fn from_jd_tt(jd_tt: Days) -> Days {
341 jd_tt - GPS_EPOCH_JD_TT
342 }
343}
344
345/// Unix Time — seconds since 1970-01-01T00:00:00 UTC, stored as **days**.
346///
347/// This scale applies the cumulative leap-second offset from IERS Bulletin C
348/// to convert between UTC-epoch Unix timestamps and the uniform TT axis.
349/// The leap-second table covers 1972–2017 (28 insertions). Prior to 1972
350/// (before the leap-second system) the offset is fixed at 10 s (the initial
351/// TAI − UTC value adopted on 1972-01-01).
352///
353/// ## References
354/// * IERS Bulletin C (leap second announcements)
355/// * POSIX.1-2017 §4.16 (definition of Unix time)
356#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
357pub struct UnixTime;
358
359/// JD of the Unix epoch (1970-01-01T00:00:00Z).
360const UNIX_EPOCH_JD: Days = Days::new(2_440_587.5);
361
362/// Leap-second table: (JD of leap-second insertion, cumulative TAI−UTC after).
363/// Source: IERS Bulletin C. Entries are the JD of 00:00:00 UTC on the day
364/// the leap second takes effect (i.e. the second is inserted at the end
365/// of the previous day).
366///
367/// TAI = UTC + leap_seconds (for times at or after each entry).
368/// TT = TAI + 32.184 s.
369const LEAP_SECONDS: [(f64, f64); 28] = [
370 (2_441_317.5, 10.0), // 1972-01-01
371 (2_441_499.5, 11.0), // 1972-07-01
372 (2_441_683.5, 12.0), // 1973-01-01
373 (2_442_048.5, 13.0), // 1974-01-01
374 (2_442_413.5, 14.0), // 1975-01-01
375 (2_442_778.5, 15.0), // 1976-01-01
376 (2_443_144.5, 16.0), // 1977-01-01
377 (2_443_509.5, 17.0), // 1978-01-01
378 (2_443_874.5, 18.0), // 1979-01-01
379 (2_444_239.5, 19.0), // 1980-01-01
380 (2_444_786.5, 20.0), // 1981-07-01
381 (2_445_151.5, 21.0), // 1982-07-01
382 (2_445_516.5, 22.0), // 1983-07-01
383 (2_446_247.5, 23.0), // 1985-07-01
384 (2_447_161.5, 24.0), // 1988-01-01
385 (2_447_892.5, 25.0), // 1990-01-01
386 (2_448_257.5, 26.0), // 1991-01-01
387 (2_448_804.5, 27.0), // 1992-07-01
388 (2_449_169.5, 28.0), // 1993-07-01
389 (2_449_534.5, 29.0), // 1994-07-01
390 (2_450_083.5, 30.0), // 1996-01-01
391 (2_450_630.5, 31.0), // 1997-07-01
392 (2_451_179.5, 32.0), // 1999-01-01
393 (2_453_736.5, 33.0), // 2006-01-01
394 (2_454_832.5, 34.0), // 2009-01-01
395 (2_456_109.5, 35.0), // 2012-07-01
396 (2_457_204.5, 36.0), // 2015-07-01
397 (2_457_754.5, 37.0), // 2017-01-01
398];
399
400/// Look up cumulative TAI−UTC (leap seconds) for a JD on the UTC axis.
401///
402/// Returns the number of leap seconds (TAI − UTC) in effect at the given
403/// Julian Date on the UTC time axis. The table covers 1972–2017
404/// (28 insertions). Before 1972 the conventional initial offset of 10 s
405/// is returned.
406///
407/// # Arguments
408///
409/// * `jd_utc` - Julian Date number on the UTC axis.
410#[inline]
411pub fn tai_minus_utc(jd_utc: f64) -> f64 {
412 // Binary search for the last entry <= jd_utc
413 let mut lo = 0usize;
414 let mut hi = LEAP_SECONDS.len();
415 while lo < hi {
416 let mid = lo + (hi - lo) / 2;
417 if LEAP_SECONDS[mid].0 <= jd_utc {
418 lo = mid + 1;
419 } else {
420 hi = mid;
421 }
422 }
423 if lo == 0 {
424 // Before 1972-01-01: use the initial offset of 10 s
425 // (this is an approximation; pre-1972 UTC had fractional offsets)
426 10.0
427 } else {
428 LEAP_SECONDS[lo - 1].1
429 }
430}
431
432/// TT = TAI + 32.184 s, and TAI = UTC + leap_seconds.
433/// So TT = UTC + leap_seconds + 32.184 s.
434const TT_MINUS_TAI_SECS: f64 = 32.184;
435
436impl TimeScale for UnixTime {
437 const LABEL: &'static str = "Unix";
438
439 #[inline]
440 fn to_jd_tt(value: Days) -> Days {
441 // value is Unix days (days since 1970-01-01 on the UTC axis)
442 let jd_utc = value.value() + UNIX_EPOCH_JD.value();
443 let ls = tai_minus_utc(jd_utc);
444 // JD(TT) = JD(UTC) + (TAI−UTC + 32.184) / 86400
445 Days::new(jd_utc + (ls + TT_MINUS_TAI_SECS) / 86_400.0)
446 }
447
448 #[inline]
449 fn from_jd_tt(jd_tt: Days) -> Days {
450 // Approximate JD(UTC) by subtracting the largest plausible offset,
451 // then refine with the correct leap-second count.
452 let approx_utc = jd_tt.value() - (37.0 + TT_MINUS_TAI_SECS) / 86_400.0;
453 let ls = tai_minus_utc(approx_utc);
454 let jd_utc = jd_tt.value() - (ls + TT_MINUS_TAI_SECS) / 86_400.0;
455 Days::new(jd_utc - UNIX_EPOCH_JD.value())
456 }
457}
458
459// ---------------------------------------------------------------------------
460// Universal Time (Earth-rotation based)
461// ---------------------------------------------------------------------------
462
463/// Universal Time — the civil time scale tied to Earth's rotation.
464///
465/// Unlike [`JD`], [`JDE`], and [`TT`] (which all live on the uniform TT
466/// axis), `UT` encodes a Julian Day on the **UT** axis. The conversion
467/// to JD(TT) adds the epoch-dependent **ΔT** correction from Meeus (1998)
468/// ch. 9, and the inverse uses a three-iteration fixed-point solver
469/// with sub-microsecond accuracy.
470///
471/// [`Time::from_utc`](super::instant::Time::from_utc) routes through this
472/// scale automatically, so callers rarely need to construct `Time<UT>` directly.
473#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
474pub struct UT;
475
476impl TimeScale for UT {
477 const LABEL: &'static str = "UT";
478
479 #[inline]
480 fn to_jd_tt(ut_value: Days) -> Days {
481 let jd_ut = super::instant::Time::<JD>::from_days(ut_value);
482 let dt_secs = super::delta_t::delta_t_seconds_from_ut(jd_ut);
483 ut_value + dt_secs.to::<qtty::Day>()
484 }
485
486 #[inline]
487 fn from_jd_tt(jd_tt: Days) -> Days {
488 // Solve ut + ΔT(ut)/86400 = tt via fixed-point iteration.
489 // dΔT/dJD ≈ 3×10⁻⁸, so convergence is immediate.
490 let mut ut = jd_tt;
491 for _ in 0..3 {
492 let jd_ut = super::instant::Time::<JD>::from_days(ut);
493 let dt_days = super::delta_t::delta_t_seconds_from_ut(jd_ut).to::<qtty::Day>();
494 ut = jd_tt - dt_days;
495 }
496 ut
497 }
498}
499
500// ---------------------------------------------------------------------------
501// Cross-scale From/Into and .to::<T>() (generated by macro)
502// ---------------------------------------------------------------------------
503
504/// Generate pairwise `From<Time<A>> for Time<B>` implementations.
505macro_rules! impl_time_conversions {
506 // Base case: single scale, nothing left.
507 ($single:ty) => {};
508
509 // Recursive: generate pairs between $first and every $rest, then recurse.
510 ($first:ty, $($rest:ty),+ $(,)?) => {
511 $(
512 impl From<super::instant::Time<$first>> for super::instant::Time<$rest> {
513 #[inline]
514 fn from(t: super::instant::Time<$first>) -> Self {
515 t.to::<$rest>()
516 }
517 }
518
519 impl From<super::instant::Time<$rest>> for super::instant::Time<$first> {
520 #[inline]
521 fn from(t: super::instant::Time<$rest>) -> Self {
522 t.to::<$first>()
523 }
524 }
525 )+
526
527 impl_time_conversions!($($rest),+);
528 };
529}
530
531impl_time_conversions!(JD, JDE, MJD, TDB, TT, TAI, TCG, TCB, GPS, UnixTime, UT);
532
533#[cfg(test)]
534mod tests {
535 use super::super::instant::Time;
536 use super::*;
537 use qtty::{Day, Second, Seconds};
538
539 #[test]
540 fn jd_mjd_roundtrip() {
541 let jd = Time::<JD>::new(2_451_545.0);
542 let mjd: Time<MJD> = jd.to::<MJD>();
543 assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
544 let back: Time<JD> = mjd.to::<JD>();
545 assert!((back.quantity() - Days::new(2_451_545.0)).abs() < Days::new(1e-10));
546 }
547
548 #[test]
549 fn jd_mjd_from_into() {
550 let jd = Time::<JD>::new(2_451_545.0);
551 let mjd: Time<MJD> = jd.into();
552 assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
553 let back: Time<JD> = Time::from(mjd);
554 assert!((back.quantity() - Days::new(2_451_545.0)).abs() < Days::new(1e-10));
555 }
556
557 #[test]
558 fn tai_tt_offset() {
559 // TT = TAI + 32.184s
560 let tai = Time::<TAI>::new(2_451_545.0);
561 let tt: Time<TT> = tai.to::<TT>();
562 let expected_offset = Seconds::new(32.184).to::<Day>();
563 assert!((tt.quantity() - (tai.quantity() + expected_offset)).abs() < Days::new(1e-15));
564 }
565
566 #[test]
567 fn gps_epoch_roundtrip() {
568 // GPS epoch is JD 2444244.5 (in UTC); in TT it is shifted by 51.184s
569 let gps_zero = Time::<GPS>::new(0.0);
570 let jd: Time<JD> = gps_zero.to::<JD>();
571 let expected = Days::new(2_444_244.5) + Seconds::new(51.184).to::<Day>();
572 assert!((jd.quantity() - expected).abs() < Days::new(1e-12));
573 }
574
575 #[test]
576 fn unix_epoch_roundtrip() {
577 // Unix epoch (1970-01-01) has 10 s TAI−UTC and 32.184 s TT−TAI = 42.184 s TT−UTC
578 let unix_zero = Time::<UnixTime>::new(0.0);
579 let jd: Time<JD> = unix_zero.to::<JD>();
580 let expected = Days::new(2_440_587.5) + Seconds::new(42.184).to::<Day>();
581 assert!(
582 (jd.quantity() - expected).abs() < Days::new(1e-10),
583 "Unix epoch JD(TT) = {}, expected {}",
584 jd.quantity(),
585 expected
586 );
587 }
588
589 #[test]
590 fn unix_2020_leap_seconds() {
591 // 2020-01-01 00:00:00 UTC: TAI−UTC = 37 s, TT−UTC = 69.184 s
592 // JD(UTC) of 2020-01-01 = 2458849.5
593 // Unix days = 2458849.5 - 2440587.5 = 18262.0
594 let unix_2020 = Time::<UnixTime>::new(18262.0);
595 let jd: Time<JD> = unix_2020.to::<JD>();
596 let expected = Days::new(2_458_849.5) + Seconds::new(69.184).to::<Day>();
597 assert!(
598 (jd.quantity() - expected).abs() < Days::new(1e-10),
599 "2020 Unix JD(TT) = {}, expected {}",
600 jd.quantity(),
601 expected
602 );
603 }
604
605 #[test]
606 fn tdb_tt_offset() {
607 // TDB − TT periodic correction is ~1.7 ms at maximum.
608 // At J2000.0 t=0, the correction should be small but non-zero.
609 let jd = Time::<JD>::new(2_451_545.0);
610 let tdb: Time<TDB> = jd.to::<TDB>();
611 let offset_secs = (tdb.quantity() - jd.quantity()).to::<Second>();
612 // Should be within ±2 ms
613 assert!(
614 offset_secs.abs() < Seconds::new(0.002),
615 "TDB−TT offset at J2000 = {} s, expected < 2 ms",
616 offset_secs
617 );
618 }
619
620 #[test]
621 fn tdb_tt_roundtrip() {
622 let jd = Time::<JD>::new(2_451_545.0);
623 let tdb: Time<TDB> = jd.to::<TDB>();
624 let back: Time<JD> = tdb.to::<JD>();
625 assert!(
626 (back.quantity() - jd.quantity()).abs() < Days::new(1e-14),
627 "TDB→TT roundtrip error: {} days",
628 (back.quantity() - jd.quantity()).abs()
629 );
630 }
631
632 #[test]
633 fn tcg_tt_offset_at_j2000() {
634 // At J2000.0 (JD 2451545.0), TCG runs ahead of TT.
635 // TT = TCG − L_G × (JD_TCG − T₀)
636 // The offset at J2000 should be ~L_G × (2451545 − 2443144.5) × 86400 s
637 // ≈ 6.97e-10 × 8400.5 × 86400 ≈ 0.506 s
638 let tt = Time::<TT>::new(2_451_545.0);
639 let tcg: Time<TCG> = tt.to::<TCG>();
640 let offset_days = tcg.quantity() - tt.quantity();
641 let offset_secs = offset_days.to::<Second>();
642 // TCG should be ahead of TT by ~0.506 s at J2000
643 assert!(
644 (offset_secs - Seconds::new(0.506)).abs() < Seconds::new(0.01),
645 "TCG−TT offset at J2000 = {} s, expected ~0.506 s",
646 offset_secs
647 );
648 }
649
650 #[test]
651 fn tcg_tt_roundtrip() {
652 let tt = Time::<TT>::new(2_451_545.0);
653 let tcg: Time<TCG> = tt.to::<TCG>();
654 let back: Time<TT> = tcg.to::<TT>();
655 assert!(
656 (back.quantity() - tt.quantity()).abs() < Days::new(1e-12),
657 "TCG→TT roundtrip error: {} days",
658 (back.quantity() - tt.quantity()).abs()
659 );
660 }
661
662 #[test]
663 fn tcb_tdb_offset_at_j2000() {
664 // TCB runs significantly ahead of TDB.
665 // Offset ≈ L_B × (2451545 − 2443144.5) × 86400 s
666 // ≈ 1.55e-8 × 8400.5 × 86400 ≈ 11.25 s
667 let tt = Time::<TT>::new(2_451_545.0);
668 let tcb: Time<TCB> = tt.to::<TCB>();
669 let offset_days = tcb.quantity() - tt.quantity();
670 let offset_secs = offset_days.to::<Second>();
671 // TCB should be ahead of TT/TDB by ~11.25 s at J2000
672 assert!(
673 (offset_secs - Seconds::new(11.25)).abs() < Seconds::new(0.5),
674 "TCB−TT offset at J2000 = {} s, expected ~11.25 s",
675 offset_secs
676 );
677 }
678
679 #[test]
680 fn tcb_tt_roundtrip() {
681 let tt = Time::<TT>::new(2_458_000.0);
682 let tcb: Time<TCB> = tt.to::<TCB>();
683 let back: Time<TT> = tcb.to::<TT>();
684 assert!(
685 (back.quantity() - tt.quantity()).abs() < Days::new(1e-10),
686 "TCB→TT roundtrip error: {} days",
687 (back.quantity() - tt.quantity()).abs()
688 );
689 }
690
691 #[test]
692 fn ut_to_jd_applies_delta_t() {
693 let ut = Time::<UT>::new(2_451_545.0);
694 let jd: Time<JD> = ut.to::<JD>();
695 // ΔT at J2000 ≈ 63.83 s
696 let offset_secs = (jd.quantity() - ut.quantity()).to::<Second>();
697 assert!(
698 (offset_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
699 "UT→JD offset = {} s, expected ~63.83 s",
700 offset_secs
701 );
702 }
703
704 #[test]
705 fn ut_jd_roundtrip() {
706 let jd = Time::<JD>::new(2_451_545.0);
707 let ut: Time<UT> = jd.to::<UT>();
708 let back: Time<JD> = ut.to::<JD>();
709 assert!(
710 (back.quantity() - jd.quantity()).abs() < Days::new(1e-12),
711 "roundtrip error: {} days",
712 (back.quantity() - jd.quantity()).abs()
713 );
714 }
715
716 #[test]
717 fn ut_from_into() {
718 let ut = Time::<UT>::new(2_451_545.0);
719 let jd: Time<JD> = ut.into();
720 let back: Time<UT> = jd.into();
721 assert!((back.quantity() - ut.quantity()).abs() < Days::new(1e-12));
722 }
723}