Skip to main content

deep_time/dt/
conveniences.rs

1use crate::{
2    ATTOS_PER_SEC_I128, ATTOS_PER_WEEK, Dt, JD_2000_2_451_545F, Real, SEC_PER_DAYI64, Scale,
3};
4
5impl Dt {
6    /// Returns this [`Dt`] but as a unix timestamp since the UNIX epoch (1970-01-01 00:00:00).
7    ///
8    /// ## Notes:
9    ///
10    /// - The [`Dt`] first converts itself and the unix epoch to the time scale of its
11    ///   `target` field before doing a raw difference with the epoch.
12    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
13    ///   if it's not then the output will be incorrect.
14    ///
15    /// ## Examples
16    ///
17    /// ```rust
18    /// use deep_time::{Dt, Scale};
19    ///
20    /// let dt = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::UTC);
21    /// let unix = dt.to_unix();
22    ///
23    /// assert_eq!(
24    ///     unix.to_sec(),
25    ///     946728000,
26    ///     "unix sec for 2000-01-01 12:00:00 UTC is wrong, got: {}, expected: 946728000",
27    ///     unix.to_sec()
28    /// );
29    ///
30    /// let dt2 = Dt::from_unix(unix);
31    ///
32    /// assert_eq!(
33    ///     dt.to_attos(), dt2.to_attos(),
34    ///     "round trip to Dt got wrong attos, old: {}, new: {}",
35    ///     dt.to_attos(), dt2.to_attos()
36    /// );
37    ///
38    /// let ymd = dt2.to_ymd();
39    /// assert_eq!(ymd.yr(), 2000_i64);
40    /// assert_eq!(ymd.mo(), 1);
41    /// assert_eq!(ymd.day(), 1);
42    /// assert_eq!(ymd.hr(), 12);
43    /// assert_eq!(ymd.min(), 0);
44    /// assert_eq!(ymd.sec(), 0);
45    /// assert_eq!(ymd.attos(), 0);
46    /// ```
47    #[inline(always)]
48    pub const fn to_unix(&self) -> Dt {
49        self.to_scale_and_diff(Self::UNIX_EPOCH, true)
50    }
51
52    /// Creates a TAI [`Dt`] from a unix (1970 epoch) timestamp.
53    ///
54    /// ## Examples
55    ///
56    /// ```rust
57    /// use deep_time::{Dt, Scale};
58    ///
59    /// let dt = Dt::from_ymd(1970, 1, 1, 0, 0, 0, 0, Scale::UTC);
60    ///
61    /// let unix = dt.to_unix().to_sec();
62    ///
63    /// assert_eq!(unix, 0);
64    ///
65    /// let roundtrip = Dt::from_unix(Dt::from_tai_sec(unix));
66    ///
67    /// assert_eq!(roundtrip, dt);
68    /// ```
69    #[inline(always)]
70    pub const fn from_unix(unix: Dt) -> Dt {
71        Self::from_diff_and_scale(unix, Dt::UNIX_EPOCH, true)
72    }
73
74    /// Returns this [`Dt`] but as an ntp timestamp since the epoch 1900-01-01 00:00:00 UTC.
75    ///
76    /// ## Notes:
77    ///
78    /// - The [`Dt`] first converts itself and the ntp epoch to the time scale of its
79    ///   `target` field before doing a raw difference with the epoch.
80    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
81    ///   if it's not then the output will be incorrect.
82    ///
83    /// ## Examples
84    ///
85    /// ```rust
86    /// use deep_time::{Dt, Scale};
87    ///
88    /// // 2698012800
89    /// let dt = Dt::from_ymd(1985, 7, 1, 0, 0, 0, 0, Scale::TAI);
90    /// let ntp = dt.to_ntp();
91    ///
92    /// assert_eq!(
93    ///     ntp.to_attos(), Dt::sec_to_attos(2698012800_i128),
94    ///     "ntp sec for 1985 is wrong, got: {}, expected: {}",
95    ///     ntp.to_attos(), Dt::sec_to_attos(2698012800_i128)
96    /// );
97    ///
98    /// let dt2 = Dt::from_ntp(ntp);
99    ///
100    /// assert_eq!(
101    ///     dt.to_attos(), dt2.to_attos(),
102    ///     "round trip to Dt got wrong sec, old: {}, new: {}",
103    ///     dt.to_attos(), dt2.to_attos()
104    /// );
105    ///
106    /// let ymd = dt2.to_ymd();
107    /// assert_eq!(ymd.yr(), 1985_i64);
108    /// assert_eq!(ymd.mo(), 7);
109    /// assert_eq!(ymd.day(), 1);
110    /// assert_eq!(ymd.hr(), 0);
111    /// assert_eq!(ymd.min(), 0);
112    /// assert_eq!(ymd.sec(), 0);
113    /// assert_eq!(ymd.attos(), 0);
114    /// ```
115    #[inline(always)]
116    pub const fn to_ntp(&self) -> Dt {
117        self.to_scale_and_diff(Self::NTP_EPOCH, true)
118    }
119
120    /// Creates a TAI [`Dt`] from an ntp (1900 epoch) timestamp.
121    #[inline(always)]
122    pub const fn from_ntp(ntp: Dt) -> Dt {
123        Self::from_diff_and_scale(ntp, Self::NTP_EPOCH, true)
124    }
125
126    /// Returns the GPS week number and the exact Time of Week (TOW) for this instant
127    /// when expressed in **GPS Time**.
128    ///
129    /// - GPS Time is continuous (no leap seconds) and starts at the
130    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
131    ///   (1980-01-06 00:00:00 UTC).
132    /// - The returned TOW is a [`Dt`] on the TAI scale.
133    ///
134    /// This is the inverse of
135    /// [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow).
136    ///
137    /// - `week`: Full GPS week number (can be negative for dates before 1980).
138    /// - `tow`: Time of Week as a [`Dt`]. Values ≥ 604800 seconds are
139    ///   automatically carried into the week number.
140    ///
141    /// ## Examples
142    ///
143    /// ```rust
144    /// use deep_time::{Dt, Scale};
145    ///
146    /// let x = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::TAI);
147    /// let g = x.to_gps_wk_and_tow();
148    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
149    /// assert_eq!(x, z);
150    /// ```
151    pub const fn to_gps_wk_and_tow(&self) -> (i64, Dt) {
152        let total_attos = self.to_gps().to_attos();
153        let wk = total_attos.div_euclid(ATTOS_PER_WEEK) as i64;
154        let tow_attos = total_attos.rem_euclid(ATTOS_PER_WEEK);
155        // was converted to target scale, scale is now target
156        (wk, Dt::new(tow_attos, self.target, self.target))
157    }
158
159    /// Creates a [`Dt`] from a GPS week number and Time of Week (TOW).
160    ///
161    /// This is the inverse of
162    /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow).
163    ///
164    /// - `week`: Full GPS week number (can be negative for dates before 1980).
165    /// - `tow`: Time of Week as a [`Dt`]. Values ≥ 604800 seconds are
166    ///   automatically carried into the week number.
167    ///
168    /// ## Examples
169    ///
170    /// ```rust
171    /// use deep_time::{Dt, Scale};
172    ///
173    /// let x = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::TAI);
174    /// let g = x.to_gps_wk_and_tow();
175    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
176    /// assert_eq!(x, z);
177    /// ```
178    pub const fn from_gps_wk_and_tow(wk: i64, tow: Dt) -> Dt {
179        let total_attos = (wk as i128)
180            .saturating_mul(ATTOS_PER_WEEK)
181            .saturating_add(tow.to_attos());
182
183        Self::from_gps(Dt::new(total_attos, tow.scale, tow.target))
184    }
185
186    /// Returns the elapsed time since the GPS epoch as a [`Dt`] on the GPS scale.
187    ///
188    /// The GPS epoch is [`Dt::GPS_EPOCH`].
189    #[inline(always)]
190    pub const fn to_gps(&self) -> Dt {
191        self.to_scale_and_diff(Self::GPS_EPOCH, true)
192    }
193
194    /// Inverse of [`Self::to_gps`].
195    #[inline(always)]
196    pub const fn from_gps(elapsed: Dt) -> Dt {
197        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
198    }
199
200    /// Returns the day of the GPS week (0 = Sunday, 1 = Monday, …, 6 = Saturday).
201    ///
202    /// This value is computed directly from the GPS Time of Week and is
203    /// independent of the Gregorian calendar or civil time.
204    pub const fn to_gps_day_of_wk(&self) -> u8 {
205        let (_, tow) = self.to_gps_wk_and_tow();
206        let secs = tow.to_attos() / ATTOS_PER_SEC_I128;
207
208        (secs / SEC_PER_DAYI64 as i128) as u8
209    }
210
211    /// Returns the elapsed time since the Chandra X-ray Center (CXC) epoch
212    /// as a [`Dt`] on the **TT** scale.
213    ///
214    /// The CXC epoch is [`Dt::CXC_EPOCH`] = 1998-01-01 00:00:00 TT
215    /// (standard reference epoch for Chandra MET — Mission Elapsed Time).
216    ///
217    /// This is the inverse of [`Self::from_cxcsec`].
218    ///
219    /// ## Examples
220    ///
221    /// ```rust
222    /// use deep_time::{Dt, Scale};
223    ///
224    /// let cxc = Dt::from_ymd(2020, 1, 1, 0, 0, 0, 0, Scale::TAI)
225    ///     .target(Scale::TT)
226    ///     .to_cxcsec()
227    ///     .to_sec_f();
228    ///
229    /// // cxcsec 694224032.184 (matches Astropy)
230    /// assert_eq!(cxc, 694224032.184);
231    /// ```
232    #[inline(always)]
233    pub const fn to_cxcsec(&self) -> Dt {
234        self.to_scale_and_diff(Self::CXC_EPOCH, true)
235    }
236
237    /// Inverse of [`Self::to_cxcsec`].
238    #[inline(always)]
239    pub const fn from_cxcsec(elapsed: Dt) -> Dt {
240        Self::from_diff_and_scale(elapsed, Self::CXC_EPOCH, true)
241    }
242
243    /// Floating-point counterpart of [`Self::from_cxcsec`].
244    #[inline(always)]
245    pub const fn from_cxcsec_f(elapsed_sec: Real) -> Dt {
246        Self::from_cxcsec(Dt::from_sec_f(elapsed_sec, Scale::TAI))
247    }
248
249    /// Returns the elapsed time since the GALEX epoch as a [`Dt`] expressed
250    /// in this object's current `target` scale.
251    ///
252    /// The GALEX epoch is [`Self::GPS_EPOCH`] (same epoch used by GPS time).
253    /// This method can match Astropy’s `Time.galexsec` format. To match
254    /// Astropy output, set `.target(Scale::UTC)` (or the appropriate scale)
255    /// before calling.
256    ///
257    /// This is the inverse of [`Self::from_galexsec`].
258    ///
259    /// ## Examples
260    ///
261    /// ```rust
262    /// use deep_time::{Dt, Scale};
263    ///
264    /// let galexsec = Dt::from_ymd(2020, 1, 1, 0, 0, 0, 0, Scale::TAI)
265    ///     .target(Scale::UTC)
266    ///     .to_galexsec()
267    ///     .to_sec_f();
268    ///
269    /// assert_eq!(galexsec, 1261871963.0);
270    /// ```
271    #[inline(always)]
272    pub const fn to_galexsec(&self) -> Dt {
273        self.to_scale_and_diff(Self::GPS_EPOCH, true)
274    }
275
276    /// Inverse of [`Self::to_galexsec`].
277    #[inline(always)]
278    pub const fn from_galexsec(elapsed: Dt) -> Dt {
279        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
280    }
281
282    /// Floating-point counterpart of [`Self::from_galexsec`].
283    #[inline(always)]
284    pub const fn from_galexsec_f(elapsed_sec: Real) -> Dt {
285        Self::from_galexsec(Dt::from_sec_f(elapsed_sec, Scale::TAI))
286    }
287
288    /// Returns the **Julian epoch year** (JYEAR) for this instant.
289    ///
290    /// Julian years are defined as exactly 365.25 days of 86400 SI seconds.
291    /// This is the system used for J2000.0 and many astronomical calculations.
292    ///
293    /// This is **not** the same as [`Self::to_decimalyear`], which uses the
294    /// actual length of the specific Gregorian year.
295    ///
296    /// This is the inverse of [`Self::from_jyear`].
297    ///
298    /// ## Examples
299    ///
300    /// ```rust
301    /// use deep_time::{Dt, Scale};
302    ///
303    /// let x = Dt::from_ymd(2020, 1, 1, 0, 0, 0, 0, Scale::TAI);
304    /// assert_eq!(x.to_jyear(), 2019.9986310746065);
305    /// ```
306    #[inline(always)]
307    pub const fn to_jyear(&self) -> Real {
308        let jd_tt = self.to_jd_f();
309        f!(2000.0) + (jd_tt - JD_2000_2_451_545F) / f!(365.25)
310    }
311
312    /// Inverse of [`Self::to_jyear`].
313    pub const fn from_jyear(jyear: Real, scale: Scale) -> Dt {
314        if jyear.is_nan() {
315            return Self::ZERO;
316        }
317        if jyear.is_infinite() {
318            return if jyear.is_sign_positive() {
319                Self::MAX
320            } else {
321                Self::MIN
322            };
323        }
324
325        let jd = JD_2000_2_451_545F + (jyear - f!(2000.0)) * f!(365.25);
326        Self::from_jd_f(jd, scale)
327    }
328
329    /// Returns the **Besselian epoch year** (BYEAR) for this instant.
330    ///
331    /// Besselian years are an older astronomical convention based on a
332    /// tropical year length of approximately 365.242198781 days.
333    ///
334    /// This is the inverse of [`Self::from_byear`].
335    ///
336    /// ## Examples
337    ///
338    /// ```rust
339    /// use deep_time::{Dt, Scale};
340    ///
341    /// let x = Dt::from_ymd(2020, 1, 1, 0, 0, 0, 0, Scale::TAI);
342    /// assert!((x.to_byear() - 2020.000335739628).abs() < 1e-12);
343    /// ```
344    #[inline]
345    pub const fn to_byear(&self) -> Real {
346        let jd_tt = self.to_jd_f();
347        f!(1900.0) + (jd_tt - f!(2415020.31352)) / f!(365.242198781)
348    }
349
350    /// Inverse of [`Self::to_byear`].
351    pub const fn from_byear(byear: Real, scale: Scale) -> Dt {
352        if byear.is_nan() {
353            return Self::ZERO;
354        }
355        if byear.is_infinite() {
356            return if byear.is_sign_positive() {
357                Self::MAX
358            } else {
359                Self::MIN
360            };
361        }
362
363        let jd = f!(2415020.31352) + (byear - f!(1900.0)) * f!(365.242198781);
364        Self::from_jd_f(jd, scale)
365    }
366
367    /// Returns the **decimal year** (Gregorian calendar year + fraction of the year).
368    ///
369    /// This is the direct equivalent of Astropy’s `Time.decimalyear`:
370    /// - Uses the *actual* length of the specific Gregorian year (365 or 366 days,
371    ///   plus any leap seconds on UTC/UtcSpice/etc.).
372    /// - Scale-aware (TAI, TT, UTC, TDB, etc.), converts to this [`Dt`]'s target time
373    ///   scale before producing an output.
374    /// - Exact integer arithmetic for the year boundaries, then a high-precision
375    ///   `to_sec_f` division (lossy only at the final `Real` step, same as Astropy).
376    ///
377    /// ## Examples
378    ///
379    /// ```rust
380    /// use deep_time::{Dt, Scale};
381    ///
382    /// let x = Dt::from_ymd(2020, 1, 1, 0, 0, 0, 0, Scale::TAI);
383    /// assert_eq!(x.to_decimalyear(), 2020.0);
384    ///
385    /// // Also works for negative years
386    /// let y = Dt::from_ymd(-2000, 1, 1, 0, 0, 0, 0, Scale::TAI);
387    /// assert_eq!(y.to_decimalyear(), -2000.0);
388    /// ```
389    pub fn to_decimalyear(&self) -> Real {
390        let ymd = self.to_ymd();
391        let year = ymd.yr;
392
393        let start = Self::from_ymd(year, 1, 1, 0, 0, 0, 0, self.target);
394        let next_start = Self::from_ymd(year + 1, 1, 1, 0, 0, 0, 0, self.target);
395
396        let elapsed = self.to_diff_raw(start).to_sec_f();
397        let year_length = next_start.to_diff_raw(start).to_sec_f();
398
399        // year_length is never zero for representable years
400        f!(year) + elapsed / year_length
401    }
402}