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 [`Self::from_cxcsec`] for a bare floating-point
543    /// second count.
544    ///
545    /// Wraps `sec` in a [`Dt`] via [`Dt::sec_f_to_attos`] and
546    /// [`Dt::new`], then passes it to [`Self::from_cxcsec`]. Unlike [`Dt::from_sec_f`],
547    /// this does not convert the count to TAI up front — [`Self::from_cxcsec`] performs
548    /// that conversion once, from `on`.
549    ///
550    /// ## Parameters
551    ///
552    /// - `sec` — seconds elapsed since
553    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
554    /// - `on` — which [`Scale`] the count is measured in (for example `Scale::TT` or
555    ///   `Scale::UTC`). This becomes the wrapped [`Dt`]'s `scale`; [`Self::from_cxcsec`]
556    ///   then uses it when turning the elapsed count into an absolute TAI instant
557    ///   (including leap-second handling where applicable). Same role as the `scale`
558    ///   field on the [`Dt`] you would hand to [`Self::from_cxcsec`] directly.
559    ///
560    /// ## Examples
561    ///
562    /// ```rust
563    /// use deep_time::{Dt, Scale};
564    ///
565    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
566    /// let cxc = x.target(Scale::TT).to_cxcsec().to_sec_f();
567    /// let roundtrip = Dt::from_cxcsec_f(cxc, Scale::TT);
568    ///
569    /// assert_eq!(roundtrip.to_cxcsec().to_sec_f(), cxc);
570    /// ```
571    ///
572    /// ## See also
573    ///
574    /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
575    /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
576    #[inline(always)]
577    pub const fn from_cxcsec_f(sec: Real, on: Scale) -> Dt {
578        Self::from_cxcsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
579    }
580
581    /// Returns the elapsed time since the GALEX epoch as a [`Dt`] expressed
582    /// in this object's current `target` scale.
583    ///
584    /// This method can match Astropy’s `Time.galexsec` format. To match
585    /// Astropy output, set `.target(Scale::UTC)` (or the appropriate scale)
586    /// before calling.
587    ///
588    /// The GALEX epoch is [`Self::GPS_EPOCH`] (same epoch used by GPS time).
589    ///
590    /// ## Important:
591    ///
592    /// - The [`Dt`] first converts itself and the [`Dt::GPS_EPOCH`] to the time
593    ///   scale of its `target` field before doing a raw difference with the epoch.
594    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
595    ///   epoch, if it's not then the output will be incorrect.
596    ///
597    /// ## Returns
598    ///
599    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
600    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
601    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
602    ///   `Scale::UTC` after `.target(Scale::UTC)`. The result's `scale` and `target` are both
603    ///   set to that same value.
604    ///
605    /// ## Examples
606    ///
607    /// ```rust
608    /// use deep_time::{Dt, Scale};
609    ///
610    /// let galexsec = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
611    ///     .target(Scale::UTC)
612    ///     .to_galexsec()
613    ///     .to_sec_f();
614    ///
615    /// assert_eq!(galexsec, 1261871963.0);
616    /// ```
617    ///
618    /// ## See also
619    ///
620    /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
621    #[inline(always)]
622    pub const fn to_galexsec(&self) -> Dt {
623        self.to_scale_and_diff(Self::GPS_EPOCH, true)
624    }
625
626    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
627    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
628    ///
629    /// This is the inverse of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec).
630    /// GALEX seconds use the same epoch as GPS time.
631    ///
632    /// ## Important:
633    ///
634    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
635    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
636    ///   return value of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
637    ///   The input's `scale` field says which time scale that count is on — if it
638    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
639    ///   included).
640    /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
641    ///   to that same scale before the sum.
642    ///
643    /// ## Returns
644    ///
645    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
646    /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
647    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
648    ///
649    /// ## Examples
650    ///
651    /// ```rust
652    /// use deep_time::{Dt, Scale};
653    ///
654    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
655    /// let galex = x.target(Scale::UTC).to_galexsec();
656    /// let roundtrip = Dt::from_galexsec(galex);
657    ///
658    /// assert_eq!(roundtrip, x);
659    /// ```
660    ///
661    /// ## See also
662    ///
663    /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
664    /// - [`Dt::from_galexsec_f`](../struct.Dt.html#method.from_galexsec_f)
665    #[inline(always)]
666    pub const fn from_galexsec(elapsed: Dt) -> Dt {
667        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
668    }
669
670    /// Convenience wrapper around [`Self::from_galexsec`] for a bare floating-point
671    /// second count.
672    ///
673    /// Wraps `sec` in a [`Dt`] via [`Dt::sec_f_to_attos`] and
674    /// [`Dt::new`], then passes it to [`Self::from_galexsec`]. Unlike [`Dt::from_sec_f`],
675    /// this does not convert the count to TAI up front — [`Self::from_galexsec`] performs
676    /// that conversion once, from `on`.
677    ///
678    /// ## Parameters
679    ///
680    /// - `sec` — seconds elapsed since
681    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
682    /// - `on` — which [`Scale`] the count is measured in (for example `Scale::UTC` or
683    ///   `Scale::TT`). This becomes the wrapped [`Dt`]'s `scale`; [`Self::from_galexsec`]
684    ///   then uses it when turning the elapsed count into an absolute TAI instant
685    ///   (including leap-second handling where applicable). Same role as the `scale`
686    ///   field on the [`Dt`] you would hand to [`Self::from_galexsec`] directly.
687    ///
688    /// ## Examples
689    ///
690    /// ```rust
691    /// use deep_time::{Dt, Scale};
692    ///
693    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
694    /// let galex = x.target(Scale::UTC).to_galexsec().to_sec_f();
695    /// let roundtrip = Dt::from_galexsec_f(galex, Scale::UTC);
696    ///
697    /// assert_eq!(roundtrip, x);
698    /// ```
699    ///
700    /// ## See also
701    ///
702    /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
703    /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
704    #[inline(always)]
705    pub const fn from_galexsec_f(sec: Real, on: Scale) -> Dt {
706        Self::from_galexsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
707    }
708
709    /// Returns the **Julian epoch year** (JYEAR) for this instant.
710    ///
711    /// Julian years are defined as exactly 365.25 days of 86400 SI seconds.
712    /// This is the system used for J2000.0 and many astronomical calculations.
713    ///
714    /// This is **not** the same as [`Self::to_decimalyear`], which uses the
715    /// actual length of the specific Gregorian year.
716    ///
717    /// This is the inverse of [`Self::from_jyear`].
718    ///
719    /// ## Examples
720    ///
721    /// ```rust
722    /// use deep_time::{Dt, Scale};
723    ///
724    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
725    /// assert_eq!(x.to_jyear(), 2019.9986310746065);
726    /// ```
727    #[inline(always)]
728    pub const fn to_jyear(&self) -> Real {
729        let jd_tt = self.to_jd_f();
730        f!(2000.0) + (jd_tt - JD_2000_2_451_545F) / f!(365.25)
731    }
732
733    /// Inverse of [`Self::to_jyear`].
734    pub const fn from_jyear(jyear: Real, scale: Scale) -> Dt {
735        if jyear.is_nan() {
736            return Self::ZERO;
737        }
738        if jyear.is_infinite() {
739            return if jyear.is_sign_positive() {
740                Self::MAX
741            } else {
742                Self::MIN
743            };
744        }
745
746        let jd = JD_2000_2_451_545F + (jyear - f!(2000.0)) * f!(365.25);
747        Self::from_jd_f(jd, scale)
748    }
749
750    /// Returns the **Besselian epoch year** (BYEAR) for this instant.
751    ///
752    /// Besselian years are an older astronomical convention based on a
753    /// tropical year length of approximately 365.242198781 days.
754    ///
755    /// This is the inverse of [`Self::from_byear`].
756    ///
757    /// ## Examples
758    ///
759    /// ```rust
760    /// use deep_time::{Dt, Scale};
761    ///
762    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
763    /// assert!((x.to_byear() - 2020.000335739628).abs() < 1e-12);
764    /// ```
765    #[inline]
766    pub const fn to_byear(&self) -> Real {
767        let jd_tt = self.to_jd_f();
768        f!(1900.0) + (jd_tt - f!(2415020.31352)) / f!(365.242198781)
769    }
770
771    /// Inverse of [`Self::to_byear`].
772    pub const fn from_byear(byear: Real, scale: Scale) -> Dt {
773        if byear.is_nan() {
774            return Self::ZERO;
775        }
776        if byear.is_infinite() {
777            return if byear.is_sign_positive() {
778                Self::MAX
779            } else {
780                Self::MIN
781            };
782        }
783
784        let jd = f!(2415020.31352) + (byear - f!(1900.0)) * f!(365.242198781);
785        Self::from_jd_f(jd, scale)
786    }
787
788    /// Returns the **decimal year** (Gregorian calendar year + fraction of the year).
789    ///
790    /// This is the direct equivalent of Astropy’s `Time.decimalyear`:
791    /// - Uses the *actual* length of the specific Gregorian year (365 or 366 days,
792    ///   plus any leap seconds on UTC/UtcSpice/etc.).
793    /// - Scale-aware (TAI, TT, UTC, TDB, etc.), converts to this [`Dt`]'s target time
794    ///   scale before producing an output.
795    /// - Exact integer arithmetic for the year boundaries, then a high-precision
796    ///   `to_sec_f` division (lossy only at the final `Real` step, same as Astropy).
797    ///
798    /// ## Examples
799    ///
800    /// ```rust
801    /// use deep_time::{Dt, Scale};
802    ///
803    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
804    /// assert_eq!(x.to_decimalyear(), 2020.0);
805    ///
806    /// // Also works for negative years
807    /// let y = Dt::from_ymd(-2000, 1, 1, Scale::TAI, 0, 0, 0, 0);
808    /// assert_eq!(y.to_decimalyear(), -2000.0);
809    /// ```
810    pub fn to_decimalyear(&self) -> Real {
811        let ymd = self.to_ymd();
812        let year = ymd.yr;
813
814        let start = Self::from_ymd(year, 1, 1, self.target, 0, 0, 0, 0);
815        let next_start = Self::from_ymd(year + 1, 1, 1, self.target, 0, 0, 0, 0);
816
817        let elapsed = self.to_diff_raw(start).to_sec_f();
818        let year_length = next_start.to_diff_raw(start).to_sec_f();
819
820        // year_length is never zero for representable years
821        f!(year) + elapsed / year_length
822    }
823}