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}