Skip to main content

deep_time/dt/
conversions.rs

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