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_ATTOS_AT_1972,
4 TCG_TCB_REF_ATTOS_SINCE_J2000, TDB0_ATTOS, TT_TAI_OFFSET,
5};
6
7impl Dt {
8 /// Converts this instant to the target scale and returns the signed difference
9 /// 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 /// * `to` — The time scale to convert `self` into before computing the difference.
17 /// * `epoch` — The reference epoch (e.g. [`Dt::UNIX_EPOCH`]) from which the
18 /// difference is calculated.
19 ///
20 /// ## Returns
21 ///
22 /// A [`Dt`] representing the signed difference (seconds + attoseconds) between
23 /// this instant (after conversion to `to`) and the provided `epoch`.
24 ///
25 /// The returned value is a signed offset relative to `epoch` in the `to` scale.
26 /// While it is most commonly used as a pure duration, it can also be interpreted
27 /// 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, 12, 0, 0, 0, Scale::UTC);
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(
104 epoch
105 .to(diff.scale)
106 .to_attos()
107 .saturating_add(diff.to_attos()),
108 diff.scale,
109 )
110 } else {
111 Self::from_attos(epoch.to_attos().saturating_add(diff.to_attos()), diff.scale)
112 }
113 }
114
115 /// Converts the internal attos to be on the TAI time [`Scale`].
116 ///
117 /// ```
118 /// use deep_time::{Dt, Scale};
119 ///
120 /// let tai = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::UTC);
121 /// let tt = tai.to(Scale::TT);
122 ///
123 /// assert_eq!(tt.scale, Scale::TT);
124 ///
125 /// let roundtrip = tt.to_tai();
126 ///
127 /// assert_eq!(tai.scale, Scale::TAI);
128 /// assert_eq!(roundtrip, tai);
129 /// ```
130 ///
131 /// See [`Dt::to`](../struct.Dt.html#method.to) for more info.
132 pub const fn to_tai(&self) -> Dt {
133 match self.scale {
134 Scale::UTC => {
135 let raw = Dt::new(self.attos, Scale::TAI, self.target);
136 raw.add_sec(raw.leap_sec(true).offset as i128)
137 }
138 Scale::TAI => *self,
139 Scale::TT => Dt::new(
140 self.attos.saturating_sub(TT_TAI_OFFSET.to_attos()),
141 Scale::TAI,
142 self.target,
143 ),
144 Scale::UTCSpice => {
145 let raw = Dt::new(self.attos, Scale::TAI, self.target);
146 if self.attos < TAI_ATTOS_AT_1972 - 10 {
147 raw.add_sec(9)
148 } else {
149 raw.add_sec(raw.leap_sec(true).offset as i128)
150 }
151 }
152 Scale::UTCSofa => {
153 let raw = Dt::new(self.attos, Scale::TAI, self.target);
154 if let Some(sofa_offset) = historical_sofa_offset_for_non_adjusted(&raw) {
155 raw.add(Dt::from_sec_f(sofa_offset, Scale::TAI))
156 } else {
157 raw.add_sec(raw.leap_sec(true).offset as i128)
158 }
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 to TAI
198 /// 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 => {
203 let offset = self.leap_sec(false).offset;
204 self.add_sec(-offset as i128).with(new)
205 }
206 Scale::TT => self.add(TT_TAI_OFFSET).with(new),
207 Scale::UTCSpice => {
208 if self.to_attos() < TAI_ATTOS_AT_1972 {
209 self.add_sec(-9).with(new)
210 } else {
211 let offset = self.leap_sec(false).offset;
212 self.add_sec(-offset as i128).with(new)
213 }
214 }
215 Scale::UTCSofa => {
216 if let Some(sofa_offset) = historical_sofa_offset_for_non_adjusted(&self) {
217 self.sub(Dt::span_f(sofa_offset)).with(new)
218 } else {
219 let offset = self.leap_sec(false).offset;
220 self.add_sec(-offset as i128).with(new)
221 }
222 }
223 Scale::GPS | Scale::QZSS | Scale::GST => {
224 self.add_attos(-Dt::SEC_19.to_attos()).with(new)
225 }
226 Scale::BDT => self.add_attos(-Dt::SEC_33.to_attos()).with(new),
227 Scale::TDB | Scale::ET => Self::tai_to_tdb(*self).with(new),
228 Scale::TCG => Self::tai_to_tcg(*self).with(new),
229 Scale::TCB => Self::tai_to_tcb(*self).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,
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 /// - It is not necessary to do this if you just want to use such functions
246 /// as [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) as these internally
247 /// convert to the scale of the object's `target` field before output.
248 /// - If a TAI [`Dt`] was created using
249 /// [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd) and the datetime
250 /// had 60 seconds, converting to UTC would lose that info. To round trip a
251 /// 60 second UTC datetime you need only set the
252 /// [`Dt::target`](../struct.Dt.html#method.target) [`Scale`] to `UTC` and
253 /// then call the desired output function, such as
254 /// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd).
255 /// - The internal `attos` field changes to be on the new time scale.
256 /// - The [`Dt`]s `target` field is ignored and left unchanged.
257 /// - The [`Dt`]s `scale` field is changed to the new [`Scale`].
258 ///
259 /// ## Returns
260 ///
261 /// - A [`Dt`] representing the same physical instant but on the `new` scale.
262 /// - The returned objects `scale` field has been changed to `new`.
263 ///
264 /// If `current == new`, this method returns `*self` without any computation.
265 ///
266 /// ## See also
267 ///
268 /// * [`Dt::to_tai`](../struct.Dt.html#method.to_tai)
269 /// * [`Dt::from_attos`](../struct.Dt.html#method.from_attos)
270 ///
271 /// ## Examples
272 ///
273 /// ```rust
274 /// use deep_time::{Dt, Scale};
275 ///
276 /// let tai = Dt::from_ymd(2024, 6, 15, 12, 0, 0, 0, Scale::UTC);
277 /// let tt = tai.to(Scale::TT);
278 /// let tdb = tt.to(Scale::TDB);
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 /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
303 /// or clock-drift correction defined by a [`Drift`] model relative to a reference instant.
304 #[inline]
305 pub const fn convert_using_drift(self, reference: Dt, drift: Drift) -> Dt {
306 let span = self.to_diff_raw(reference);
307 let correction = drift.time_diff_after(&span);
308 self.add(correction)
309 }
310
311 /// Performs the inverse conversion of [`Dt::convert_using_drift`], recovering the original proper
312 /// time on the source clock scale.
313 ///
314 /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
315 /// case of a pure constant offset the function returns immediately without iteration.
316 pub const fn convert_back_using_drift(self, reference: Dt, drift: Drift) -> Dt {
317 if drift.rate.is_zero() && drift.accel.is_zero() {
318 return self.sub(drift.constant);
319 }
320 let mut guess = self;
321 let mut i = 0u32;
322 while i < 16 {
323 let span = guess.to_diff_raw(reference);
324 let correction = drift.time_diff_after(&span);
325 guess = self.sub(correction);
326 i += 1;
327 }
328 guess
329 }
330
331 #[inline]
332 pub(crate) const fn tai_to_tcg(tai: Dt) -> Dt {
333 let tt = tai.add(TT_TAI_OFFSET);
334 Self::tt_to_tcg(tt)
335 }
336
337 #[inline]
338 pub(crate) const fn tai_to_tcb(tai: Dt) -> Dt {
339 let tdb = Self::tai_to_tdb(tai);
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]
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, no overflow for library time range).
351 pub(crate) const fn mul_rate(attos: i128, num: i128, den: i128) -> i128 {
352 if attos == 0 {
353 return 0;
354 }
355 let sign = if attos < 0 { -1i128 } else { 1i128 };
356 let a = if attos < 0 { -attos } else { attos };
357 let q = a / den;
358 let r = a % den;
359 sign * (q * num + (r * num) / den)
360 }
361
362 #[inline]
363 pub(crate) const fn mul_lg(attos: i128) -> i128 {
364 Self::mul_rate(attos, LG_NUM, LG_DEN)
365 }
366
367 #[inline]
368 pub(crate) const fn mul_lb(attos: i128) -> i128 {
369 Self::mul_rate(attos, LB_NUM, LB_DEN)
370 }
371
372 pub(crate) const fn tt_to_tcg(tt: Dt) -> Dt {
373 let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
374 let span_attos = Self::mul_lg(elapsed);
375 tt.add_attos(span_attos)
376 }
377
378 pub(crate) const fn tcg_to_tt(tcg: Dt) -> Dt {
379 let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcg);
380 let span_attos = Self::mul_rate(elapsed_cg, LG_NUM, LG_DEN + LG_NUM);
381 tcg.add_attos(-span_attos)
382 }
383
384 pub(crate) const fn tcb_to_tdb(tcb: Dt) -> Dt {
385 let elapsed_cg = Self::to_attos_since_tcg_tcb_epoch(tcb);
386 let span_attos = Self::mul_rate(elapsed_cg, LB_NUM, LB_DEN + LB_NUM);
387 tcb.add_attos(-span_attos).add_attos(-TDB0_ATTOS)
388 }
389
390 pub(crate) const fn tdb_to_tcb(tdb: Dt) -> Dt {
391 let elapsed = Self::to_attos_since_tcg_tcb_epoch(tdb);
392 let span_attos = Self::mul_lb(elapsed);
393 tdb.add_attos(span_attos).add_attos(TDB0_ATTOS)
394 }
395}