deep_time/dt/conversions.rs
1use crate::historical_sofa::historical_sofa_offset_for_non_adjusted;
2use crate::{
3 Drift, Dt, LB_DEN, LB_NUM, LG_DEN, LG_NUM, Scale, TAI_SEC_AT_1972,
4 TCG_TCB_REF_ATTOS_SINCE_J2000, TDB0_ATTOS, TT_TAI_OFFSET,
5};
6
7impl Dt {
8 /// Convenience wrapper for [`Dt::from`](../struct.Dt.html#method.from)
9 #[inline]
10 pub const fn from_dt(dt: Dt, scale: Scale) -> Dt {
11 Self::from(dt.sec, dt.attos, scale)
12 }
13
14 /// Low level constructor from total attoseconds since a given `epoch`.
15 ///
16 /// Simply adds the total attoseconds to the epoch.
17 ///
18 /// ## Examples
19 ///
20 /// ```rust
21 /// use deep_time::Dt;
22 ///
23 /// // A leap second from the middle of the table (36 leap seconds accumulated)
24 /// let original = Dt::from_ymdhms(2015, 6, 30, 23, 59, 60, 123_456_789_000_000_000);
25 ///
26 /// // Round-trip through canonical attoseconds
27 /// let canon = original.to_diff_raw(Dt::UNIX_EPOCH).to_attos();
28 /// let roundtrip1 = Dt::from_attos_since(canon, Dt::UNIX_EPOCH);
29 ///
30 /// assert_eq!(original, roundtrip1, "Canonical round-trip failed");
31 /// ```
32 #[inline]
33 pub const fn from_attos_since(attos: i128, epoch: Dt) -> Self {
34 epoch.add(Dt::from_attos(attos, Scale::TAI))
35 }
36
37 /// Converts this instant to the target scale and returns the signed difference
38 /// from the given epoch.
39 ///
40 /// This is a low-level `const fn` used internally by higher-level conversion
41 /// methods such as [`to_ymdhms_on`](Dt::to_ymdhms_on).
42 ///
43 /// ## Arguments
44 ///
45 /// * `to` — The time scale to convert `self` into before computing the difference.
46 /// * `epoch` — The reference epoch (e.g. [`Dt::UNIX_EPOCH`]) from which the
47 /// difference is calculated.
48 ///
49 /// ## Returns
50 ///
51 /// A [`Dt`] representing the signed difference (seconds + attoseconds) between
52 /// this instant (after conversion to `to`) and the provided `epoch`.
53 ///
54 /// The returned value is a signed offset relative to `epoch` in the `to` scale.
55 /// While it is most commonly used as a pure duration, it can also be interpreted
56 /// as a timestamp when `epoch` is something like [`Dt::UNIX_EPOCH`] (e.g. for
57 /// generating Unix timestamps via `.to_ms()` or `.to_sec()`).
58 ///
59 /// ## See also
60 ///
61 /// * [`Dt::to_internal`](../struct.Dt.html#method.to_internal) — the conversion step used internally.
62 /// * [`Dt::to_diff_raw`](../struct.Dt.html#method.to_diff_raw) — the raw difference method.
63 /// * [`Dt::from_diff_and_scale`](../struct.Dt.html#method.from_diff_and_scale) — the complementary operation.
64 ///
65 /// ## Examples
66 ///
67 /// ```rust
68 /// use deep_time::{Dt, Scale};
69 ///
70 /// let dt = Dt::from_ymdhms(2024, 6, 15, 12, 0, 0, 0);
71 /// let diff = dt.to_scale_and_then_diff(Scale::UTC, Dt::UNIX_EPOCH);
72 ///
73 /// // diff can be used as a Unix timestamp offset
74 /// let unix_ms = diff.to_ms();
75 /// assert!(unix_ms > 1_700_000_000_000);
76 /// ```
77 #[inline]
78 pub const fn to_scale_and_then_diff(&self, to: Scale, epoch: Dt) -> Dt {
79 self.to_internal(to).to_diff_raw(epoch)
80 }
81
82 /// Creates a TAI [`Dt`] by adding a difference to an epoch and interpreting
83 /// the result on the given time scale.
84 ///
85 /// This is the inverse-style counterpart to [`to_scale_and_then_diff`](Dt::to_scale_and_then_diff)
86 /// and is used by [`from_ymdhms_on`](Dt::from_ymdhms_on) and related constructors.
87 ///
88 /// ## Arguments
89 ///
90 /// * `diff` — The signed difference (as a [`Dt`]) to add to the epoch.
91 /// * `epoch` — The reference epoch (commonly [`Dt::UNIX_EPOCH`] or [`Dt::ZERO`]).
92 /// * `current` — The time scale on which `diff` + `epoch` should be interpreted.
93 ///
94 /// ## Returns
95 ///
96 /// A [`Dt`] on the **TAI** scale representing the absolute instant
97 /// `epoch + diff` when interpreted on `current`.
98 ///
99 /// ## Notes
100 ///
101 /// - The input `diff` is treated as being on the `current` scale.
102 /// - The final result is always converted to TAI (the internal canonical representation).
103 ///
104 /// ## See also
105 ///
106 /// * [`Dt::from_dt`](../struct.Dt.html#method.from_dt) — the underlying constructor.
107 /// * [`Dt::to_scale_and_then_diff`](../struct.Dt.html#method.to_scale_and_then_diff) — the complementary operation.
108 /// * [`Dt::from_ymdhms_on`](../struct.Dt.html#method.from_ymdhms_on) — a higher-level user of this function.
109 ///
110 /// ## Examples
111 ///
112 /// ```rust
113 /// use deep_time::{Dt, Scale};
114 ///
115 /// let diff = Dt::new(1_718_467_200, 0); // ~2024-06-15
116 /// let dt = Dt::from_diff_and_scale(diff, Dt::UNIX_EPOCH, Scale::UTC);
117 ///
118 /// let ymd = dt.to_ymdhms(Scale::TAI);
119 /// assert_eq!(ymd.yr, 2024);
120 /// assert_eq!(ymd.mo, 6);
121 /// assert_eq!(ymd.day, 15);
122 /// ```
123 #[inline]
124 pub const fn from_diff_and_scale(diff: Dt, epoch: Dt, current: Scale) -> Self {
125 Dt::from_dt(epoch.add(diff), current)
126 }
127
128 /// Creates a TAI [`Dt`].
129 ///
130 /// - Assumes the given `sec` and `attos` are on the given scale.
131 /// - See [`Scale`] for more information on available time scales.
132 ///
133 /// ## Example
134 ///
135 /// ```
136 /// use deep_time::{Dt, Scale};
137 ///
138 /// let dt = Dt::from(-32, 0, Scale::UTC);
139 ///
140 /// // leap seconds were added to the `-32` UTC sec
141 /// // and the returned [`Dt`] is on the TAI scale
142 /// assert_eq!(dt.sec, 0);
143 /// ```
144 pub const fn from(sec: i64, attos: u64, current: Scale) -> Dt {
145 let raw = Dt::new(sec, attos);
146 match current {
147 Scale::UTC => raw.add(Dt {
148 sec: raw.leap_sec(true).offset,
149 attos: 0,
150 }),
151 Scale::TAI => raw,
152 Scale::TT => raw.sub(TT_TAI_OFFSET),
153 Scale::UTCSpice => {
154 let tai = raw.add(Dt {
155 sec: raw.leap_sec(true).offset,
156 attos: 0,
157 });
158 if sec < TAI_SEC_AT_1972 - 10 {
159 tai.add(Dt::from_sec(9, Scale::TAI))
160 } else {
161 tai
162 }
163 }
164 Scale::UTCSofa => {
165 let tai = raw.add(Dt {
166 sec: raw.leap_sec(true).offset,
167 attos: 0,
168 });
169 if let Some(offset) = historical_sofa_offset_for_non_adjusted(&raw) {
170 tai.add(Dt::from_sec_f(offset))
171 } else {
172 tai
173 }
174 }
175 Scale::GPS | Scale::QZSS | Scale::GST => raw.add(Dt::SEC_19),
176 Scale::BDT => raw.add(Dt::SEC_33),
177 Scale::TDB | Scale::ET => Self::tdb_to_tai(raw),
178 Scale::TCG => {
179 let tt = Self::tcg_to_tt(raw);
180 tt.sub(TT_TAI_OFFSET)
181 }
182 Scale::TCB => {
183 let tdb = Self::tcb_to_tdb(raw);
184 Self::tdb_to_tai(tdb)
185 }
186 Scale::LTC => {
187 let tt = Self::ltc_to_tt(raw);
188 tt.sub(TT_TAI_OFFSET)
189 }
190 Scale::TCL => Self::tcl_to_tai(raw),
191 _ => raw,
192 }
193 }
194
195 pub(crate) const fn to_internal(&self, scale: Scale) -> Dt {
196 match scale {
197 Scale::TAI | Scale::Custom => *self,
198 Scale::UTC => self.sub(Dt {
199 sec: self.leap_sec(false).offset,
200 attos: 0,
201 }),
202 Scale::TT => self.add(TT_TAI_OFFSET),
203 Scale::UTCSpice => {
204 let spice = self.sub(Dt {
205 sec: self.leap_sec(false).offset,
206 attos: 0,
207 });
208 if self.sec < TAI_SEC_AT_1972 {
209 spice.sub(Dt::from_sec_f(f!(9.0)))
210 } else {
211 spice
212 }
213 }
214 Scale::UTCSofa => {
215 let sofa = self.sub(Dt {
216 sec: self.leap_sec(false).offset,
217 attos: 0,
218 });
219 if let Some(offset) = historical_sofa_offset_for_non_adjusted(self) {
220 sofa.sub(Dt::from_sec_f(offset))
221 } else {
222 sofa
223 }
224 }
225 Scale::GPS | Scale::QZSS | Scale::GST => self.sub(Dt::SEC_19),
226 Scale::BDT => self.sub(Dt::SEC_33),
227 Scale::TDB | Scale::ET => Self::tai_to_tdb(*self),
228 Scale::TCG => Self::tai_to_tcg(*self),
229 Scale::TCB => Self::tai_to_tcb(*self),
230 Scale::LTC => {
231 let tt = self.add(TT_TAI_OFFSET);
232 Self::tt_to_ltc(tt)
233 }
234 Scale::TCL => Self::tai_to_tcl(*self),
235 }
236 }
237
238 /// Converts this instant from the given scale into TAI.
239 ///
240 /// This is a convenience wrapper around [`Dt::from`](../struct.Dt.html#method.from) that always
241 /// returns a [`Dt`] on the TAI scale.
242 ///
243 /// ## Arguments
244 ///
245 /// * `current` — The time scale in which `self` is currently expressed.
246 ///
247 /// ## Returns
248 ///
249 /// A [`Dt`] representing the same instant on the **TAI** scale.
250 ///
251 /// ## Notes
252 ///
253 /// - The numerical `sec` and `attos` of `self` are assumed to be on `current`.
254 /// - This method is equivalent to `Dt::from(self.sec, self.attos, current)`.
255 ///
256 /// ## See also
257 ///
258 /// * [`Dt::to`](../struct.Dt.html#method.to) — the general conversion method between any two scales.
259 /// * [`Dt::from`](../struct.Dt.html#method.from) — the underlying constructor.
260 ///
261 /// ## Examples
262 ///
263 /// ```rust
264 /// use deep_time::{Dt, Scale};
265 ///
266 /// let dt_utc = Dt::from_ymdhms(2024, 6, 15, 12, 0, 0, 0);
267 /// let dt_tai = dt_utc.to_tai(Scale::UTC);
268 ///
269 /// assert_eq!(dt_tai.to_ymdhms(Scale::TAI).yr, 2024);
270 /// ```
271 #[inline]
272 pub const fn to_tai(&self, current: Scale) -> Dt {
273 Self::from(self.sec, self.attos, current)
274 }
275
276 /// Converts this instant from one time scale to another.
277 ///
278 /// This is the primary public method for converting between any two supported
279 /// time scales (TAI, UTC, TT, TDB, GPS, TCG, LTC, etc.).
280 ///
281 /// ## Arguments
282 ///
283 /// * `current` — The time scale in which `self` is currently expressed.
284 /// * `new` — The target time scale to convert into.
285 ///
286 /// ## Returns
287 ///
288 /// A [`Dt`] representing the same physical instant on the `new` scale.
289 ///
290 /// If `current == new`, this method returns `*self` without any computation.
291 ///
292 /// ## Notes
293 ///
294 /// - The numerical `sec` and `attos` of `self` are assumed to be on `current`.
295 /// - The returned [`Dt`] contains the correct `sec` and `attos` values for the
296 /// `new` scale (the scale is never stored inside [`Dt`]).
297 /// - This method is `const fn` and performs no heap allocation.
298 ///
299 /// ## See also
300 ///
301 /// * [`Dt::to_tai`](../struct.Dt.html#method.to_tai) — convenience method that always targets TAI.
302 /// * [`Dt::from`](../struct.Dt.html#method.from) — the underlying scale conversion logic.
303 /// * [`Dt::to_internal`](../struct.Dt.html#method.to_internal) — the internal implementation (not public API).
304 ///
305 /// ## Examples
306 ///
307 /// ```rust
308 /// use deep_time::{Dt, Scale};
309 ///
310 /// let dt_tai = Dt::from_ymdhms(2024, 6, 15, 12, 0, 0, 0);
311 ///
312 /// // Convert from TAI to UTC
313 /// let dt_utc = dt_tai.to(Scale::TAI, Scale::UTC);
314 /// let ymd = dt_utc.to_ymdhms(Scale::UTC);
315 ///
316 /// assert_eq!(ymd.yr, 2024);
317 /// assert_eq!(ymd.mo, 6);
318 /// assert_eq!(ymd.day, 15);
319 /// ```
320 #[inline]
321 pub const fn to(&self, current: Scale, new: Scale) -> Dt {
322 if !current.eq(new) {
323 Self::from(self.sec, self.attos, current).to_internal(new)
324 } else {
325 *self
326 }
327 }
328
329 /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
330 /// or clock-drift correction defined by a [`Drift`] model relative to a reference instant.
331 #[inline]
332 pub const fn convert_using_drift(self, reference: Self, drift: Drift) -> Self {
333 let span = self.to_diff_raw(reference);
334 let correction = drift.time_diff_after(&span);
335 self.add(correction)
336 }
337
338 /// Performs the inverse conversion of [`Dt::convert_using_drift`], recovering the original proper
339 /// time on the source clock scale.
340 ///
341 /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
342 /// case of a pure constant offset the function returns immediately without iteration.
343 pub const fn convert_back_using_drift(self, reference: Self, drift: Drift) -> Self {
344 if drift.rate.is_zero() && drift.accel.is_zero() {
345 return self.sub(drift.constant);
346 }
347 let mut guess = self;
348 let mut i = 0u32;
349 while i < 16 {
350 let span = guess.to_diff_raw(reference);
351 let correction = drift.time_diff_after(&span);
352 guess = self.sub(correction);
353 i += 1;
354 }
355 guess
356 }
357
358 #[inline]
359 pub(crate) const fn tai_to_tcg(tai: Self) -> Self {
360 let tt = tai.add(TT_TAI_OFFSET);
361 Self::tt_to_tcg(tt)
362 }
363
364 #[inline]
365 pub(crate) const fn tai_to_tcb(tai: Self) -> Self {
366 let tdb = Self::tai_to_tdb(tai);
367 Self::tdb_to_tcb(tdb)
368 }
369
370 /// Exact integer helper: elapsed attoseconds since the TCG/TCB reference epoch (1977-01-01.0 TAI),
371 /// using only the numerical `sec`/`attos` of the supplied `Dt` (scale is ignored).
372 #[inline]
373 pub(crate) const fn to_attos_since_tcg_tcb_epoch(numerical: Self) -> i128 {
374 numerical.to_attos() - TCG_TCB_REF_ATTOS_SINCE_J2000
375 }
376
377 /// Exact fixed-point multiplication: `attos * num / den` (handles negative values safely, no overflow for library time range).
378 pub(crate) const fn mul_rate(attos: i128, num: i128, den: i128) -> i128 {
379 if attos == 0 {
380 return 0;
381 }
382 let sign = if attos < 0 { -1i128 } else { 1i128 };
383 let a = if attos < 0 { -attos } else { attos };
384 let q = a / den;
385 let r = a % den;
386 sign * (q * num + (r * num) / den)
387 }
388
389 #[inline]
390 pub(crate) const fn mul_lg(attos: i128) -> i128 {
391 Self::mul_rate(attos, LG_NUM, LG_DEN)
392 }
393
394 #[inline]
395 pub(crate) const fn mul_lb(attos: i128) -> i128 {
396 Self::mul_rate(attos, LB_NUM, LB_DEN)
397 }
398
399 pub(crate) const fn tt_to_tcg(tt: Self) -> Self {
400 let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
401 let span_attos = Self::mul_lg(elapsed);
402 tt.add(Dt::from_attos(span_attos, Scale::TAI))
403 }
404
405 pub(crate) const fn tcg_to_tt(tcg: Self) -> Self {
406 let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcg);
407 let span_attos = Self::mul_rate(elapsed_cg, LG_NUM, LG_DEN + LG_NUM);
408 tcg.sub(Dt::from_attos(span_attos, Scale::TAI))
409 }
410
411 pub(crate) const fn tcb_to_tdb(tcb: Self) -> Self {
412 let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcb);
413 let span_attos = Self::mul_rate(elapsed_cg, LB_NUM, LB_DEN + LB_NUM);
414 tcb.sub(Dt::from_attos(span_attos, Scale::TAI))
415 .sub(Dt::from_attos(TDB0_ATTOS, Scale::TAI))
416 }
417
418 pub(crate) const fn tdb_to_tcb(tdb: Self) -> Self {
419 let elapsed = Self::to_attos_since_tcg_tcb_epoch(tdb);
420 let span_attos = Self::mul_lb(elapsed);
421 tdb.add(Dt::from_attos(span_attos, Scale::TAI))
422 .add(Dt::from_attos(TDB0_ATTOS, Scale::TAI))
423 }
424}