Skip to main content

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