Skip to main content

deep_time/dt/
to_scale.rs

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