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