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}