Skip to main content

deep_time/dt/
conveniences.rs

1use crate::{
2    ATTOS_PER_SEC_I128, ATTOS_PER_WEEK, Dt, JD_2000_2_451_545F, Real, SEC_PER_DAYI64, Scale,
3};
4
5impl Dt {
6    /// Returns this [`Dt`] but as time since the
7    /// [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) on its
8    /// `target` time scale.
9    ///
10    /// ## Important:
11    ///
12    /// - The [`Dt`] first converts itself and the epoch to the time scale of its
13    ///   `target` field before doing a raw difference with the epoch.
14    /// - **You may need to change the [`Dt`]'s `target` field** before calling the function
15    ///   if you need the timestamp to be on a particular time scale, e.g. `UTC`.
16    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
17    ///   if it's not then the output will be incorrect.
18    ///
19    /// ## Returns
20    ///
21    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
22    ///   [`UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH).
23    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
24    ///   `Scale::UTC` if you built it with `from_ymd(..., Scale::UTC, ...)`. The result's
25    ///   `scale` and `target` are both set to that same value.
26    ///
27    /// ## Examples
28    ///
29    /// ```rust
30    /// use deep_time::{Dt, Scale};
31    ///
32    /// // because from_ymd() with Scale::UTC sets the returned
33    /// // Dt's target field to Scale::UTC, we do not need to use
34    /// // .target() prior to calling to_unix() in order to get
35    /// // a utc unix timestamp
36    /// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
37    /// let unix = dt.to_unix();
38    ///
39    /// assert_eq!(
40    ///     unix.to_sec(),
41    ///     946728000,
42    ///     "unix sec for 2000-01-01 12:00:00 UTC is wrong, got: {}, expected: 946728000",
43    ///     unix.to_sec()
44    /// );
45    ///
46    /// let dt2 = Dt::from_unix(unix);
47    ///
48    /// assert_eq!(
49    ///     dt.to_attos(), dt2.to_attos(),
50    ///     "round trip to Dt got wrong attos, old: {}, new: {}",
51    ///     dt.to_attos(), dt2.to_attos()
52    /// );
53    ///
54    /// let ymd = dt2.to_ymd();
55    /// assert_eq!(ymd.yr(), 2000_i64);
56    /// assert_eq!(ymd.mo(), 1);
57    /// assert_eq!(ymd.day(), 1);
58    /// assert_eq!(ymd.hr(), 12);
59    /// assert_eq!(ymd.min(), 0);
60    /// assert_eq!(ymd.sec(), 0);
61    /// assert_eq!(ymd.attos(), 0);
62    /// ```
63    ///
64    /// ## See also
65    ///
66    /// - [`Dt::from_unix`](../struct.Dt.html#method.from_unix)
67    #[inline(always)]
68    pub const fn to_unix(&self) -> Dt {
69        self.to_scale_and_diff(Self::UNIX_EPOCH, true)
70    }
71
72    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
73    /// [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH).
74    ///
75    /// This is the inverse of [`Dt::to_unix`](../struct.Dt.html#method.to_unix).
76    ///
77    /// ## Important:
78    ///
79    /// - `unix` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
80    ///   [`UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) — typically the
81    ///   return value of [`Dt::to_unix`](../struct.Dt.html#method.to_unix).
82    ///   The input's `scale` field says which time scale that count is on — if it
83    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
84    ///   included).
85    /// - [`Dt::UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) is converted
86    ///   to that same scale before the sum.
87    ///
88    /// ## Returns
89    ///
90    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
91    /// [`UNIX_EPOCH`](../struct.Dt.html#associatedconstant.UNIX_EPOCH) — it is attoseconds since
92    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `unix`.
93    ///
94    /// ## Examples
95    ///
96    /// ```rust
97    /// use deep_time::{Dt, Scale};
98    ///
99    /// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
100    /// let unix = dt.to_unix();
101    /// let roundtrip = Dt::from_unix(unix);
102    ///
103    /// assert_eq!(roundtrip, dt);
104    /// ```
105    ///
106    /// ### From an external POSIX unix seconds count
107    ///
108    /// ```rust
109    /// use deep_time::{Dt, Scale};
110    ///
111    /// // 2012-08-08 15:30:00 → 1344439800.000000 s
112    /// let unix = 1344439800_i128;
113    ///
114    /// // use Dt::new to avoid time scale conversions on the
115    /// // seconds count, other functions can do the same thing
116    /// // but this way lets us easily set the time scale fields
117    /// // in one go
118    /// let unix_dt = Dt::new_sec(unix, Scale::UTC, Scale::UTC);
119    ///
120    /// let dt = Dt::from_unix(unix_dt);
121    ///
122    /// let ymd = dt.to_ymd();
123    /// assert_eq!(ymd.yr(), 2012);
124    /// assert_eq!(ymd.mo(), 8);
125    /// assert_eq!(ymd.day(), 8);
126    /// assert_eq!(ymd.hr(), 15);
127    /// assert_eq!(ymd.min(), 30);
128    /// assert_eq!(ymd.sec(), 0);
129    /// assert_eq!(ymd.attos(), 0);
130    /// ```
131    ///
132    /// ## See also
133    ///
134    /// - [`Dt::to_unix`](../struct.Dt.html#method.to_unix)
135    #[inline(always)]
136    pub const fn from_unix(unix: Dt) -> Dt {
137        Self::from_diff_and_scale(unix, Dt::UNIX_EPOCH, true)
138    }
139
140    /// Returns this [`Dt`] but as time since the
141    /// [`Dt::NTP_EPOCH`](../struct.Dt.html#associatedconstant.NTP_EPOCH) on its
142    /// `target` time scale.
143    ///
144    /// ## Important:
145    ///
146    /// - The [`Dt`] first converts itself and the epoch to the time scale of its
147    ///   `target` field before doing a raw difference with the epoch.
148    /// - **You may need to change the [`Dt`]'s `target` field** before calling the function
149    ///   if you need the timestamp to be on a particular time scale, e.g. `UTC`.
150    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
151    ///   if it's not then the output will be incorrect.
152    ///
153    /// ## Returns
154    ///
155    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
156    ///   [`NTP_EPOCH`](../struct.Dt.html#associatedconstant.NTP_EPOCH).
157    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
158    ///   `Scale::UTC` if you built it with `from_ymd(..., Scale::UTC, ...)`. The result's
159    ///   `scale` and `target` are both set to that same value.
160    ///
161    /// ## Examples
162    ///
163    /// ```rust
164    /// use deep_time::{Dt, Scale};
165    ///
166    /// // 2698012800
167    /// let dt = Dt::from_ymd(1985, 7, 1, Scale::TAI, 0, 0, 0, 0);
168    /// let ntp = dt.to_ntp();
169    ///
170    /// assert_eq!(
171    ///     ntp.to_attos(), Dt::sec_to_attos(2698012800_i128),
172    ///     "ntp sec for 1985 is wrong, got: {}, expected: {}",
173    ///     ntp.to_attos(), Dt::sec_to_attos(2698012800_i128)
174    /// );
175    ///
176    /// let dt2 = Dt::from_ntp(ntp);
177    ///
178    /// assert_eq!(
179    ///     dt.to_attos(), dt2.to_attos(),
180    ///     "round trip to Dt got wrong sec, old: {}, new: {}",
181    ///     dt.to_attos(), dt2.to_attos()
182    /// );
183    ///
184    /// let ymd = dt2.to_ymd();
185    /// assert_eq!(ymd.yr(), 1985_i64);
186    /// assert_eq!(ymd.mo(), 7);
187    /// assert_eq!(ymd.day(), 1);
188    /// assert_eq!(ymd.hr(), 0);
189    /// assert_eq!(ymd.min(), 0);
190    /// assert_eq!(ymd.sec(), 0);
191    /// assert_eq!(ymd.attos(), 0);
192    /// ```
193    ///
194    /// ## See also
195    ///
196    /// - [`Dt::from_ntp`](../struct.Dt.html#method.from_ntp)
197    #[inline(always)]
198    pub const fn to_ntp(&self) -> Dt {
199        self.to_scale_and_diff(Self::NTP_EPOCH, true)
200    }
201
202    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
203    /// [`Dt::NTP_EPOCH`](../struct.Dt.html#associatedconstant.NTP_EPOCH).
204    ///
205    /// This is the inverse of [`Dt::to_ntp`](../struct.Dt.html#method.to_ntp).
206    ///
207    /// ## Important:
208    ///
209    /// - `ntp` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
210    ///   [`NTP_EPOCH`](../struct.Dt.html#associatedconstant.NTP_EPOCH) — typically the
211    ///   return value of [`Dt::to_ntp`](../struct.Dt.html#method.to_ntp)
212    ///   The input's `scale` field says which time scale that count is on — if it
213    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
214    ///   included).
215    /// - [`Dt::NTP_EPOCH`](../struct.Dt.html#associatedconstant.NTP_EPOCH) is converted
216    ///   to that same scale before the sum.
217    ///
218    /// ## Returns
219    ///
220    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
221    /// [`NTP_EPOCH`](../struct.Dt.html#associatedconstant.NTP_EPOCH) — it is attoseconds since
222    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `ntp`.
223    ///
224    /// ## Examples
225    ///
226    /// ```rust
227    /// use deep_time::{Dt, Scale};
228    ///
229    /// let dt = Dt::from_ymd(1985, 7, 1, Scale::TAI, 0, 0, 0, 0);
230    /// let ntp = dt.to_ntp();
231    /// let roundtrip = Dt::from_ntp(ntp);
232    ///
233    /// assert_eq!(roundtrip, dt);
234    /// ```
235    ///
236    /// ## See also
237    ///
238    /// - [`Dt::to_ntp`](../struct.Dt.html#method.to_ntp)
239    #[inline(always)]
240    pub const fn from_ntp(ntp: Dt) -> Dt {
241        Self::from_diff_and_scale(ntp, Self::NTP_EPOCH, true)
242    }
243
244    /// Returns this [`Dt`] but as time since the
245    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) on its
246    /// `target` time scale.
247    ///
248    /// ## Important:
249    ///
250    /// - The [`Dt`] first converts itself and the epoch to the time scale of its
251    ///   `target` field before doing a raw difference with the epoch.
252    /// - **You may need to change the [`Dt`]'s `target` field** before calling the function
253    ///   if you need the timestamp to be on a particular time scale, e.g.
254    ///   `.target(Scale::GPS)`.
255    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
256    ///   if it's not then the output will be incorrect.
257    ///
258    /// ## Returns
259    ///
260    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
261    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
262    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
263    ///   `Scale::GPS` after `.target(Scale::GPS)`. The result's `scale` and `target` are both
264    ///   set to that same value.
265    ///
266    /// ## See also
267    ///
268    /// - [`Dt::from_gps`](../struct.Dt.html#method.from_gps)
269    /// - [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd)
270    /// - [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
271    ///
272    /// ## Implementation
273    ///
274    /// `convert_epoch` is `true`. If we did not convert the epoch, we would not get seconds
275    /// since the GPS epoch; we would get seconds since something else.
276    ///
277    /// [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd) / [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
278    /// do the opposite: if they converted the epoch too, the difference would cancel out. See
279    /// [`to_ymd`](../struct.Dt.html#method.to_ymd).
280    #[inline(always)]
281    pub const fn to_gps(&self) -> Dt {
282        self.to_scale_and_diff(Self::GPS_EPOCH, true)
283    }
284
285    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
286    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
287    ///
288    /// This is the inverse of [`Dt::to_gps`](../struct.Dt.html#method.to_gps).
289    ///
290    /// ## Important:
291    ///
292    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
293    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
294    ///   return value of [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
295    ///   The input's `scale` field says which time scale that count is on — if it
296    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
297    ///   included).
298    /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
299    ///   to that same scale before the sum.
300    ///
301    /// ## Returns
302    ///
303    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
304    /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
305    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
306    ///
307    /// ## Examples
308    ///
309    /// ```rust
310    /// use deep_time::{Dt, Scale};
311    ///
312    /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
313    /// let gps = x.target(Scale::GPS).to_gps();
314    /// let roundtrip = Dt::from_gps(gps);
315    ///
316    /// assert_eq!(roundtrip, x);
317    /// ```
318    ///
319    /// ## See also
320    ///
321    /// - [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
322    /// - [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow)
323    #[inline(always)]
324    pub const fn from_gps(elapsed: Dt) -> Dt {
325        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
326    }
327
328    /// Returns the GPS week number and Time of Week (TOW) for this instant.
329    ///
330    /// Elapsed time since [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
331    /// is computed by [`Dt::to_gps`](../struct.Dt.html#method.to_gps) — on this [`Dt`]'s
332    /// `target` time scale — and then split into whole weeks plus a remainder.
333    ///
334    /// This is the inverse of
335    /// [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow).
336    ///
337    /// ## Important:
338    ///
339    /// - Uses [`Dt::to_gps`](../struct.Dt.html#method.to_gps) internally: this [`Dt`] and
340    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) are both converted
341    ///   to the `target` time scale before differencing.
342    /// - **You may need to change the [`Dt`]'s `target` field** before calling if you need
343    ///   week/TOW on a particular time scale, e.g. `Scale::GPS`.
344    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
345    ///   if it's not then the output will be incorrect.
346    ///
347    /// ## Returns
348    ///
349    /// A `(week, tow)` pair:
350    ///
351    /// - `week` (`i64`): whole weeks in the elapsed time from
352    ///   [`Dt::to_gps`](../struct.Dt.html#method.to_gps). Week 0 starts at the GPS epoch
353    ///   (1980-01-06). Before that date the elapsed time is negative and `div_euclid` yields a
354    ///   negative week — this is not a broadcast GPS week number, just how the split is defined.
355    ///   A plain integer is enough here; it is only a week count, not a duration in attoseconds.
356    /// - `tow` ([`Dt`]): seconds-within-the-week as attoseconds in `0 .. 604800`. Its `scale` and
357    ///   `target` are set to this [`Dt`]'s `target` so
358    ///   [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow) knows which
359    ///   time scale the pair belongs to. `tow` is a [`Dt`] rather than a bare integer so
360    ///   sub-second precision and scale are preserved together; the week number alone cannot
361    ///   carry either. `div_euclid` / `rem_euclid` are used (not truncating `/`) so TOW stays
362    ///   non-negative even when the elapsed time is negative.
363    ///
364    /// ## Examples
365    ///
366    /// ```rust
367    /// use deep_time::{Dt, Scale};
368    ///
369    /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
370    /// let g = x.to_gps_wk_and_tow();
371    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
372    /// assert_eq!(x, z);
373    ///
374    /// // for conventional GPS-time week/TOW, set target first:
375    /// let g = x.target(Scale::GPS).to_gps_wk_and_tow();
376    /// ```
377    ///
378    /// ## See also
379    ///
380    /// - [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow)
381    /// - [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
382    pub const fn to_gps_wk_and_tow(&self) -> (i64, Dt) {
383        let total_attos = self.to_gps().to_attos();
384        let wk = total_attos.div_euclid(ATTOS_PER_WEEK) as i64;
385        let tow_attos = total_attos.rem_euclid(ATTOS_PER_WEEK);
386        // was converted to target scale, scale is now target
387        (wk, Dt::new(tow_attos, self.target, self.target))
388    }
389
390    /// Creates a [`Dt`] from a GPS week number and Time of Week (TOW).
391    ///
392    /// Recombines `week` and `tow` into elapsed time since
393    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH), then passes that to
394    /// [`Dt::from_gps`](../struct.Dt.html#method.from_gps).
395    ///
396    /// This is the inverse of
397    /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow).
398    ///
399    /// ## Important:
400    ///
401    /// - Uses [`Dt::from_gps`](../struct.Dt.html#method.from_gps) internally: the elapsed time
402    ///   is interpreted on the `tow` [`Dt`]'s `scale` / `target` fields, and
403    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted to that
404    ///   same scale before the sum.
405    /// - Pass back the `tow` from [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
406    ///   unchanged if you want a round trip.
407    ///
408    /// ## Returns
409    ///
410    /// A **TAI** [`Dt`] for the reconstructed instant. Its `target` field is taken from `tow`.
411    ///
412    /// `tow` must be a [`Dt`] (not a bare second count) because
413    /// [`Dt::from_gps`](../struct.Dt.html#method.from_gps) needs both the within-week attoseconds
414    /// and the `scale` / `target` that say which time scale `week` and `tow` were expressed on.
415    /// The week number is multiplied back into attoseconds (`week * 604800` seconds); only `tow`
416    /// carries the scale and sub-week precision needed for the round trip.
417    ///
418    /// `tow` should be in `0 .. 604800` seconds, as returned by
419    /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow). Negative `week`
420    /// values only arise from dates before 1980-01-06 (see that function).
421    ///
422    /// ## Examples
423    ///
424    /// ```rust
425    /// use deep_time::{Dt, Scale};
426    ///
427    /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
428    /// let g = x.to_gps_wk_and_tow();
429    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
430    /// assert_eq!(x, z);
431    /// ```
432    ///
433    /// ## See also
434    ///
435    /// - [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
436    /// - [`Dt::from_gps`](../struct.Dt.html#method.from_gps)
437    pub const fn from_gps_wk_and_tow(wk: i64, tow: Dt) -> Dt {
438        let total_attos = (wk as i128)
439            .saturating_mul(ATTOS_PER_WEEK)
440            .saturating_add(tow.to_attos());
441
442        Self::from_gps(Dt::new(total_attos, tow.scale, tow.target))
443    }
444
445    /// Returns the day of the GPS week (0 = Sunday, 1 = Monday, …, 6 = Saturday).
446    ///
447    /// This value is computed directly from the GPS Time of Week and is
448    /// independent of the Gregorian calendar or civil time.
449    pub const fn to_gps_day_of_wk(&self) -> u8 {
450        let (_, tow) = self.to_gps_wk_and_tow();
451        let secs = tow.to_attos() / ATTOS_PER_SEC_I128;
452
453        (secs / SEC_PER_DAYI64 as i128) as u8
454    }
455
456    /// Returns this [`Dt`] but as time since the
457    /// [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) on its
458    /// `target` time scale.
459    ///
460    /// ## Important:
461    ///
462    /// - The [`Dt`] first converts itself and the epoch to the time scale of its
463    ///   `target` field before doing a raw difference with the epoch.
464    /// - **You may need to change the [`Dt`]'s `target` field** before calling the function
465    ///   if you need the timestamp to be on a particular time scale, e.g. `UTC`.
466    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
467    ///   if it's not then the output will be incorrect.
468    ///
469    /// ## Returns
470    ///
471    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
472    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
473    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
474    ///   `Scale::TT` after `.target(Scale::TT)`. The result's `scale` and `target` are both
475    ///   set to that same value.
476    ///
477    /// ## Examples
478    ///
479    /// ```rust
480    /// use deep_time::{Dt, Scale};
481    ///
482    /// let cxc = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
483    ///     .target(Scale::TT)
484    ///     .to_cxcsec()
485    ///     .to_sec_f();
486    ///
487    /// // cxcsec 694224032.184 (matches Astropy)
488    /// assert_eq!(cxc, 694224032.184);
489    /// ```
490    ///
491    /// ## See also
492    ///
493    /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
494    #[inline(always)]
495    pub const fn to_cxcsec(&self) -> Dt {
496        self.to_scale_and_diff(Self::CXC_EPOCH, true)
497    }
498
499    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
500    /// [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
501    ///
502    /// This is the inverse of [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec).
503    ///
504    /// ## Important:
505    ///
506    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
507    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) — typically the
508    ///   return value of [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
509    ///   The input's `scale` field says which time scale that count is on — if it
510    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
511    ///   included).
512    /// - [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) is converted
513    ///   to that same scale before the sum.
514    ///
515    /// ## Returns
516    ///
517    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
518    /// [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) — it is attoseconds since
519    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
520    ///
521    /// ## Examples
522    ///
523    /// ```rust
524    /// use deep_time::{Dt, Scale};
525    ///
526    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
527    /// let cxc = x.target(Scale::TT).to_cxcsec();
528    /// let roundtrip = Dt::from_cxcsec(cxc);
529    ///
530    /// assert_eq!(roundtrip, x);
531    /// ```
532    ///
533    /// ## See also
534    ///
535    /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
536    /// - [`Dt::from_cxcsec_f`](../struct.Dt.html#method.from_cxcsec_f)
537    #[inline(always)]
538    pub const fn from_cxcsec(elapsed: Dt) -> Dt {
539        Self::from_diff_and_scale(elapsed, Self::CXC_EPOCH, true)
540    }
541
542    /// Convenience wrapper around
543    /// [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
544    /// for a bare floating-point second count.
545    ///
546    /// ## Parameters
547    ///
548    /// - `sec` — seconds elapsed since
549    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
550    /// - `on` — which [`Scale`] the count is measured in (for example `Scale::TT` or
551    ///   `Scale::UTC`). This becomes the wrapped [`Dt`]'s `scale`;
552    ///   [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
553    ///   then uses it when turning the elapsed count into an absolute TAI instant
554    ///   (including leap-second handling where applicable). Same role as the `scale`
555    ///   field on the [`Dt`] you would hand to
556    ///   [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
557    ///   directly.
558    ///
559    /// ## Examples
560    ///
561    /// ```rust
562    /// use deep_time::{Dt, Scale};
563    ///
564    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
565    /// let cxc = x.target(Scale::TT).to_cxcsec().to_sec_f();
566    /// let roundtrip = Dt::from_cxcsec_f(cxc, Scale::TT);
567    ///
568    /// assert_eq!(roundtrip.to_cxcsec().to_sec_f(), cxc);
569    /// ```
570    ///
571    /// ## See also
572    ///
573    /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
574    /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
575    #[inline(always)]
576    pub const fn from_cxcsec_f(sec: Real, on: Scale) -> Dt {
577        Self::from_cxcsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
578    }
579
580    /// Returns the elapsed time since the GALEX epoch as a [`Dt`] expressed
581    /// in this object's current `target` scale.
582    ///
583    /// This method can match Astropy’s `Time.galexsec` format. To match
584    /// Astropy output, set `.target(Scale::UTC)`
585    /// before calling.
586    ///
587    /// The GALEX epoch is
588    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
589    /// (same epoch used by GPS time).
590    ///
591    /// ## Important:
592    ///
593    /// - The [`Dt`] first converts itself and the [`Dt::GPS_EPOCH`] to the time
594    ///   scale of its `target` field before doing a raw difference with the epoch.
595    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
596    ///   epoch, if it's not then the output will be incorrect.
597    ///
598    /// ## Returns
599    ///
600    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
601    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
602    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
603    ///   `Scale::UTC` after `.target(Scale::UTC)`. The result's `scale` and `target` are both
604    ///   set to that same value.
605    ///
606    /// ## Examples
607    ///
608    /// ```rust
609    /// use deep_time::{Dt, Scale};
610    ///
611    /// let galexsec = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
612    ///     .target(Scale::UTC)
613    ///     .to_galexsec()
614    ///     .to_sec_f();
615    ///
616    /// assert_eq!(galexsec, 1261871963.0);
617    /// ```
618    ///
619    /// ## See also
620    ///
621    /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
622    #[inline(always)]
623    pub const fn to_galexsec(&self) -> Dt {
624        self.to_scale_and_diff(Self::GPS_EPOCH, true)
625    }
626
627    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
628    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
629    ///
630    /// This is the inverse of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec).
631    /// GALEX seconds use the same epoch as GPS time.
632    ///
633    /// ## Important:
634    ///
635    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
636    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
637    ///   return value of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
638    ///   The input's `scale` field says which time scale that count is on — if it
639    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
640    ///   included).
641    /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
642    ///   to that same scale before the sum.
643    ///
644    /// ## Returns
645    ///
646    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
647    /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
648    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
649    ///
650    /// ## Examples
651    ///
652    /// ```rust
653    /// use deep_time::{Dt, Scale};
654    ///
655    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
656    /// let galex = x.target(Scale::UTC).to_galexsec();
657    /// let roundtrip = Dt::from_galexsec(galex);
658    ///
659    /// assert_eq!(roundtrip, x);
660    /// ```
661    ///
662    /// ## See also
663    ///
664    /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
665    /// - [`Dt::from_galexsec_f`](../struct.Dt.html#method.from_galexsec_f)
666    #[inline(always)]
667    pub const fn from_galexsec(elapsed: Dt) -> Dt {
668        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
669    }
670
671    /// Convenience wrapper around
672    /// [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
673    /// for a bare floating-point second count.
674    ///
675    /// ## Parameters
676    ///
677    /// - `sec` — seconds elapsed since
678    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
679    /// - `on` — which [`Scale`] the count is measured in (for example `Scale::UTC` or
680    ///   `Scale::TT`). This becomes the wrapped [`Dt`]'s `scale`;
681    ///   [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
682    ///   then uses it when turning the elapsed count into an absolute TAI instant
683    ///   (including leap-second handling where applicable). Same role as the `scale`
684    ///   field on the [`Dt`] you would hand to
685    ///   [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec) directly.
686    ///
687    /// ## Examples
688    ///
689    /// ```rust
690    /// use deep_time::{Dt, Scale};
691    ///
692    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
693    /// let galex = x.target(Scale::UTC).to_galexsec().to_sec_f();
694    /// let roundtrip = Dt::from_galexsec_f(galex, Scale::UTC);
695    ///
696    /// assert_eq!(roundtrip, x);
697    /// ```
698    ///
699    /// ## See also
700    ///
701    /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
702    /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
703    #[inline(always)]
704    pub const fn from_galexsec_f(sec: Real, on: Scale) -> Dt {
705        Self::from_galexsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
706    }
707
708    /// Returns the **Julian epoch year** (JYEAR) for this instant.
709    ///
710    /// Julian years are defined as exactly 365.25 days of 86400 SI seconds.
711    /// This is the system used for J2000.0 and many astronomical calculations.
712    ///
713    /// This is **not** the same as
714    /// [`Dt::to_decimalyear`](../struct.Dt.html#method.to_decimalyear),
715    /// which uses the actual length of the specific Gregorian year.
716    ///
717    /// This is the inverse of
718    /// [`Dt::from_jyear`](../struct.Dt.html#method.from_jyear).
719    ///
720    /// ## Important:
721    ///
722    /// - The [`Dt`] first converts itself to the time scale of its `target` field
723    ///   before producing a result.
724    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
725    ///   epoch, if it's not then the output will be incorrect.
726    ///
727    /// ## Examples
728    ///
729    /// ```rust
730    /// use deep_time::{Dt, Scale};
731    ///
732    /// let x = Dt::from_ymd(2020, 1, 1, Scale::UTC, 0, 0, 0, 0);
733    ///
734    /// assert_eq!(x.to_jyear(), 2019.9986310746065);
735    /// ```
736    #[inline(always)]
737    pub const fn to_jyear(&self) -> Real {
738        let jd_tt = self.to_jd_f();
739        f!(2000.0) + (jd_tt - JD_2000_2_451_545F) / f!(365.25)
740    }
741
742    /// Inverse of
743    /// [`Dt::to_jyear`](../struct.Dt.html#method.to_jyear).
744    pub const fn from_jyear(jyear: Real, scale: Scale) -> Dt {
745        if jyear.is_nan() {
746            return Self::ZERO;
747        }
748        if jyear.is_infinite() {
749            return if jyear.is_sign_positive() {
750                Self::MAX
751            } else {
752                Self::MIN
753            };
754        }
755
756        let jd = JD_2000_2_451_545F + (jyear - f!(2000.0)) * f!(365.25);
757        Self::from_jd_f(jd, scale)
758    }
759
760    /// Returns the **Besselian epoch year** (BYEAR) for this instant.
761    ///
762    /// Besselian years are an older astronomical convention based on a
763    /// tropical year length of approximately 365.242198781 days.
764    ///
765    /// This is the inverse of
766    /// [`Dt::from_byear`](../struct.Dt.html#method.from_byear).
767    ///
768    /// ## Important:
769    ///
770    /// - The [`Dt`] first converts itself to the time scale of its `target` field
771    ///   before producing a result.
772    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
773    ///   epoch, if it's not then the output will be incorrect.
774    ///
775    /// ## Examples
776    ///
777    /// ```rust
778    /// use deep_time::{Dt, Scale};
779    ///
780    /// let x = Dt::from_ymd(2020, 1, 1, Scale::UTC, 0, 0, 0, 0);
781    ///
782    /// assert!((x.to_byear() - 2020.000335739628).abs() < 1e-12);
783    /// ```
784    #[inline]
785    pub const fn to_byear(&self) -> Real {
786        let jd_tt = self.to_jd_f();
787        f!(1900.0) + (jd_tt - f!(2415020.31352)) / f!(365.242198781)
788    }
789
790    /// Inverse of
791    /// [`Dt::to_byear`](../struct.Dt.html#method.to_byear).
792    pub const fn from_byear(byear: Real, scale: Scale) -> Dt {
793        if byear.is_nan() {
794            return Self::ZERO;
795        }
796        if byear.is_infinite() {
797            return if byear.is_sign_positive() {
798                Self::MAX
799            } else {
800                Self::MIN
801            };
802        }
803
804        let jd = f!(2415020.31352) + (byear - f!(1900.0)) * f!(365.242198781);
805        Self::from_jd_f(jd, scale)
806    }
807
808    /// Returns the **decimal year** (Gregorian calendar year + fraction of the year).
809    ///
810    /// This is the direct equivalent of Astropy’s `Time.decimalyear`:
811    /// - Uses the *actual* length of the specific Gregorian year (365 or 366 days,
812    ///   plus any leap seconds on UTC/UtcSpice/etc.).
813    /// - Scale-aware (TAI, TT, UTC, TDB, etc.), converts to this [`Dt`]'s target time
814    ///   scale before producing an output.
815    /// - Exact integer arithmetic for the year boundaries, then a high-precision
816    ///   `to_sec_f` division (lossy only at the final `Real` step, same as Astropy).
817    ///
818    /// ## Important:
819    ///
820    /// - The [`Dt`] first converts itself to the time scale of its `target` field
821    ///   before producing a result.
822    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
823    ///   epoch, if it's not then the output will be incorrect.
824    ///
825    /// ## Examples
826    ///
827    /// ```rust
828    /// use deep_time::{Dt, Scale};
829    ///
830    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
831    /// assert_eq!(x.to_decimalyear(), 2020.0);
832    ///
833    /// // Also works for negative years
834    /// let y = Dt::from_ymd(-2000, 1, 1, Scale::TAI, 0, 0, 0, 0);
835    /// assert_eq!(y.to_decimalyear(), -2000.0);
836    /// ```
837    pub fn to_decimalyear(&self) -> Real {
838        let ymd = self.to_ymd();
839        let year = ymd.yr;
840
841        let start = Self::from_ymd(year, 1, 1, self.target, 0, 0, 0, 0);
842        let next_start = Self::from_ymd(year + 1, 1, 1, self.target, 0, 0, 0, 0);
843
844        let elapsed = self.to_diff_raw(start).to_sec_f();
845        let year_length = next_start.to_diff_raw(start).to_sec_f();
846
847        // year_length is never zero for representable years
848        f!(year) + elapsed / year_length
849    }
850}