deep_time/scale.rs
1use core::fmt;
2
3/// Time scales supported for conversions.
4///
5/// This `#[non_exhaustive]` enum defines the complete set of time scales used by
6/// the library for representing instants [`Dt`] and performing conversions
7/// between them.
8///
9/// It covers atomic, dynamical, coordinate, civil/coordinated, GNSS, and emerging
10/// lunar scales, plus a `Custom` variant for mission-specific or experimental use.
11///
12/// ## Overview
13///
14/// Time scales fall into several broad categories:
15///
16/// - **Atomic / proper time scales**: TAI (basis), TT, TDB/ET — continuous and
17/// suitable for internal representation and dynamical modeling.
18/// - **Coordinate time scales** (relativistic): TCG, TCB, **TCL** — defined in
19/// specific reference frames (GCRS, BCRS, LCRS). Ideal for ephemeris
20/// integration and high-accuracy modeling; not directly realized by clocks.
21/// - **Coordinated / civil scales**: UTC (atomic time with leap seconds inserted
22/// to keep it close to UT1), **UT1** (observed Earth rotation angle — does **not**
23/// use leap seconds), and the lunar operational scale **LTC** (uses defined
24/// secular rate offsets for traceability and cislunar operations).
25/// - **GNSS / navigation scales**: GPS, GST, BDT, QZSS — tied to specific
26/// satellite constellations.
27/// - **Custom**: Fallback for custom scales.
28///
29/// The library's epoch when performing conversions between all scales is
30/// 2000-01-01 noon.
31///
32/// ## Lunar Time Scales (LTC and TCL)
33///
34/// The library provides high-accuracy implementations of both lunar time scales
35/// based on the **LTE440** model (Lu et al. 2025, A&A 704, A76):
36///
37/// - [`LTC`] (Coordinated Lunar Time): Applies the secular rate offset
38/// (`L_M ≈ +56.02 µs/day`) **plus** the 13 dominant periodic terms from LTE440.
39/// Conversions use fixed-point iteration for numerical stability.
40/// Achieves sub-nanosecond accuracy (< 0.15 ns before 2050) when the periodic
41/// terms are included.
42/// - [`TCL`] (Lunar Coordinate Time): IAU-defined relativistic coordinate time
43/// in the LCRS. The implementation includes the secular rate vs TDB, the same
44/// LTE440 periodic terms, and a constant bias calibrated so that the model
45/// reproduces the official LTE440 reference value at J2000.0 TDB.
46/// Inverse conversion also uses fixed-point iteration.
47///
48/// See the documentation on the individual variants for rates, historical
49/// models, and conversion notes.
50///
51/// ## Features
52///
53/// - `serde` — full serialization/deserialization support.
54/// - `js` — TypeScript definitions via `tsify`.
55///
56/// ## Non-exhaustive
57///
58/// The enum is marked `#[non_exhaustive]` so new scales can be added in
59/// future minor versions without breaking changes.
60#[non_exhaustive]
61#[repr(u8)]
62#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[cfg_attr(feature = "js", derive(tsify::Tsify))]
65pub enum Scale {
66 /// TAI is the representation of an Epoch internally.
67 #[default]
68 TAI,
69
70 /// Terrestrial Time (TT) (previously called Terrestrial Dynamical Time (TDT)).
71 TT,
72
73 /// Ephemeris Time as defined by NASA/NAIF SPICE (identical to TDB).
74 ET,
75
76 /// Barycentric Dynamical Time (TDB) — SPICE ephemeris time (ET is an alias for this).
77 TDB,
78
79 /// Universal Coordinated Time using modern IERS leap second rules.
80 UTC,
81
82 /// Universal Coordinated Time using the SPICE historical model
83 /// (fixed +9 s offset against TAI for all dates before 1972-01-01).
84 UTCSpice,
85
86 /// Universal Coordinated Time using the full SOFA historical model
87 /// (varying fractional "rubber second" offsets from 1960–1971).
88 UTCSofa,
89
90 /// GPS Time scale whose reference epoch is UTC midnight between 05 January and
91 /// 06 January 1980.
92 GPS,
93
94 /// Galileo Time scale.
95 GST,
96
97 /// BeiDou Time scale.
98 BDT,
99
100 /// QZSS Time scale has the same properties as GPS but with dedicated clocks.
101 QZSS,
102
103 /// **Geocentric Coordinate Time (TCG)** – relativistic coordinate time in the
104 /// Geocentric Celestial Reference System (GCRS).
105 TCG,
106
107 /// **Barycentric Coordinate Time (TCB)** – relativistic coordinate time in the
108 /// Barycentric Celestial Reference System (BCRS).
109 TCB,
110
111 /// **Coordinated Lunar Time (LTC)** – NASA’s operational lunar time scale
112 /// for Artemis and cislunar operations (based on the NIST/Ashby & Patla
113 /// relativistic framework).
114 ///
115 /// Implements the full **LTE440** model (Lu et al. 2025):
116 /// - Secular rate: **+56.02 µs per Earth day** (`L_M = 6.48378 × 10^{-10}`)
117 /// relative to terrestrial time.
118 /// - Plus the 13 dominant periodic terms (> 1 µs amplitude) from the LTE440
119 /// ephemeris.
120 LTC,
121
122 /// **Lunar Coordinate Time (TCL)** – IAU-defined (2024 Resolution II)
123 /// relativistic coordinate time in the Lunar Celestial Reference System (LCRS).
124 ///
125 /// Directly analogous to **TCG**. This is the theoretical coordinate time
126 /// at the Moon’s center of mass.
127 ///
128 /// The implementation follows the **LTE440** model (Lu et al. 2025):
129 /// - Secular rate vs TDB (`L_D^M`).
130 /// - The same 13-term LTE440 periodic series used for LTC.
131 /// - A constant bias (`TCL_TDB_BIAS_SPAN`) calibrated so the model
132 /// reproduces the published LTE440 reference value at J2000.0 TDB.
133 TCL,
134
135 /// Custom / user-defined type.
136 Custom,
137}
138
139impl Scale {
140 /// Returns `true` if this scale is TAI.
141 #[inline]
142 pub const fn is_tai(&self) -> bool {
143 matches!(self, Self::TAI)
144 }
145
146 /// Converts this [`Scale`] to UTC.
147 /// - If the scale is already one of the UTC variants
148 /// including historical UTC then no change occurs.
149 #[inline]
150 pub const fn to_utc(&self) -> Scale {
151 if self.uses_leap_seconds() {
152 *self
153 } else {
154 Scale::UTC
155 }
156 }
157
158 /// Returns `true` if this scale accounts for leap seconds
159 /// (or historical UTC civil time rules).
160 #[inline]
161 pub const fn uses_leap_seconds(&self) -> bool {
162 matches!(self, Self::UTC | Self::UTCSpice | Self::UTCSofa)
163 }
164
165 /// Returns `true` if this scale is based off a GNSS constellation.
166 #[inline]
167 pub const fn is_gnss(&self) -> bool {
168 matches!(self, Self::GPS | Self::GST | Self::BDT | Self::QZSS)
169 }
170
171 /// Parse scale from abbreviation.
172 /// Returns `None` for any non-ASCII input.
173 pub fn from_abbrev(s: &str) -> Option<Self> {
174 let bytes = s.as_bytes();
175 if !bytes.is_ascii() {
176 return None;
177 }
178 let mut buf = [0u8; 8];
179 let mut len = 0;
180 for &byte in bytes {
181 if len >= 8 {
182 return None;
183 }
184 buf[len] = if byte.is_ascii_lowercase() {
185 byte - 32
186 } else {
187 byte
188 };
189 len += 1;
190 }
191 let upper = core::str::from_utf8(&buf[..len]).ok()?;
192 match upper {
193 "TAI" => Some(Self::TAI),
194 "TT" => Some(Self::TT),
195 "ET" => Some(Self::ET),
196 "TDB" => Some(Self::TDB),
197 "UTC" => Some(Self::UTC),
198 "UTCSPICE" => Some(Self::UTCSpice),
199 "UTCSOFA" => Some(Self::UTCSofa),
200 "GPS" => Some(Self::GPS),
201 "GST" => Some(Self::GST),
202 "BDT" => Some(Self::BDT),
203 "QZSS" => Some(Self::QZSS),
204 "TCG" => Some(Self::TCG),
205 "TCB" => Some(Self::TCB),
206 "LTC" => Some(Self::LTC),
207 "TCL" => Some(Self::TCL),
208 "CUSTOM" => Some(Self::Custom),
209 _ => None,
210 }
211 }
212
213 /// Short abbreviation used for formatting / display (e.g. "TAI", "UTC", "UTCSpice").
214 pub const fn abbrev(&self) -> &'static str {
215 match self {
216 Self::TAI => "TAI",
217 Self::TT => "TT",
218 Self::ET => "ET",
219 Self::TDB => "TDB",
220 Self::UTC => "UTC",
221 Self::UTCSpice => "UTCSPICE",
222 Self::UTCSofa => "UTCSOFA",
223 Self::TCG => "TCG",
224 Self::TCB => "TCB",
225 Self::GPS => "GPS",
226 Self::GST => "GST",
227 Self::BDT => "BDT",
228 Self::QZSS => "QZSS",
229 Self::LTC => "LTC",
230 Self::TCL => "TCL",
231 Self::Custom => "CUSTOM",
232 }
233 }
234
235 /// Const-friendly equality comparison.
236 #[inline(always)]
237 pub const fn eq(self, other: Self) -> bool {
238 self.to_u8() == other.to_u8()
239 }
240
241 /// Size of the canonical wire representation in bytes.
242 pub const WIRE_SIZE: usize = 1;
243
244 /// Attempts to reconstruct a `Scale` from its wire byte representation.
245 ///
246 /// - Returns `Custom` for any value that does not correspond to a known variant.
247 /// - This provides safe deserialization from untrusted sources.
248 pub const fn from_u8(v: u8) -> Scale {
249 match v {
250 0 => Self::TAI,
251 1 => Self::TT,
252 2 => Self::ET,
253 3 => Self::TDB,
254 4 => Self::UTC,
255 5 => Self::UTCSpice,
256 6 => Self::UTCSofa,
257 7 => Self::GPS,
258 8 => Self::GST,
259 9 => Self::BDT,
260 10 => Self::QZSS,
261 11 => Self::TCG,
262 12 => Self::TCB,
263 13 => Self::LTC,
264 14 => Self::TCL,
265 _ => Self::Custom,
266 }
267 }
268
269 /// Returns the wire representation of this `Scale` as a single byte.
270 ///
271 /// The returned byte is the `repr(u8)` discriminant of the enum.
272 /// This is the canonical on-wire form used by [`Dt`].
273 #[inline(always)]
274 pub const fn to_u8(self) -> u8 {
275 self as u8
276 }
277}
278
279impl fmt::Display for Scale {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 f.write_str(self.abbrev())
282 }
283}