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
57 /// [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) (e.g. for
58 /// generating Unix timestamps via `.to_ms()` or `.to_sec()`).
59 ///
60 /// ## See also
61 ///
62 /// * [`Dt::to`](../struct.Dt.html#method.to).
63 /// * [`Dt::to_diff_raw`](../struct.Dt.html#method.to_diff_raw).
64 /// * [`Dt::from_diff_and_scale`](../struct.Dt.html#method.from_diff_and_scale).
65 ///
66 /// ## Examples
67 ///
68 /// ```rust
69 /// use deep_time::{Dt, Scale};
70 ///
71 /// let dt = Dt::from_ymdhms(2024, 6, 15, 12, 0, 0, 0);
72 /// let diff = dt.to_scale_and_then_diff(Scale::UTC, Dt::UNIX_EPOCH);
73 ///
74 /// // diff can be used as a Unix timestamp offset
75 /// let unix_ms = diff.to_ms();
76 /// assert!(unix_ms > 1_700_000_000_000);
77 /// ```
78 #[inline]
79 pub const fn to_scale_and_then_diff(&self, to: Scale, epoch: Dt) -> Dt {
80 self.to_internal(to).to_diff_raw(epoch)
81 }
82
83 /// Creates a **TAI** [`Dt`] by adding a difference to an epoch and interpreting
84 /// the result on the given time scale.
85 ///
86 /// This is the inverse-style counterpart to
87 /// [`Dt::to_scale_and_then_diff`](../struct.Dt.html#method.to_scale_and_then_diff)
88 /// and is used by [`Dt::from_ymdhms_on`](../struct.Dt.html#method.from_ymdhms_on)
89 /// and related constructors.
90 ///
91 /// ## Arguments
92 ///
93 /// * `diff` — The signed difference (as a [`Dt`]) to add to the epoch.
94 /// * `epoch` — The reference epoch (commonly
95 /// [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) or
96 /// [`Dt::ZERO`](../struct.Dt.html#associatedconstant.ZERO)).
97 /// * `current` — The time scale on which `diff` + `epoch` should be interpreted.
98 ///
99 /// ## Returns
100 ///
101 /// A [`Dt`] on the **TAI** scale representing the absolute instant
102 /// `epoch + diff` when interpreted on `current`.
103 ///
104 /// ## Notes
105 ///
106 /// - The input `diff` is treated as being on the `current` scale.
107 /// - The final result is always converted to TAI (the internal canonical representation).
108 ///
109 /// ## See also
110 ///
111 /// * [`Dt::from_dt`](../struct.Dt.html#method.from_dt) — the underlying constructor.
112 /// * [`Dt::to_scale_and_then_diff`](../struct.Dt.html#method.to_scale_and_then_diff) — the complementary operation.
113 /// * [`Dt::from_ymdhms_on`](../struct.Dt.html#method.from_ymdhms_on) — a higher-level user of this function.
114 ///
115 /// ## Examples
116 ///
117 /// ```rust
118 /// use deep_time::{Dt, Scale};
119 ///
120 /// let diff = Dt::new(1_718_467_200, 0); // ~2024-06-15
121 /// let dt = Dt::from_diff_and_scale(diff, Dt::UNIX_EPOCH, Scale::UTC);
122 ///
123 /// let ymd = dt.to_ymdhms(Scale::TAI);
124 /// assert_eq!(ymd.yr(), 2024);
125 /// assert_eq!(ymd.mo(), 6);
126 /// assert_eq!(ymd.day(), 15);
127 /// ```
128 #[inline]
129 pub const fn from_diff_and_scale(diff: Dt, epoch: Dt, current: Scale) -> Self {
130 Dt::from_dt(epoch.add(diff), current)
131 }
132
133 /// Creates a **TAI** [`Dt`].
134 ///
135 /// - Assumes the given `sec` and `attos` are on the given scale.
136 /// - See [`Scale`] for more information on available time scales.
137 ///
138 /// ## Example
139 ///
140 /// ```
141 /// use deep_time::{Dt, Scale};
142 ///
143 /// let dt = Dt::from(-32, 0, Scale::UTC);
144 ///
145 /// // leap seconds were added to the `-32` UTC sec
146 /// // and the returned [`Dt`] is on the TAI scale
147 /// assert_eq!(dt.sec, 0);
148 /// ```
149 pub const fn from(sec: i64, attos: u64, current: Scale) -> Dt {
150 let raw = Dt::new(sec, attos);
151 match current {
152 Scale::UTC => Dt {
153 sec: raw.sec.saturating_add(raw.leap_sec(true).offset),
154 attos: raw.attos,
155 },
156 Scale::TAI => raw,
157 Scale::TT => raw.sub(TT_TAI_OFFSET),
158 Scale::UTCSpice => {
159 let tai = raw.add(Dt {
160 sec: raw.leap_sec(true).offset,
161 attos: 0,
162 });
163 if sec < TAI_SEC_AT_1972 - 10 {
164 tai.add(Dt::from_sec(9, Scale::TAI))
165 } else {
166 tai
167 }
168 }
169 Scale::UTCSofa => {
170 let tai = raw.add(Dt {
171 sec: raw.leap_sec(true).offset,
172 attos: 0,
173 });
174 if let Some(offset) = historical_sofa_offset_for_non_adjusted(&raw) {
175 tai.add(Dt::from_sec_f(offset))
176 } else {
177 tai
178 }
179 }
180 Scale::GPS | Scale::QZSS | Scale::GST => raw.add(Dt::SEC_19),
181 Scale::BDT => raw.add(Dt::SEC_33),
182 Scale::TDB | Scale::ET => Self::tdb_to_tai(raw),
183 Scale::TCG => {
184 let tt = Self::tcg_to_tt(raw);
185 tt.sub(TT_TAI_OFFSET)
186 }
187 Scale::TCB => {
188 let tdb = Self::tcb_to_tdb(raw);
189 Self::tdb_to_tai(tdb)
190 }
191 Scale::LTC => {
192 let tt = Self::ltc_to_tt(raw);
193 tt.sub(TT_TAI_OFFSET)
194 }
195 Scale::TCL => Self::tcl_to_tai(raw),
196 _ => raw,
197 }
198 }
199
200 pub(crate) const fn to_internal(&self, scale: Scale) -> Dt {
201 match scale {
202 Scale::TAI | Scale::Custom => *self,
203 Scale::UTC => Dt {
204 sec: self.sec.saturating_sub(self.leap_sec(false).offset),
205 attos: self.attos,
206 },
207 Scale::TT => self.add(TT_TAI_OFFSET),
208 Scale::UTCSpice => {
209 let spice = self.sub(Dt {
210 sec: self.leap_sec(false).offset,
211 attos: 0,
212 });
213 if self.sec < TAI_SEC_AT_1972 {
214 spice.sub(Dt::from_sec_f(f!(9.0)))
215 } else {
216 spice
217 }
218 }
219 Scale::UTCSofa => {
220 let sofa = self.sub(Dt {
221 sec: self.leap_sec(false).offset,
222 attos: 0,
223 });
224 if let Some(offset) = historical_sofa_offset_for_non_adjusted(self) {
225 sofa.sub(Dt::from_sec_f(offset))
226 } else {
227 sofa
228 }
229 }
230 Scale::GPS | Scale::QZSS | Scale::GST => self.sub(Dt::SEC_19),
231 Scale::BDT => self.sub(Dt::SEC_33),
232 Scale::TDB | Scale::ET => Self::tai_to_tdb(*self),
233 Scale::TCG => Self::tai_to_tcg(*self),
234 Scale::TCB => Self::tai_to_tcb(*self),
235 Scale::LTC => {
236 let tt = self.add(TT_TAI_OFFSET);
237 Self::tt_to_ltc(tt)
238 }
239 Scale::TCL => Self::tai_to_tcl(*self),
240 }
241 }
242
243 /// Converts this instant from the given scale into TAI.
244 ///
245 /// This is a convenience wrapper around [`Dt::from`](../struct.Dt.html#method.from) that always
246 /// returns a [`Dt`] on the TAI scale.
247 ///
248 /// ## Arguments
249 ///
250 /// * `current` — The time scale in which `self` is currently expressed.
251 ///
252 /// ## Returns
253 ///
254 /// A [`Dt`] representing the same instant on the **TAI** scale.
255 ///
256 /// ## Notes
257 ///
258 /// - The numerical `sec` and `attos` of `self` are assumed to be on `current`.
259 /// - This method is equivalent to `Dt::from(self.sec, self.attos, current)`.
260 ///
261 /// ## See also
262 ///
263 /// * [`Dt::to`](../struct.Dt.html#method.to) — the general conversion method between any two scales.
264 /// * [`Dt::from`](../struct.Dt.html#method.from) — the underlying constructor.
265 ///
266 /// ## Examples
267 ///
268 /// ```rust
269 /// use deep_time::{Dt, Scale};
270 ///
271 /// let dt_utc = Dt::from_ymdhms(2024, 6, 15, 12, 0, 0, 0);
272 /// let dt_tai = dt_utc.to_tai(Scale::UTC);
273 ///
274 /// assert_eq!(dt_tai.to_ymdhms(Scale::TAI).yr(), 2024);
275 /// ```
276 #[inline]
277 pub const fn to_tai(&self, current: Scale) -> Dt {
278 Self::from(self.sec, self.attos, current)
279 }
280
281 /// Converts this instant from one time scale to another.
282 ///
283 /// This is the primary public method for converting between any two supported
284 /// time scales (TAI, UTC, TT, TDB, GPS, TCG, LTC, etc.).
285 ///
286 /// ## Arguments
287 ///
288 /// * `current` — The time scale in which `self` is currently expressed.
289 /// * `new` — The target time scale to convert into.
290 ///
291 /// ## Returns
292 ///
293 /// A [`Dt`] representing the same physical instant on the `new` scale.
294 ///
295 /// If `current == new`, this method returns `*self` without any computation.
296 ///
297 /// ## Notes
298 ///
299 /// - The numerical `sec` and `attos` of `self` are assumed to be on `current`.
300 /// - The returned [`Dt`] contains the correct `sec` and `attos` values for the
301 /// `new` scale (the scale is never stored inside [`Dt`]).
302 /// - This method is `const fn` and performs no heap allocation.
303 ///
304 /// ## See also
305 ///
306 /// * [`Dt::to_tai`](../struct.Dt.html#method.to_tai) — convenience method that always targets TAI.
307 /// * [`Dt::from`](../struct.Dt.html#method.from) — the underlying scale conversion logic.
308 ///
309 /// ## Examples
310 ///
311 /// ```rust
312 /// use deep_time::{Dt, Scale};
313 ///
314 /// let dt_tai = Dt::from_ymdhms(2024, 6, 15, 12, 0, 0, 0);
315 ///
316 /// // Convert from TAI to UTC
317 /// let dt_utc = dt_tai.to(Scale::TAI, Scale::UTC);
318 /// let ymd = dt_utc.to_ymdhms(Scale::UTC);
319 ///
320 /// assert_eq!(ymd.yr(), 2024);
321 /// assert_eq!(ymd.mo(), 6);
322 /// assert_eq!(ymd.day(), 15);
323 /// ```
324 #[inline]
325 pub const fn to(&self, current: Scale, new: Scale) -> Dt {
326 if !current.eq(new) {
327 Self::from(self.sec, self.attos, current).to_internal(new)
328 } else {
329 *self
330 }
331 }
332
333 /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
334 /// or clock-drift correction defined by a [`Drift`] model relative to a reference instant.
335 #[inline]
336 pub const fn convert_using_drift(self, reference: Self, drift: Drift) -> Self {
337 let span = self.to_diff_raw(reference);
338 let correction = drift.time_diff_after(&span);
339 self.add(correction)
340 }
341
342 /// Performs the inverse conversion of [`Dt::convert_using_drift`], recovering the original proper
343 /// time on the source clock scale.
344 ///
345 /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
346 /// case of a pure constant offset the function returns immediately without iteration.
347 pub const fn convert_back_using_drift(self, reference: Self, drift: Drift) -> Self {
348 if drift.rate.is_zero() && drift.accel.is_zero() {
349 return self.sub(drift.constant);
350 }
351 let mut guess = self;
352 let mut i = 0u32;
353 while i < 16 {
354 let span = guess.to_diff_raw(reference);
355 let correction = drift.time_diff_after(&span);
356 guess = self.sub(correction);
357 i += 1;
358 }
359 guess
360 }
361
362 #[inline]
363 pub(crate) const fn tai_to_tcg(tai: Self) -> Self {
364 let tt = tai.add(TT_TAI_OFFSET);
365 Self::tt_to_tcg(tt)
366 }
367
368 #[inline]
369 pub(crate) const fn tai_to_tcb(tai: Self) -> Self {
370 let tdb = Self::tai_to_tdb(tai);
371 Self::tdb_to_tcb(tdb)
372 }
373
374 /// Exact integer helper: elapsed attoseconds since the TCG/TCB reference epoch (1977-01-01.0 TAI),
375 /// using only the numerical `sec`/`attos` of the supplied `Dt` (scale is ignored).
376 #[inline]
377 pub(crate) const fn to_attos_since_tcg_tcb_epoch(numerical: Self) -> i128 {
378 numerical.to_attos() - TCG_TCB_REF_ATTOS_SINCE_J2000
379 }
380
381 /// Exact fixed-point multiplication: `attos * num / den` (handles negative values safely, no overflow for library time range).
382 pub(crate) const fn mul_rate(attos: i128, num: i128, den: i128) -> i128 {
383 if attos == 0 {
384 return 0;
385 }
386 let sign = if attos < 0 { -1i128 } else { 1i128 };
387 let a = if attos < 0 { -attos } else { attos };
388 let q = a / den;
389 let r = a % den;
390 sign * (q * num + (r * num) / den)
391 }
392
393 #[inline]
394 pub(crate) const fn mul_lg(attos: i128) -> i128 {
395 Self::mul_rate(attos, LG_NUM, LG_DEN)
396 }
397
398 #[inline]
399 pub(crate) const fn mul_lb(attos: i128) -> i128 {
400 Self::mul_rate(attos, LB_NUM, LB_DEN)
401 }
402
403 pub(crate) const fn tt_to_tcg(tt: Self) -> Self {
404 let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
405 let span_attos = Self::mul_lg(elapsed);
406 tt.add(Dt::from_attos(span_attos, Scale::TAI))
407 }
408
409 pub(crate) const fn tcg_to_tt(tcg: Self) -> Self {
410 let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcg);
411 let span_attos = Self::mul_rate(elapsed_cg, LG_NUM, LG_DEN + LG_NUM);
412 tcg.sub(Dt::from_attos(span_attos, Scale::TAI))
413 }
414
415 pub(crate) const fn tcb_to_tdb(tcb: Self) -> Self {
416 let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcb);
417 let span_attos = Self::mul_rate(elapsed_cg, LB_NUM, LB_DEN + LB_NUM);
418 tcb.sub(Dt::from_attos(span_attos, Scale::TAI))
419 .sub(Dt::from_attos(TDB0_ATTOS, Scale::TAI))
420 }
421
422 pub(crate) const fn tdb_to_tcb(tdb: Self) -> Self {
423 let elapsed = Self::to_attos_since_tcg_tcb_epoch(tdb);
424 let span_attos = Self::mul_lb(elapsed);
425 tdb.add(Dt::from_attos(span_attos, Scale::TAI))
426 .add(Dt::from_attos(TDB0_ATTOS, Scale::TAI))
427 }
428}