Skip to main content

deep_time/dt/
conveniences.rs

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