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 [`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}