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    #[inline(always)]
15    pub const fn to_unix(&self) -> Dt {
16        self.to(self.target)
17            .to_diff_raw(Dt::UNIX_EPOCH.to(self.target))
18    }
19
20    /// Creates a TAI [`Dt`] from a unix (1970 epoch) timestamp.
21    ///
22    /// ## Examples
23    ///
24    /// ```rust
25    /// use deep_time::{Dt, Scale};
26    ///
27    /// let dt = Dt::from_ymd(1970, 1, 1, 0, 0, 0, 0, Scale::UTC);
28    ///
29    /// let unix = dt.to_unix().to_sec();
30    ///
31    /// assert_eq!(unix, 0);
32    ///
33    /// let roundtrip = Dt::from_unix(Dt::from_tai_sec(unix));
34    ///
35    /// assert_eq!(roundtrip, dt);
36    /// ```
37    #[inline(always)]
38    pub const fn from_unix(unix: Dt) -> Dt {
39        Self::from_diff_and_scale(unix, Dt::UNIX_EPOCH, true)
40    }
41
42    /// Returns this [`Dt`] but as an ntp timestamp since the epoch 1900-01-01 00:00:00 UTC.
43    ///
44    /// ## Notes:
45    ///
46    /// - The [`Dt`] first converts itself and the ntp epoch to the time scale of its
47    ///   `target` field before doing a raw difference with the epoch.
48    /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
49    ///   if it's not then the output will be incorrect.
50    ///
51    /// ## Examples
52    ///
53    /// ```rust
54    /// use deep_time::{Dt, Scale};
55    ///
56    /// // 2698012800
57    /// let dt = Dt::from_ymd(1985, 7, 1, 0, 0, 0, 0, Scale::TAI);
58    /// let ntp = dt.to_ntp();
59    ///
60    /// assert_eq!(
61    ///     ntp.to_attos(), Dt::sec_to_attos(2698012800_i128),
62    ///     "ntp sec for 1985 is wrong, got: {}, expected: {}",
63    ///     ntp.to_attos(), Dt::sec_to_attos(2698012800_i128)
64    /// );
65    ///
66    /// let dt2 = Dt::from_ntp(ntp);
67    ///
68    /// assert_eq!(
69    ///     dt.to_attos(), dt2.to_attos(),
70    ///     "round trip to Dt got wrong sec, old: {}, new: {}",
71    ///     dt.to_attos(), dt2.to_attos()
72    /// );
73    ///
74    /// let ymd = dt2.to_ymd();
75    /// assert_eq!(ymd.yr(), 1985_i64);
76    /// assert_eq!(ymd.mo(), 7);
77    /// assert_eq!(ymd.day(), 1);
78    /// assert_eq!(ymd.hr(), 0);
79    /// assert_eq!(ymd.min(), 0);
80    /// assert_eq!(ymd.sec(), 0);
81    /// assert_eq!(ymd.attos(), 0);
82    /// ```
83    #[inline(always)]
84    pub const fn to_ntp(&self) -> Dt {
85        self.to(self.target)
86            .to_diff_raw(Dt::NTP_EPOCH.to(self.target))
87    }
88
89    /// Creates a TAI [`Dt`] from an ntp (1900 epoch) timestamp.
90    #[inline(always)]
91    pub const fn from_ntp(ntp: Dt) -> Dt {
92        Self::from_diff_and_scale(ntp, Dt::NTP_EPOCH, true)
93    }
94
95    /// Returns the GPS week number and the exact Time of Week (TOW) for this instant
96    /// when expressed in **GPS Time**.
97    ///
98    /// - GPS Time is continuous (no leap seconds) and starts at the
99    ///   [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
100    ///   (1980-01-06 00:00:00 UTC).
101    /// - The returned TOW is a [`Dt`] on the TAI scale.
102    ///
103    /// This is the inverse of
104    /// [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow).
105    ///
106    /// - `week`: Full GPS week number (can be negative for dates before 1980).
107    /// - `tow`: Time of Week as a [`Dt`]. Values ≥ 604800 seconds are
108    ///   automatically carried into the week number.
109    ///
110    /// ## Examples
111    ///
112    /// ```rust
113    /// use deep_time::{Dt, Scale};
114    ///
115    /// let x = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::TAI);
116    /// let g = x.to_gps_wk_and_tow();
117    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
118    /// assert_eq!(x, z);
119    /// ```
120    pub const fn to_gps_wk_and_tow(&self) -> (i64, Dt) {
121        let total_attos = self.to_gps().to_attos();
122        let wk = total_attos.div_euclid(ATTOS_PER_WEEK) as i64;
123        let tow_attos = total_attos.rem_euclid(ATTOS_PER_WEEK);
124        // was converted to target scale, scale is now target
125        (wk, Dt::new(tow_attos, self.target, self.target))
126    }
127
128    /// Creates a [`Dt`] from a GPS week number and Time of Week (TOW).
129    ///
130    /// This is the inverse of
131    /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow).
132    ///
133    /// - `week`: Full GPS week number (can be negative for dates before 1980).
134    /// - `tow`: Time of Week as a [`Dt`]. Values ≥ 604800 seconds are
135    ///   automatically carried into the week number.
136    ///
137    /// ## Examples
138    ///
139    /// ```rust
140    /// use deep_time::{Dt, Scale};
141    ///
142    /// let x = Dt::from_ymd(2000, 1, 1, 12, 0, 0, 0, Scale::TAI);
143    /// let g = x.to_gps_wk_and_tow();
144    /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
145    /// assert_eq!(x, z);
146    /// ```
147    pub const fn from_gps_wk_and_tow(wk: i64, tow: Dt) -> Dt {
148        let total_attos = (wk as i128)
149            .saturating_mul(ATTOS_PER_WEEK)
150            .saturating_add(tow.to_attos());
151
152        Self::from_gps(Dt::new(total_attos, tow.scale, tow.target))
153    }
154
155    /// Returns the elapsed time since the GPS epoch as a [`Dt`] on the GPS scale.
156    ///
157    /// The GPS epoch is [`Dt::GPS_EPOCH`].
158    #[inline(always)]
159    pub const fn to_gps(&self) -> Dt {
160        self.to_scale_and_diff(Self::GPS_EPOCH, true)
161    }
162
163    /// Inverse of [`Self::to_gps`].
164    #[inline(always)]
165    pub const fn from_gps(elapsed: Dt) -> Dt {
166        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
167    }
168
169    /// Returns the day of the GPS week (0 = Sunday, 1 = Monday, …, 6 = Saturday).
170    ///
171    /// This value is computed directly from the GPS Time of Week and is
172    /// independent of the Gregorian calendar or civil time.
173    pub const fn to_gps_day_of_wk(&self) -> u8 {
174        let (_, tow) = self.to_gps_wk_and_tow();
175        let secs = tow.to_attos() / ATTOS_PER_SEC_I128;
176
177        (secs / SEC_PER_DAYI64 as i128) as u8
178    }
179
180    /// Returns the elapsed time since the Chandra X-ray Center (CXC) epoch
181    /// as a [`Dt`] on the TT scale.
182    ///
183    /// The CXC epoch is [`Dt::CXC_EPOCH`].
184    #[inline(always)]
185    pub const fn to_cxcsec(&self) -> Dt {
186        self.to_scale_and_diff(Self::CXC_EPOCH, true)
187    }
188
189    /// Inverse of [`Self::to_cxcsec`].
190    #[inline(always)]
191    pub const fn from_cxcsec(elapsed: Dt) -> Dt {
192        Self::from_diff_and_scale(elapsed, Self::CXC_EPOCH, true)
193    }
194
195    /// Floating-point counterpart of [`Self::from_cxcsec`].
196    #[inline(always)]
197    pub const fn from_cxcsec_f(elapsed_sec: Real) -> Dt {
198        Self::from_cxcsec(Dt::from_sec_f(elapsed_sec, Scale::TAI))
199    }
200
201    /// Returns the elapsed time since the GPS/Galileo Experiment (GALEX) epoch
202    /// as a [`Dt`] on the TAI scale.
203    ///
204    /// The GALEX epoch is [`Self::GPS_EPOCH`].
205    #[inline(always)]
206    pub const fn to_galexsec(&self) -> Dt {
207        self.to_scale_and_diff(Self::GPS_EPOCH, true)
208    }
209
210    /// Inverse of [`Self::to_galexsec`].
211    #[inline(always)]
212    pub const fn from_galexsec(elapsed: Dt) -> Dt {
213        Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
214    }
215
216    /// Floating-point counterpart of [`Self::from_galexsec`].
217    #[inline(always)]
218    pub const fn from_galexsec_f(elapsed_sec: Real) -> Dt {
219        Self::from_galexsec(Dt::from_sec_f(elapsed_sec, Scale::TAI))
220    }
221
222    /// Returns the **Julian epoch year**.
223    #[inline(always)]
224    pub const fn to_jyear(&self) -> Real {
225        let jd_tt = self.to_jd_f();
226        f!(2000.0) + (jd_tt - JD_2000_2_451_545F) / f!(365.25)
227    }
228
229    /// Inverse of [`Self::to_jyear`].
230    pub const fn from_jyear(jyear: Real, scale: Scale) -> Dt {
231        if jyear.is_nan() {
232            return Self::ZERO;
233        }
234        if jyear.is_infinite() {
235            return if jyear.is_sign_positive() {
236                Self::MAX
237            } else {
238                Self::MIN
239            };
240        }
241
242        let jd = JD_2000_2_451_545F + (jyear - f!(2000.0)) * f!(365.25);
243        Self::from_jd_f(jd, scale)
244    }
245
246    /// Returns the **Besselian epoch year**.
247    #[inline]
248    pub const fn to_byear(&self) -> Real {
249        let jd_tt = self.to_jd_f();
250        f!(1900.0) + (jd_tt - f!(2415020.31352)) / f!(365.242198781)
251    }
252
253    /// Inverse of [`Self::to_byear`].
254    pub const fn from_byear(byear: Real, scale: Scale) -> Dt {
255        if byear.is_nan() {
256            return Self::ZERO;
257        }
258        if byear.is_infinite() {
259            return if byear.is_sign_positive() {
260                Self::MAX
261            } else {
262                Self::MIN
263            };
264        }
265
266        let jd = f!(2415020.31352) + (byear - f!(1900.0)) * f!(365.242198781);
267        Self::from_jd_f(jd, scale)
268    }
269
270    /// Returns the **decimal year** (Gregorian calendar year + fraction of the year).
271    ///
272    /// This is the direct equivalent of Astropy’s `Time.decimalyear`:
273    /// - Uses the *actual* length of the specific Gregorian year (365 or 366 days,
274    ///   plus any leap seconds on UTC/UtcSpice/etc.).
275    /// - Fully scale-aware (TAI, TT, UTC, TDB, custom clocks, …).
276    /// - Exact integer arithmetic for the year boundaries, then a high-precision
277    ///   `to_sec_f` division (lossy only at the final `Real` step, same as Astropy).
278    pub fn to_decimalyear(&self) -> Real {
279        let ymd = self.to_ymd();
280        let year = ymd.yr;
281
282        let start = Self::from_ymd(year, 1, 1, 0, 0, 0, 0, self.target);
283        let next_start = Self::from_ymd(year + 1, 1, 1, 0, 0, 0, 0, self.target);
284
285        let elapsed = self.to_diff_raw(start).to_sec_f();
286        let year_length = next_start.to_diff_raw(start).to_sec_f();
287
288        // year_length is never zero for representable years
289        f!(year) + elapsed / year_length
290    }
291}