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