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}