Skip to main content

deep_time/dt/
conveniences.rs

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