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}