Skip to main content

deep_time/dt/
conversions.rs

1use crate::historical_utc::historical_utc_offset;
2use crate::{
3    Drift, Dt, LB_DEN, LB_NUM, LG_DEN, LG_NUM, Scale, TCG_TCB_REF_ATTOS_SINCE_J2000, TDB0_ATTOS,
4    TT_TAI_OFFSET,
5};
6
7impl Dt {
8    /// Converts this instant to its internally stored `target` scale and returns
9    /// the signed difference from the given epoch.
10    ///
11    /// This is a low-level `const fn` used internally by higher-level conversion
12    /// methods such as [`to_ymd`](Dt::to_ymd).
13    ///
14    /// ## Arguments
15    ///
16    /// - `epoch` — The reference epoch (e.g. [`Dt::UNIX_EPOCH`]) from which the
17    ///   difference is calculated.
18    /// - `convert_epoch` — Whether to also convert the provided `epoch` to this
19    ///   [`Dt`]'s `target` time scale.
20    ///
21    /// ## Returns
22    ///
23    /// A [`Dt`] representing the signed difference (seconds + attoseconds) between
24    /// this instant (after conversion to `to`) and the provided `epoch`.
25    ///
26    /// It can be interpreted as a timestamp when `epoch` is something like
27    /// [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) (e.g. for
28    /// generating Unix timestamps via `.to_ms()` or `.to_sec()`).
29    ///
30    /// ## See also
31    ///
32    /// * [`Dt::to`](../struct.Dt.html#method.to).
33    /// * [`Dt::to_diff_raw`](../struct.Dt.html#method.to_diff_raw).
34    /// * [`Dt::from_diff_and_scale`](../struct.Dt.html#method.from_diff_and_scale).
35    ///
36    /// ## Examples
37    ///
38    /// ```rust
39    /// use deep_time::{Dt, Scale};
40    ///
41    /// let dt = Dt::from_ymd(2024, 6, 15, 12, 0, 0, 0, Scale::UTC);
42    /// let diff = dt.to_scale_and_diff(Dt::UNIX_EPOCH, true);
43    ///
44    /// // diff can be used as a Unix timestamp offset
45    /// let unix_ms = diff.to_ms();
46    /// assert!(unix_ms > 1_700_000_000_000);
47    /// ```
48    pub const fn to_scale_and_diff(&self, epoch: Dt, convert_epoch: bool) -> Dt {
49        if convert_epoch {
50            self.to(self.target).to_diff_raw(epoch.to(self.target))
51        } else {
52            self.to(self.target).to_diff_raw(epoch)
53        }
54    }
55
56    /// Creates a **TAI** [`Dt`] by adding a difference to an epoch and interpreting
57    /// the result on the given time scale.
58    ///
59    /// This is the inverse counterpart to
60    /// [`Dt::to_scale_and_diff`](../struct.Dt.html#method.to_scale_and_diff)
61    /// and is used by [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd)
62    /// and related constructors.
63    ///
64    /// ## Arguments
65    ///
66    /// - `diff` — The signed difference (as a [`Dt`]) to add to the epoch.
67    /// - `epoch` — The reference epoch (commonly
68    ///   [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) or
69    ///   [`Dt::ZERO`](../struct.Dt.html#associatedconstant.ZERO)).
70    /// - `current` — The time scale on which `diff` + `epoch` should be interpreted.
71    ///
72    /// ## Returns
73    ///
74    /// A [`Dt`] on the **TAI** scale representing the absolute instant
75    /// `epoch + diff` when interpreted on `current`.
76    ///
77    /// ## Notes
78    ///
79    /// - The input `diff` is treated as being on the `current` scale.
80    /// - The final result is always converted to TAI (the internal canonical representation).
81    ///
82    /// ## See also
83    ///
84    /// - [`Dt::to_scale_and_diff`](../struct.Dt.html#method.to_scale_and_diff)
85    /// - [`Dt::from_attos`](../struct.Dt.html#method.from_attos)
86    ///
87    /// ## Examples
88    ///
89    /// ```rust
90    /// use deep_time::{Dt, Scale};
91    ///
92    /// let diff = Dt::from_tai_sec(1_718_467_200); // ~2024-06-15
93    /// let dt = Dt::from_diff_and_scale(diff, Dt::UNIX_EPOCH, true);
94    ///
95    /// let ymd = dt.to_ymd();
96    /// assert_eq!(ymd.yr(), 2024);
97    /// assert_eq!(ymd.mo(), 6);
98    /// assert_eq!(ymd.day(), 15);
99    /// ```
100    pub const fn from_diff_and_scale(diff: Dt, epoch: Dt, convert_epoch: bool) -> Dt {
101        if convert_epoch {
102            Self::from_attos(
103                epoch
104                    .to(diff.scale)
105                    .to_attos()
106                    .saturating_add(diff.to_attos()),
107                diff.scale,
108            )
109        } else {
110            Self::from_attos(epoch.to_attos().saturating_add(diff.to_attos()), diff.scale)
111        }
112    }
113
114    /// Converts the internal attos to be on the TAI time [`Scale`].
115    ///
116    /// ```rust
117    /// use deep_time::{Dt, Scale};
118    ///
119    /// let tai = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::UTC);
120    /// let tt = tai.to(Scale::TT);
121    ///
122    /// assert_eq!(tt.scale, Scale::TT);
123    ///
124    /// let roundtrip = tt.to_tai();
125    ///
126    /// assert_eq!(tai.scale, Scale::TAI);
127    /// assert_eq!(roundtrip, tai);
128    /// ```
129    ///
130    /// See [`Dt::to`](../struct.Dt.html#method.to) for more info.
131    pub const fn to_tai(&self) -> Dt {
132        match self.scale {
133            // we're going utc -> tai, check if it's
134            // post 1972 using the leap seconds table
135            Scale::UTC | Scale::UtcHist | Scale::UtcSpice => match self.utc_to_tai() {
136                // leap seconds table returned an offset, so use that
137                Some(dt) => dt.with(Scale::TAI),
138                // leap seconds table returned None so it must be pre 1972
139                None => match self.scale {
140                    Scale::UtcHist => match historical_utc_offset(&self) {
141                        Some(offset) => self.add(Dt::span_f(offset)).with(Scale::TAI),
142                        None => self.with(Scale::TAI),
143                    },
144                    Scale::UtcSpice => self.add_sec(9).with(Scale::TAI),
145                    _ => self.with(Scale::TAI),
146                },
147            },
148            Scale::TAI => *self,
149            Scale::TT => Dt::new(
150                self.attos.saturating_sub(TT_TAI_OFFSET.to_attos()),
151                Scale::TAI,
152                self.target,
153            ),
154            Scale::GPS | Scale::QZSS | Scale::GST => Dt::new(
155                self.attos.saturating_add(Dt::SEC_19.to_attos()),
156                Scale::TAI,
157                self.target,
158            ),
159            Scale::BDT => Dt::new(
160                self.attos.saturating_add(Dt::SEC_33.to_attos()),
161                Scale::TAI,
162                self.target,
163            ),
164            Scale::TDB | Scale::ET => {
165                Self::tdb_to_tai(Dt::new(self.attos, Scale::TAI, self.target))
166            }
167            Scale::TCG => {
168                let tt = Self::tcg_to_tt(Dt::new(self.attos, Scale::TAI, self.target));
169                tt.sub(TT_TAI_OFFSET)
170            }
171            Scale::TCB => {
172                let tdb = Self::tcb_to_tdb(Dt::new(self.attos, Scale::TAI, self.target));
173                Self::tdb_to_tai(tdb)
174            }
175            Scale::LTC => {
176                let tt = Self::ltc_to_tt(Dt::new(self.attos, Scale::TAI, self.target));
177                tt.sub(TT_TAI_OFFSET)
178            }
179            Scale::TCL => Self::tcl_to_tai(Dt::new(self.attos, Scale::TAI, self.target)),
180            _ => Dt::new(self.attos, Scale::TAI, self.target),
181        }
182    }
183
184    /// Converts directly to `new` [`Scale`], without first converting to TAI.
185    ///
186    /// **Warning:**
187    ///
188    /// - This function should really only be used if the [`Dt`] is on the TAI
189    ///   time scale, or if you really know what you're doing.
190    /// - For the normal time scale conversion function see
191    ///   [`Dt::to`](../struct.Dt.html#method.to) which correctly first converts
192    ///   to TAI before converting to the target scale.
193    pub const fn convert(&self, new: Scale) -> Dt {
194        match new {
195            Scale::TAI => self.to_tai(),
196            Scale::UTC | Scale::UtcHist | Scale::UtcSpice => match self.tai_to_utc() {
197                // leap seconds table returned an offset, so use that
198                Some(dt) => dt.with(new),
199                // leap seconds table returned None so it must be pre 1972
200                None => match self.scale {
201                    Scale::UtcHist => match historical_utc_offset(&self) {
202                        Some(offset) => self.sub(Dt::span_f(offset)).with(new),
203                        None => self.with(new),
204                    },
205                    Scale::UtcSpice => self.add_sec(-9).with(new),
206                    _ => self.with(new),
207                },
208            },
209            Scale::TT => self.add(TT_TAI_OFFSET).with(new),
210            Scale::GPS | Scale::QZSS | Scale::GST => {
211                self.add_attos(-Dt::SEC_19.to_attos()).with(new)
212            }
213            Scale::BDT => self.add_attos(-Dt::SEC_33.to_attos()).with(new),
214            Scale::TDB | Scale::ET => Self::tai_to_tdb(*self).with(new),
215            Scale::TCG => Self::tai_to_tcg(*self).with(new),
216            Scale::TCB => Self::tai_to_tcb(*self).with(new),
217            Scale::LTC => {
218                let tt = self.add(TT_TAI_OFFSET);
219                Self::tt_to_ltc(tt).with(new)
220            }
221            Scale::TCL => Self::tai_to_tcl(*self).with(new),
222            _ => *self,
223        }
224    }
225
226    /// Converts this instant to another time scale, going via TAI.
227    ///
228    /// Essentially when converting TT to TDB the internal process goes like TT
229    /// -> TAI -> TDB. It uses the [`Dt`]s `scale` field to determine what scale
230    /// to convert from to TAI, and then the `new` arg dictates the new time scale.
231    ///
232    /// - It is not necessary to do this if you just want to use such functions
233    ///   as [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) as these internally
234    ///   convert to the scale of the object's `target` field before output.
235    /// - If a TAI [`Dt`] was created using
236    ///   [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd) and the datetime
237    ///   had 60 seconds, converting to UTC would lose that info. To round trip a
238    ///   60 second UTC datetime you need only set the
239    ///   [`Dt::target`](../struct.Dt.html#method.target) [`Scale`] to `UTC` and
240    ///   then call the desired output function, such as
241    ///   [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd).
242    /// - The internal `attos` field changes to be on the new time scale.
243    /// - The [`Dt`]s `target` field is ignored and left unchanged.
244    /// - The [`Dt`]s `scale` field is changed to the new [`Scale`].
245    ///
246    /// ## Returns
247    ///
248    /// - A [`Dt`] representing the same physical instant but on the `new` scale.
249    /// - The returned objects `scale` field has been changed to `new`.
250    ///
251    /// If `current == new`, this method returns `*self` without any computation.
252    ///
253    /// ## See also
254    ///
255    /// * [`Dt::to_tai`](../struct.Dt.html#method.to_tai)
256    /// * [`Dt::from_attos`](../struct.Dt.html#method.from_attos)
257    ///
258    /// ## Examples
259    ///
260    /// ```rust
261    /// use deep_time::{Dt, Scale};
262    ///
263    /// let tai = Dt::from_ymd(2024, 6, 15, 12, 0, 0, 0, Scale::UTC);
264    /// let tt = tai.to(Scale::TT);
265    /// let tdb = tt.to(Scale::TDB);
266    /// let roundtrip = tdb.to(Scale::TAI);
267    ///
268    /// let ymd = roundtrip.to_ymd();
269    ///
270    /// assert_eq!(ymd.yr(), 2024);
271    /// assert_eq!(ymd.mo(), 6);
272    /// assert_eq!(ymd.day(), 15);
273    /// assert_eq!(ymd.hr(), 12);
274    /// assert_eq!(ymd.min(), 0);
275    /// assert_eq!(ymd.sec(), 0);
276    /// assert_eq!(ymd.attos(), 0);
277    /// ```
278    #[inline]
279    pub const fn to(&self, new: Scale) -> Dt {
280        if matches!(self.scale, Scale::TAI) {
281            self.convert(new)
282        } else if !self.scale.eq(new) {
283            self.to_tai().convert(new)
284        } else {
285            *self
286        }
287    }
288
289    #[inline(always)]
290    pub(crate) const fn utc_to_tai(&self) -> Option<Dt> {
291        match self.leap_sec(true) {
292            Some(info) => Some(self.add_sec(info.offset as i128)),
293            None => None,
294        }
295    }
296
297    #[inline(always)]
298    pub(crate) const fn tai_to_utc(&self) -> Option<Dt> {
299        match self.leap_sec(false) {
300            Some(info) => Some(self.add_sec(-info.offset as i128)),
301            None => None,
302        }
303    }
304
305    #[inline]
306    pub(crate) const fn tai_to_tcg(tai: Dt) -> Dt {
307        let tt = tai.add(TT_TAI_OFFSET);
308        Self::tt_to_tcg(tt)
309    }
310
311    #[inline]
312    pub(crate) const fn tai_to_tcb(tai: Dt) -> Dt {
313        let tdb = Self::tai_to_tdb(tai);
314        Self::tdb_to_tcb(tdb)
315    }
316
317    /// Exact integer helper: elapsed attoseconds since the TCG/TCB reference epoch (1977-01-01.0 TAI),
318    /// using only the numerical value of the supplied `Dt` (scale is ignored).
319    #[inline]
320    pub(crate) const fn to_attos_since_tcg_tcb_epoch(numerical: Dt) -> i128 {
321        numerical.to_attos() - TCG_TCB_REF_ATTOS_SINCE_J2000
322    }
323
324    /// Exact fixed-point multiplication: `attos * num / den` (handles negative values safely, no overflow for library time range).
325    pub(crate) const fn mul_rate(attos: i128, num: i128, den: i128) -> i128 {
326        if attos == 0 {
327            return 0;
328        }
329        let sign = if attos < 0 { -1i128 } else { 1i128 };
330        let a = if attos < 0 { -attos } else { attos };
331        let q = a / den;
332        let r = a % den;
333        sign * (q * num + (r * num) / den)
334    }
335
336    #[inline]
337    pub(crate) const fn mul_lg(attos: i128) -> i128 {
338        Self::mul_rate(attos, LG_NUM, LG_DEN)
339    }
340
341    #[inline]
342    pub(crate) const fn mul_lb(attos: i128) -> i128 {
343        Self::mul_rate(attos, LB_NUM, LB_DEN)
344    }
345
346    pub(crate) const fn tt_to_tcg(tt: Dt) -> Dt {
347        let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
348        let span_attos = Self::mul_lg(elapsed);
349        tt.add_attos(span_attos)
350    }
351
352    pub(crate) const fn tcg_to_tt(tcg: Dt) -> Dt {
353        let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcg);
354        let span_attos = Self::mul_rate(elapsed_cg, LG_NUM, LG_DEN + LG_NUM);
355        tcg.add_attos(-span_attos)
356    }
357
358    pub(crate) const fn tcb_to_tdb(tcb: Dt) -> Dt {
359        let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcb);
360        let span_attos = Self::mul_rate(elapsed_cg, LB_NUM, LB_DEN + LB_NUM);
361        tcb.add_attos(-span_attos).add_attos(-TDB0_ATTOS)
362    }
363
364    pub(crate) const fn tdb_to_tcb(tdb: Dt) -> Dt {
365        let elapsed = Self::to_attos_since_tcg_tcb_epoch(tdb);
366        let span_attos = Self::mul_lb(elapsed);
367        tdb.add_attos(span_attos).add_attos(TDB0_ATTOS)
368    }
369
370    /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
371    /// or clock-drift correction defined by a [`Drift`] model relative to a reference instant.
372    #[inline]
373    pub const fn convert_using_drift(self, reference: Dt, drift: Drift) -> Dt {
374        let span = self.to_diff_raw(reference);
375        let correction = drift.time_diff_after(&span);
376        self.add(correction)
377    }
378
379    /// Performs the inverse conversion of [`Dt::convert_using_drift`], recovering the original proper
380    /// time on the source clock scale.
381    ///
382    /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
383    /// case of a pure constant offset the function returns immediately without iteration.
384    pub const fn convert_back_using_drift(self, reference: Dt, drift: Drift) -> Dt {
385        if drift.rate.is_zero() && drift.accel.is_zero() {
386            return self.sub(drift.constant);
387        }
388        let mut guess = self;
389        let mut i = 0u32;
390        while i < 16 {
391            let span = guess.to_diff_raw(reference);
392            let correction = drift.time_diff_after(&span);
393            guess = self.sub(correction);
394            i += 1;
395        }
396        guess
397    }
398}