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