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