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