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