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    ///
459    /// ## Implementation
460    ///
461    /// Uses [`Dt::to_scale_and_diff`](../struct.Dt.html#method.to_scale_and_diff) with
462    /// `convert_epoch = true` so both this instant and
463    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
464    /// are brought onto `target` before subtracting. Without converting the epoch, the
465    /// result would not be elapsed time since the GPS epoch on that scale.
466    #[inline(always)]
467    pub const fn to_gps(&self) -> Dt {
468        self.to_scale_and_diff(Self::GPS_EPOCH, true)
469    }
470
471    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
472    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
473    ///
474    /// This is the inverse of [`Dt::to_gps`](../struct.Dt.html#method.to_gps).
475    ///
476    /// ## Important:
477    ///
478    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
479    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
480    ///   return value of [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
481    ///   The input's `scale` field says which time scale that count is on — if it
482    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
483    ///   included).
484    /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
485    ///   to that same scale before the sum.
486    ///
487    /// ## Returns
488    ///
489    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
490    /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
491    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
492    ///
493    /// ## Examples
494    ///
495    /// ```rust
496    /// use deep_time::{Dt, Scale};
497    ///
498    /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
499    /// let gps = x.target(Scale::GPS).to_gps();
500    /// let roundtrip = Dt::from_gps(gps);
501    ///
502    /// assert_eq!(roundtrip, x);
503    /// ```
504    ///
505    /// ## See also
506    ///
507    /// - [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
508    /// - [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow)
509    #[inline(always)]
510    pub const fn from_gps(elapsed: Dt) -> Dt {
511        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
512    }
513
514    /// Returns the continuous GPS week number and Time of Week (TOW) for this instant.
515    ///
516    /// Elapsed time since [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
517    /// is computed by [`Dt::to_gps`](../struct.Dt.html#method.to_gps) — on this [`Dt`]'s
518    /// `target` time scale — and then split with
519    /// [`Dt::to_weeks_floor`](../struct.Dt.html#method.to_weeks_floor) into whole weeks plus a
520    /// non-negative remainder.
521    ///
522    /// This is the inverse of
523    /// [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow).
524    ///
525    /// ## What this is (GPS terms)
526    ///
527    /// Matches the usual software form of GPS time: **continuous week** + **time of week**
528    /// (TOW / seconds of week), as in RINEX, IGS calendars, and most receiver APIs.
529    ///
530    /// - **Week 0** starts at the GPS epoch (**1980-01-06 00:00:00** GPS time). GPS weeks
531    ///   start on **Sunday**.
532    /// - **TOW** is how far into that week the instant is. GNSS docs often quote that span in
533    ///   SI seconds in `[0, 604_800)`; here it is a [`Dt`] so sub-second precision is kept.
534    ///   Use [`to_sec`](../struct.Dt.html#method.to_sec) /
535    ///   [`to_sec_f`](../struct.Dt.html#method.to_sec_f) on `tow` when you want whole or
536    ///   floating seconds of week.
537    /// - The returned **week is continuous** (e.g. 1845 in 2015), not the modular week field
538    ///   in the navigation message (legacy LNAV: 10 bits, `0…1023`; modern CNAV: 13 bits,
539    ///   `0…8191`). For the modular broadcast field, use `week.rem_euclid(1024)` (LNAV) or
540    ///   `week.rem_euclid(8192)` (CNAV).
541    ///
542    /// ## Important:
543    ///
544    /// - Uses [`Dt::to_gps`](../struct.Dt.html#method.to_gps) internally: this [`Dt`] and
545    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) are both converted
546    ///   to the `target` time scale before differencing.
547    /// - **You may need to change the [`Dt`]'s `target` field** before calling if you need
548    ///   week/TOW on a particular time scale, e.g. `Scale::GPS`.
549    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
550    ///   if it's not then the output will be incorrect.
551    ///
552    /// ## Returns
553    ///
554    /// A `(week, tow)` pair:
555    ///
556    /// - `week` (`i128`): how many full weeks have completed since
557    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) (via
558    ///   [`Dt::to_gps`](../struct.Dt.html#method.to_gps)). Example: `1845`. Before the epoch,
559    ///   floor division yields a **negative** week — continuous library time, not a broadcast
560    ///   GPS week number.
561    /// - `tow` ([`Dt`]): the within-week remainder. Its attosecond count is in
562    ///   `[0, ATTOS_PER_WEEK)` — less than one week, and ≥ 0 after the epoch. Its `scale` and
563    ///   `target` are set to this [`Dt`]'s `target` so
564    ///   [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow) knows which
565    ///   time scale the pair belongs to. Floor division keeps `tow` non-negative even when
566    ///   the elapsed time is negative (pre-epoch).
567    ///
568    /// ## Examples
569    ///
570    /// ```rust
571    /// use deep_time::{Dt, Scale};
572    ///
573    /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
574    /// let g = x.to_gps_wk_and_tow();
575    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
576    /// assert_eq!(x, z);
577    ///
578    /// // for conventional GPS-time week/TOW, set target first:
579    /// let g = x.target(Scale::GPS).to_gps_wk_and_tow();
580    /// ```
581    ///
582    /// ## See also
583    ///
584    /// - [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow)
585    /// - [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
586    /// - [`Dt::to_gps_day_of_wk`](../struct.Dt.html#method.to_gps_day_of_wk)
587    #[inline]
588    pub const fn to_gps_wk_and_tow(&self) -> (i128, Dt) {
589        let (wk, tow_attos) = self.to_gps().to_weeks_floor();
590        // was converted to target scale, scale is now target
591        (wk, Dt::new(tow_attos, self.target, self.target))
592    }
593
594    /// Creates a [`Dt`] from a GPS week number and Time of Week (TOW).
595    ///
596    /// Recombines `week` and `tow` into elapsed time since
597    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH), then passes that to
598    /// [`Dt::from_gps`](../struct.Dt.html#method.from_gps).
599    ///
600    /// This is the inverse of
601    /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow).
602    ///
603    /// ## Important:
604    ///
605    /// - Uses [`Dt::from_gps`](../struct.Dt.html#method.from_gps) internally: the elapsed time
606    ///   is interpreted on the `tow` [`Dt`]'s `scale` / `target` fields, and
607    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted to that
608    ///   same scale before the sum.
609    /// - Pass back the `tow` from [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
610    ///   unchanged if you want a round trip.
611    ///
612    /// ## Returns
613    ///
614    /// A **TAI** [`Dt`] for the reconstructed instant. Its `target` field is taken from `tow`.
615    ///
616    /// `tow` must be a [`Dt`] (not a bare second count) because
617    /// [`Dt::from_gps`](../struct.Dt.html#method.from_gps) needs both the within-week attoseconds
618    /// and the `scale` / `target` that say which time scale `week` and `tow` were expressed on.
619    /// The week number is multiplied back into attoseconds (`week * ATTOS_PER_WEEK`); only `tow`
620    /// carries the scale and sub-week precision needed for the round trip.
621    ///
622    /// Prefer a `tow` from [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
623    /// (non-negative, strictly less than one week). `week` is the **continuous** GPS week
624    /// (not the modular 10-/13-bit broadcast field). Negative `week` values only arise from
625    /// dates before 1980-01-06 (see that function).
626    ///
627    /// ## Examples
628    ///
629    /// ```rust
630    /// use deep_time::{Dt, Scale};
631    ///
632    /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
633    /// let g = x.to_gps_wk_and_tow();
634    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
635    /// assert_eq!(x, z);
636    /// ```
637    ///
638    /// ## See also
639    ///
640    /// - [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
641    /// - [`Dt::from_gps`](../struct.Dt.html#method.from_gps)
642    pub const fn from_gps_wk_and_tow(wk: i128, tow: Dt) -> Dt {
643        let total_attos = wk
644            .saturating_mul(ATTOS_PER_WEEK)
645            .saturating_add(tow.to_attos());
646
647        Self::from_gps(Dt::new(total_attos, tow.scale, tow.target))
648    }
649
650    /// Returns the day of the GPS week (0 = Sunday, 1 = Monday, …, 6 = Saturday).
651    ///
652    /// GPS weeks start on **Sunday**. Derived from the Time of Week of
653    /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
654    /// (`floor(tow.to_attos() / ATTOS_PER_DAY)`), not from the civil Gregorian weekday of the
655    /// calendar date.
656    pub const fn to_gps_day_of_wk(&self) -> u8 {
657        let (_, tow) = self.to_gps_wk_and_tow();
658        let sec = tow.to_attos() / ATTOS_PER_SEC_I128;
659
660        (sec / SEC_PER_DAY_I64 as i128) as u8
661    }
662
663    /// Returns this [`Dt`] but as time since the
664    /// [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) on its
665    /// `target` time scale.
666    ///
667    /// ## Important:
668    ///
669    /// - The [`Dt`] first converts itself and the epoch to the time scale of its
670    ///   `target` field before doing a raw difference with the epoch.
671    /// - **You may need to change the [`Dt`]'s `target` field** before calling the function
672    ///   if you need the timestamp to be on a particular time scale, e.g. `UTC`.
673    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
674    ///   if it's not then the output will be incorrect.
675    ///
676    /// ## Returns
677    ///
678    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
679    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
680    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
681    ///   `Scale::TT` after `.target(Scale::TT)`. The result's `scale` and `target` are both
682    ///   set to that same value.
683    ///
684    /// ## Examples
685    ///
686    /// ```rust
687    /// use deep_time::{Dt, Scale};
688    ///
689    /// let cxc = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
690    ///     .target(Scale::TT)
691    ///     .to_cxcsec()
692    ///     .to_sec_f();
693    ///
694    /// // cxcsec 694224032.184 (matches Astropy)
695    /// assert_eq!(cxc, 694224032.184);
696    /// ```
697    ///
698    /// ## See also
699    ///
700    /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
701    #[inline(always)]
702    pub const fn to_cxcsec(&self) -> Dt {
703        self.to_scale_and_diff(Self::CXC_EPOCH, true)
704    }
705
706    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
707    /// [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
708    ///
709    /// This is the inverse of [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec).
710    ///
711    /// ## Important:
712    ///
713    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
714    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) — typically the
715    ///   return value of [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
716    ///   The input's `scale` field says which time scale that count is on — if it
717    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
718    ///   included).
719    /// - [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) is converted
720    ///   to that same scale before the sum.
721    ///
722    /// ## Returns
723    ///
724    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
725    /// [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) — it is attoseconds since
726    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
727    ///
728    /// ## Examples
729    ///
730    /// ```rust
731    /// use deep_time::{Dt, Scale};
732    ///
733    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
734    /// let cxc = x.target(Scale::TT).to_cxcsec();
735    /// let roundtrip = Dt::from_cxcsec(cxc);
736    ///
737    /// assert_eq!(roundtrip, x);
738    /// ```
739    ///
740    /// ## See also
741    ///
742    /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
743    /// - [`Dt::from_cxcsec_f`](../struct.Dt.html#method.from_cxcsec_f)
744    #[inline(always)]
745    pub const fn from_cxcsec(elapsed: Dt) -> Dt {
746        Self::from_diff_and_scale(elapsed, Self::CXC_EPOCH, true)
747    }
748
749    /// Convenience wrapper around
750    /// [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
751    /// for a bare floating-point second count.
752    ///
753    /// ## Parameters
754    ///
755    /// - `sec` — seconds elapsed since
756    ///   [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
757    /// - `on` — which [`Scale`] the count is measured in (for example `Scale::TT` or
758    ///   `Scale::UTC`). This becomes the wrapped [`Dt`]'s `scale`;
759    ///   [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
760    ///   then uses it when turning the elapsed count into an absolute TAI instant
761    ///   (including leap-second handling where applicable). Same role as the `scale`
762    ///   field on the [`Dt`] you would hand to
763    ///   [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
764    ///   directly.
765    ///
766    /// ## Examples
767    ///
768    /// ```rust
769    /// use deep_time::{Dt, Scale};
770    ///
771    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
772    /// let cxc = x.target(Scale::TT).to_cxcsec().to_sec_f();
773    /// let roundtrip = Dt::from_cxcsec_f(cxc, Scale::TT);
774    ///
775    /// assert_eq!(roundtrip.to_cxcsec().to_sec_f(), cxc);
776    /// ```
777    ///
778    /// ## See also
779    ///
780    /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
781    /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
782    #[inline(always)]
783    pub const fn from_cxcsec_f(sec: Real, on: Scale) -> Dt {
784        Self::from_cxcsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
785    }
786
787    /// Returns the elapsed time since the GALEX epoch as a [`Dt`] expressed
788    /// in this object's current `target` scale.
789    ///
790    /// This method can match Astropy’s `Time.galexsec` format. To match
791    /// Astropy output, set `.target(Scale::UTC)`
792    /// before calling.
793    ///
794    /// The GALEX epoch is
795    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
796    /// (same epoch used by GPS time).
797    ///
798    /// ## Important:
799    ///
800    /// - The [`Dt`] first converts itself and the [`Dt::GPS_EPOCH`] to the time
801    ///   scale of its `target` field before doing a raw difference with the epoch.
802    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
803    ///   epoch, if it's not then the output will be incorrect.
804    ///
805    /// ## Returns
806    ///
807    /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
808    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
809    /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
810    ///   `Scale::UTC` after `.target(Scale::UTC)`. The result's `scale` and `target` are both
811    ///   set to that same value.
812    ///
813    /// ## Examples
814    ///
815    /// ```rust
816    /// use deep_time::{Dt, Scale};
817    ///
818    /// let galexsec = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
819    ///     .target(Scale::UTC)
820    ///     .to_galexsec()
821    ///     .to_sec_f();
822    ///
823    /// assert_eq!(galexsec, 1261871963.0);
824    /// ```
825    ///
826    /// ## See also
827    ///
828    /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
829    #[inline(always)]
830    pub const fn to_galexsec(&self) -> Dt {
831        self.to_scale_and_diff(Self::GPS_EPOCH, true)
832    }
833
834    /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
835    /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
836    ///
837    /// This is the inverse of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec).
838    /// GALEX seconds use the same epoch as GPS time.
839    ///
840    /// ## Important:
841    ///
842    /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
843    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
844    ///   return value of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
845    ///   The input's `scale` field says which time scale that count is on — if it
846    ///   is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
847    ///   included).
848    /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
849    ///   to that same scale before the sum.
850    ///
851    /// ## Returns
852    ///
853    /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
854    /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
855    /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
856    ///
857    /// ## Examples
858    ///
859    /// ```rust
860    /// use deep_time::{Dt, Scale};
861    ///
862    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
863    /// let galex = x.target(Scale::UTC).to_galexsec();
864    /// let roundtrip = Dt::from_galexsec(galex);
865    ///
866    /// assert_eq!(roundtrip, x);
867    /// ```
868    ///
869    /// ## See also
870    ///
871    /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
872    /// - [`Dt::from_galexsec_f`](../struct.Dt.html#method.from_galexsec_f)
873    #[inline(always)]
874    pub const fn from_galexsec(elapsed: Dt) -> Dt {
875        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
876    }
877
878    /// Convenience wrapper around
879    /// [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
880    /// for a bare floating-point second count.
881    ///
882    /// ## Parameters
883    ///
884    /// - `sec` — seconds elapsed since
885    ///   [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
886    /// - `on` — which [`Scale`] the count is measured in (for example `Scale::UTC` or
887    ///   `Scale::TT`). This becomes the wrapped [`Dt`]'s `scale`;
888    ///   [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
889    ///   then uses it when turning the elapsed count into an absolute TAI instant
890    ///   (including leap-second handling where applicable). Same role as the `scale`
891    ///   field on the [`Dt`] you would hand to
892    ///   [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec) directly.
893    ///
894    /// ## Examples
895    ///
896    /// ```rust
897    /// use deep_time::{Dt, Scale};
898    ///
899    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
900    /// let galex = x.target(Scale::UTC).to_galexsec().to_sec_f();
901    /// let roundtrip = Dt::from_galexsec_f(galex, Scale::UTC);
902    ///
903    /// assert_eq!(roundtrip, x);
904    /// ```
905    ///
906    /// ## See also
907    ///
908    /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
909    /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
910    #[inline(always)]
911    pub const fn from_galexsec_f(sec: Real, on: Scale) -> Dt {
912        Self::from_galexsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
913    }
914
915    /// Returns the **Julian epoch year** (JYEAR) for this instant.
916    ///
917    /// Julian years are defined as exactly 365.25 days of 86400 SI seconds.
918    /// This is the system used for J2000.0 and many astronomical calculations.
919    ///
920    /// This is **not** the same as
921    /// [`Dt::to_decimalyear`](../struct.Dt.html#method.to_decimalyear),
922    /// which uses the actual length of the specific Gregorian year.
923    ///
924    /// This is the inverse of
925    /// [`Dt::from_jyear`](../struct.Dt.html#method.from_jyear).
926    ///
927    /// ## Important:
928    ///
929    /// - The [`Dt`] first converts itself to the time scale of its `target` field
930    ///   before producing a result.
931    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
932    ///   epoch, if it's not then the output will be incorrect.
933    ///
934    /// ## Examples
935    ///
936    /// ```rust
937    /// use deep_time::{Dt, Scale};
938    ///
939    /// let x = Dt::from_ymd(2020, 1, 1, Scale::UTC, 0, 0, 0, 0);
940    ///
941    /// assert_eq!(x.to_jyear(), 2019.9986310746065);
942    /// ```
943    #[inline(always)]
944    pub const fn to_jyear(&self) -> Real {
945        let jd_tt = self.to_jd_f();
946        f!(2000.0) + (jd_tt - JD_2000_2_451_545F) / f!(365.25)
947    }
948
949    /// Inverse of
950    /// [`Dt::to_jyear`](../struct.Dt.html#method.to_jyear).
951    pub const fn from_jyear(jyear: Real, scale: Scale) -> Dt {
952        if jyear.is_nan() {
953            return Self::ZERO;
954        }
955        if jyear.is_infinite() {
956            return if jyear.is_sign_positive() {
957                Self::MAX
958            } else {
959                Self::MIN
960            };
961        }
962
963        let jd = JD_2000_2_451_545F + (jyear - f!(2000.0)) * f!(365.25);
964        Self::from_jd_f(jd, scale)
965    }
966
967    /// Returns the **Besselian epoch year** (BYEAR) for this instant.
968    ///
969    /// Besselian years are an older astronomical convention based on a
970    /// tropical year length of approximately 365.242198781 days.
971    ///
972    /// This is the inverse of
973    /// [`Dt::from_byear`](../struct.Dt.html#method.from_byear).
974    ///
975    /// ## Important:
976    ///
977    /// - The [`Dt`] first converts itself to the time scale of its `target` field
978    ///   before producing a result.
979    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
980    ///   epoch, if it's not then the output will be incorrect.
981    ///
982    /// ## Examples
983    ///
984    /// ```rust
985    /// use deep_time::{Dt, Scale};
986    ///
987    /// let x = Dt::from_ymd(2020, 1, 1, Scale::UTC, 0, 0, 0, 0);
988    ///
989    /// assert!((x.to_byear() - 2020.000335739628).abs() < 1e-12);
990    /// ```
991    #[inline]
992    pub const fn to_byear(&self) -> Real {
993        let jd_tt = self.to_jd_f();
994        f!(1900.0) + (jd_tt - f!(2415020.31352)) / f!(365.242198781)
995    }
996
997    /// Inverse of
998    /// [`Dt::to_byear`](../struct.Dt.html#method.to_byear).
999    pub const fn from_byear(byear: Real, scale: Scale) -> Dt {
1000        if byear.is_nan() {
1001            return Self::ZERO;
1002        }
1003        if byear.is_infinite() {
1004            return if byear.is_sign_positive() {
1005                Self::MAX
1006            } else {
1007                Self::MIN
1008            };
1009        }
1010
1011        let jd = f!(2415020.31352) + (byear - f!(1900.0)) * f!(365.242198781);
1012        Self::from_jd_f(jd, scale)
1013    }
1014
1015    /// Returns the **decimal year** (Gregorian calendar year + fraction of the year).
1016    ///
1017    /// This is the direct equivalent of Astropy’s `Time.decimalyear`:
1018    /// - Uses the *actual* length of the specific Gregorian year (365 or 366 days,
1019    ///   plus any leap seconds on UTC/UtcSpice/etc.).
1020    /// - Scale-aware (TAI, TT, UTC, TDB, etc.), converts to this [`Dt`]'s target time
1021    ///   scale before producing an output.
1022    /// - Exact integer arithmetic for the year boundaries, then a high-precision
1023    ///   `to_sec_f` division (lossy only at the final `Real` step, same as Astropy).
1024    ///
1025    /// ## Important:
1026    ///
1027    /// - The [`Dt`] first converts itself to the time scale of its `target` field
1028    ///   before producing a result.
1029    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
1030    ///   epoch, if it's not then the output will be incorrect.
1031    ///
1032    /// ## Examples
1033    ///
1034    /// ```rust
1035    /// use deep_time::{Dt, Scale};
1036    ///
1037    /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
1038    /// assert_eq!(x.to_decimalyear(), 2020.0);
1039    ///
1040    /// // Also works for negative years
1041    /// let y = Dt::from_ymd(-2000, 1, 1, Scale::TAI, 0, 0, 0, 0);
1042    /// assert_eq!(y.to_decimalyear(), -2000.0);
1043    /// ```
1044    pub fn to_decimalyear(&self) -> Real {
1045        let ymd = self.to_ymd();
1046        let year = ymd.yr;
1047
1048        let start = Self::from_ymd(year, 1, 1, self.target, 0, 0, 0, 0);
1049        let next_start = Self::from_ymd(year.saturating_add(1), 1, 1, self.target, 0, 0, 0, 0);
1050
1051        let elapsed = self.to_diff_raw(start).to_sec_f();
1052        let year_length = next_start.to_diff_raw(start).to_sec_f();
1053
1054        // If start and next_start collapse (extreme / saturated years), avoid / 0.
1055        if year_length == 0.0 {
1056            return f!(year);
1057        }
1058        f!(year) + elapsed / year_length
1059    }
1060}