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 /// - [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd)
459 /// - [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
460 ///
461 /// ## Implementation
462 ///
463 /// `convert_epoch` is `true`. If we did not convert the epoch, we would not get seconds
464 /// since the GPS epoch; we would get seconds since something else.
465 ///
466 /// [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd) / [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
467 /// do the opposite: if they converted the epoch too, the difference would cancel out. See
468 /// [`to_ymd`](../struct.Dt.html#method.to_ymd).
469 #[inline(always)]
470 pub const fn to_gps(&self) -> Dt {
471 self.to_scale_and_diff(Self::GPS_EPOCH, true)
472 }
473
474 /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
475 /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
476 ///
477 /// This is the inverse of [`Dt::to_gps`](../struct.Dt.html#method.to_gps).
478 ///
479 /// ## Important:
480 ///
481 /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
482 /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
483 /// return value of [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
484 /// The input's `scale` field says which time scale that count is on — if it
485 /// is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
486 /// included).
487 /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
488 /// to that same scale before the sum.
489 ///
490 /// ## Returns
491 ///
492 /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
493 /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
494 /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
495 ///
496 /// ## Examples
497 ///
498 /// ```rust
499 /// use deep_time::{Dt, Scale};
500 ///
501 /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
502 /// let gps = x.target(Scale::GPS).to_gps();
503 /// let roundtrip = Dt::from_gps(gps);
504 ///
505 /// assert_eq!(roundtrip, x);
506 /// ```
507 ///
508 /// ## See also
509 ///
510 /// - [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
511 /// - [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow)
512 #[inline(always)]
513 pub const fn from_gps(elapsed: Dt) -> Dt {
514 Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
515 }
516
517 /// Returns the GPS week number and Time of Week (TOW) for this instant.
518 ///
519 /// Elapsed time since [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
520 /// is computed by [`Dt::to_gps`](../struct.Dt.html#method.to_gps) — on this [`Dt`]'s
521 /// `target` time scale — and then split into whole weeks plus a remainder.
522 ///
523 /// This is the inverse of
524 /// [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow).
525 ///
526 /// ## Important:
527 ///
528 /// - Uses [`Dt::to_gps`](../struct.Dt.html#method.to_gps) internally: this [`Dt`] and
529 /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) are both converted
530 /// to the `target` time scale before differencing.
531 /// - **You may need to change the [`Dt`]'s `target` field** before calling if you need
532 /// week/TOW on a particular time scale, e.g. `Scale::GPS`.
533 /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
534 /// if it's not then the output will be incorrect.
535 ///
536 /// ## Returns
537 ///
538 /// A `(week, tow)` pair:
539 ///
540 /// - `week` (`i64`): whole weeks in the elapsed time from
541 /// [`Dt::to_gps`](../struct.Dt.html#method.to_gps). Week 0 starts at the GPS epoch
542 /// (1980-01-06). Before that date the elapsed time is negative and `div_euclid` yields a
543 /// negative week — this is not a broadcast GPS week number, just how the split is defined.
544 /// A plain integer is enough here; it is only a week count, not a duration in attoseconds.
545 /// - `tow` ([`Dt`]): seconds-within-the-week as attoseconds in `0 .. 604800`. Its `scale` and
546 /// `target` are set to this [`Dt`]'s `target` so
547 /// [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow) knows which
548 /// time scale the pair belongs to. `tow` is a [`Dt`] rather than a bare integer so
549 /// sub-second precision and scale are preserved together; the week number alone cannot
550 /// carry either. `div_euclid` / `rem_euclid` are used (not truncating `/`) so TOW stays
551 /// non-negative even when the elapsed time is negative.
552 ///
553 /// ## Examples
554 ///
555 /// ```rust
556 /// use deep_time::{Dt, Scale};
557 ///
558 /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
559 /// let g = x.to_gps_wk_and_tow();
560 /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
561 /// assert_eq!(x, z);
562 ///
563 /// // for conventional GPS-time week/TOW, set target first:
564 /// let g = x.target(Scale::GPS).to_gps_wk_and_tow();
565 /// ```
566 ///
567 /// ## See also
568 ///
569 /// - [`Dt::from_gps_wk_and_tow`](../struct.Dt.html#method.from_gps_wk_and_tow)
570 /// - [`Dt::to_gps`](../struct.Dt.html#method.to_gps)
571 pub const fn to_gps_wk_and_tow(&self) -> (i128, Dt) {
572 let total_attos = self.to_gps().to_attos();
573 let wk = total_attos.div_euclid(ATTOS_PER_WEEK);
574 let tow_attos = total_attos.rem_euclid(ATTOS_PER_WEEK);
575 // was converted to target scale, scale is now target
576 (wk, Dt::new(tow_attos, self.target, self.target))
577 }
578
579 /// Creates a [`Dt`] from a GPS week number and Time of Week (TOW).
580 ///
581 /// Recombines `week` and `tow` into elapsed time since
582 /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH), then passes that to
583 /// [`Dt::from_gps`](../struct.Dt.html#method.from_gps).
584 ///
585 /// This is the inverse of
586 /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow).
587 ///
588 /// ## Important:
589 ///
590 /// - Uses [`Dt::from_gps`](../struct.Dt.html#method.from_gps) internally: the elapsed time
591 /// is interpreted on the `tow` [`Dt`]'s `scale` / `target` fields, and
592 /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted to that
593 /// same scale before the sum.
594 /// - Pass back the `tow` from [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
595 /// unchanged if you want a round trip.
596 ///
597 /// ## Returns
598 ///
599 /// A **TAI** [`Dt`] for the reconstructed instant. Its `target` field is taken from `tow`.
600 ///
601 /// `tow` must be a [`Dt`] (not a bare second count) because
602 /// [`Dt::from_gps`](../struct.Dt.html#method.from_gps) needs both the within-week attoseconds
603 /// and the `scale` / `target` that say which time scale `week` and `tow` were expressed on.
604 /// The week number is multiplied back into attoseconds (`week * 604800` seconds); only `tow`
605 /// carries the scale and sub-week precision needed for the round trip.
606 ///
607 /// `tow` should be in `0 .. 604800` seconds, as returned by
608 /// [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow). Negative `week`
609 /// values only arise from dates before 1980-01-06 (see that function).
610 ///
611 /// ## Examples
612 ///
613 /// ```rust
614 /// use deep_time::{Dt, Scale};
615 ///
616 /// let x = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
617 /// let g = x.to_gps_wk_and_tow();
618 /// let z = Dt::from_gps_wk_and_tow(g.0, g.1);
619 /// assert_eq!(x, z);
620 /// ```
621 ///
622 /// ## See also
623 ///
624 /// - [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
625 /// - [`Dt::from_gps`](../struct.Dt.html#method.from_gps)
626 pub const fn from_gps_wk_and_tow(wk: i128, tow: Dt) -> Dt {
627 let total_attos = wk
628 .saturating_mul(ATTOS_PER_WEEK)
629 .saturating_add(tow.to_attos());
630
631 Self::from_gps(Dt::new(total_attos, tow.scale, tow.target))
632 }
633
634 /// Returns the day of the GPS week (0 = Sunday, 1 = Monday, …, 6 = Saturday).
635 ///
636 /// This value is computed directly from the GPS Time of Week and is
637 /// independent of the Gregorian calendar or civil time.
638 pub const fn to_gps_day_of_wk(&self) -> u8 {
639 let (_, tow) = self.to_gps_wk_and_tow();
640 let sec = tow.to_attos() / ATTOS_PER_SEC_I128;
641
642 (sec / SEC_PER_DAY_I64 as i128) as u8
643 }
644
645 /// Returns this [`Dt`] but as time since the
646 /// [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) on its
647 /// `target` time scale.
648 ///
649 /// ## Important:
650 ///
651 /// - The [`Dt`] first converts itself and the epoch to the time scale of its
652 /// `target` field before doing a raw difference with the epoch.
653 /// - **You may need to change the [`Dt`]'s `target` field** before calling the function
654 /// if you need the timestamp to be on a particular time scale, e.g. `UTC`.
655 /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon epoch,
656 /// if it's not then the output will be incorrect.
657 ///
658 /// ## Returns
659 ///
660 /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
661 /// [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
662 /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
663 /// `Scale::TT` after `.target(Scale::TT)`. The result's `scale` and `target` are both
664 /// set to that same value.
665 ///
666 /// ## Examples
667 ///
668 /// ```rust
669 /// use deep_time::{Dt, Scale};
670 ///
671 /// let cxc = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
672 /// .target(Scale::TT)
673 /// .to_cxcsec()
674 /// .to_sec_f();
675 ///
676 /// // cxcsec 694224032.184 (matches Astropy)
677 /// assert_eq!(cxc, 694224032.184);
678 /// ```
679 ///
680 /// ## See also
681 ///
682 /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
683 #[inline(always)]
684 pub const fn to_cxcsec(&self) -> Dt {
685 self.to_scale_and_diff(Self::CXC_EPOCH, true)
686 }
687
688 /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
689 /// [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
690 ///
691 /// This is the inverse of [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec).
692 ///
693 /// ## Important:
694 ///
695 /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
696 /// [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) — typically the
697 /// return value of [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
698 /// The input's `scale` field says which time scale that count is on — if it
699 /// is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
700 /// included).
701 /// - [`Dt::CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) is converted
702 /// to that same scale before the sum.
703 ///
704 /// ## Returns
705 ///
706 /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
707 /// [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH) — it is attoseconds since
708 /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
709 ///
710 /// ## Examples
711 ///
712 /// ```rust
713 /// use deep_time::{Dt, Scale};
714 ///
715 /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
716 /// let cxc = x.target(Scale::TT).to_cxcsec();
717 /// let roundtrip = Dt::from_cxcsec(cxc);
718 ///
719 /// assert_eq!(roundtrip, x);
720 /// ```
721 ///
722 /// ## See also
723 ///
724 /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
725 /// - [`Dt::from_cxcsec_f`](../struct.Dt.html#method.from_cxcsec_f)
726 #[inline(always)]
727 pub const fn from_cxcsec(elapsed: Dt) -> Dt {
728 Self::from_diff_and_scale(elapsed, Self::CXC_EPOCH, true)
729 }
730
731 /// Convenience wrapper around
732 /// [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
733 /// for a bare floating-point second count.
734 ///
735 /// ## Parameters
736 ///
737 /// - `sec` — seconds elapsed since
738 /// [`CXC_EPOCH`](../struct.Dt.html#associatedconstant.CXC_EPOCH).
739 /// - `on` — which [`Scale`] the count is measured in (for example `Scale::TT` or
740 /// `Scale::UTC`). This becomes the wrapped [`Dt`]'s `scale`;
741 /// [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
742 /// then uses it when turning the elapsed count into an absolute TAI instant
743 /// (including leap-second handling where applicable). Same role as the `scale`
744 /// field on the [`Dt`] you would hand to
745 /// [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
746 /// directly.
747 ///
748 /// ## Examples
749 ///
750 /// ```rust
751 /// use deep_time::{Dt, Scale};
752 ///
753 /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
754 /// let cxc = x.target(Scale::TT).to_cxcsec().to_sec_f();
755 /// let roundtrip = Dt::from_cxcsec_f(cxc, Scale::TT);
756 ///
757 /// assert_eq!(roundtrip.to_cxcsec().to_sec_f(), cxc);
758 /// ```
759 ///
760 /// ## See also
761 ///
762 /// - [`Dt::from_cxcsec`](../struct.Dt.html#method.from_cxcsec)
763 /// - [`Dt::to_cxcsec`](../struct.Dt.html#method.to_cxcsec)
764 #[inline(always)]
765 pub const fn from_cxcsec_f(sec: Real, on: Scale) -> Dt {
766 Self::from_cxcsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
767 }
768
769 /// Returns the elapsed time since the GALEX epoch as a [`Dt`] expressed
770 /// in this object's current `target` scale.
771 ///
772 /// This method can match Astropy’s `Time.galexsec` format. To match
773 /// Astropy output, set `.target(Scale::UTC)`
774 /// before calling.
775 ///
776 /// The GALEX epoch is
777 /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH)
778 /// (same epoch used by GPS time).
779 ///
780 /// ## Important:
781 ///
782 /// - The [`Dt`] first converts itself and the [`Dt::GPS_EPOCH`] to the time
783 /// scale of its `target` field before doing a raw difference with the epoch.
784 /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
785 /// epoch, if it's not then the output will be incorrect.
786 ///
787 /// ## Returns
788 ///
789 /// - A [`Dt`] whose `attos` is how many attoseconds have elapsed since
790 /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
791 /// - The count is on whatever scale sits in this [`Dt`]'s `target` field — for example
792 /// `Scale::UTC` after `.target(Scale::UTC)`. The result's `scale` and `target` are both
793 /// set to that same value.
794 ///
795 /// ## Examples
796 ///
797 /// ```rust
798 /// use deep_time::{Dt, Scale};
799 ///
800 /// let galexsec = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0)
801 /// .target(Scale::UTC)
802 /// .to_galexsec()
803 /// .to_sec_f();
804 ///
805 /// assert_eq!(galexsec, 1261871963.0);
806 /// ```
807 ///
808 /// ## See also
809 ///
810 /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
811 #[inline(always)]
812 pub const fn to_galexsec(&self) -> Dt {
813 self.to_scale_and_diff(Self::GPS_EPOCH, true)
814 }
815
816 /// Creates a **TAI** [`Dt`] from a [`Dt`] that is attoseconds since
817 /// [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
818 ///
819 /// This is the inverse of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec).
820 /// GALEX seconds use the same epoch as GPS time.
821 ///
822 /// ## Important:
823 ///
824 /// - `elapsed` must be a [`Dt`] whose `attos` is how many attoseconds have elapsed since
825 /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — typically the
826 /// return value of [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
827 /// The input's `scale` field says which time scale that count is on — if it
828 /// is `Scale::UTC`, the count is treated as UTC and converted to TAI (leap seconds
829 /// included).
830 /// - [`Dt::GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) is converted
831 /// to that same scale before the sum.
832 ///
833 /// ## Returns
834 ///
835 /// A **TAI** [`Dt`] for the reconstructed instant. Its `attos` is no longer a count since
836 /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH) — it is attoseconds since
837 /// the library epoch (2000-01-01 noon TAI). Its `target` field is taken from `elapsed`.
838 ///
839 /// ## Examples
840 ///
841 /// ```rust
842 /// use deep_time::{Dt, Scale};
843 ///
844 /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
845 /// let galex = x.target(Scale::UTC).to_galexsec();
846 /// let roundtrip = Dt::from_galexsec(galex);
847 ///
848 /// assert_eq!(roundtrip, x);
849 /// ```
850 ///
851 /// ## See also
852 ///
853 /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
854 /// - [`Dt::from_galexsec_f`](../struct.Dt.html#method.from_galexsec_f)
855 #[inline(always)]
856 pub const fn from_galexsec(elapsed: Dt) -> Dt {
857 Self::from_diff_and_scale(elapsed, Self::GPS_EPOCH, true)
858 }
859
860 /// Convenience wrapper around
861 /// [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
862 /// for a bare floating-point second count.
863 ///
864 /// ## Parameters
865 ///
866 /// - `sec` — seconds elapsed since
867 /// [`GPS_EPOCH`](../struct.Dt.html#associatedconstant.GPS_EPOCH).
868 /// - `on` — which [`Scale`] the count is measured in (for example `Scale::UTC` or
869 /// `Scale::TT`). This becomes the wrapped [`Dt`]'s `scale`;
870 /// [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
871 /// then uses it when turning the elapsed count into an absolute TAI instant
872 /// (including leap-second handling where applicable). Same role as the `scale`
873 /// field on the [`Dt`] you would hand to
874 /// [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec) directly.
875 ///
876 /// ## Examples
877 ///
878 /// ```rust
879 /// use deep_time::{Dt, Scale};
880 ///
881 /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
882 /// let galex = x.target(Scale::UTC).to_galexsec().to_sec_f();
883 /// let roundtrip = Dt::from_galexsec_f(galex, Scale::UTC);
884 ///
885 /// assert_eq!(roundtrip, x);
886 /// ```
887 ///
888 /// ## See also
889 ///
890 /// - [`Dt::from_galexsec`](../struct.Dt.html#method.from_galexsec)
891 /// - [`Dt::to_galexsec`](../struct.Dt.html#method.to_galexsec)
892 #[inline(always)]
893 pub const fn from_galexsec_f(sec: Real, on: Scale) -> Dt {
894 Self::from_galexsec(Dt::new(Dt::sec_f_to_attos(sec), on, on))
895 }
896
897 /// Returns the **Julian epoch year** (JYEAR) for this instant.
898 ///
899 /// Julian years are defined as exactly 365.25 days of 86400 SI seconds.
900 /// This is the system used for J2000.0 and many astronomical calculations.
901 ///
902 /// This is **not** the same as
903 /// [`Dt::to_decimalyear`](../struct.Dt.html#method.to_decimalyear),
904 /// which uses the actual length of the specific Gregorian year.
905 ///
906 /// This is the inverse of
907 /// [`Dt::from_jyear`](../struct.Dt.html#method.from_jyear).
908 ///
909 /// ## Important:
910 ///
911 /// - The [`Dt`] first converts itself to the time scale of its `target` field
912 /// before producing a result.
913 /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
914 /// epoch, if it's not then the output will be incorrect.
915 ///
916 /// ## Examples
917 ///
918 /// ```rust
919 /// use deep_time::{Dt, Scale};
920 ///
921 /// let x = Dt::from_ymd(2020, 1, 1, Scale::UTC, 0, 0, 0, 0);
922 ///
923 /// assert_eq!(x.to_jyear(), 2019.9986310746065);
924 /// ```
925 #[inline(always)]
926 pub const fn to_jyear(&self) -> Real {
927 let jd_tt = self.to_jd_f();
928 f!(2000.0) + (jd_tt - JD_2000_2_451_545F) / f!(365.25)
929 }
930
931 /// Inverse of
932 /// [`Dt::to_jyear`](../struct.Dt.html#method.to_jyear).
933 pub const fn from_jyear(jyear: Real, scale: Scale) -> Dt {
934 if jyear.is_nan() {
935 return Self::ZERO;
936 }
937 if jyear.is_infinite() {
938 return if jyear.is_sign_positive() {
939 Self::MAX
940 } else {
941 Self::MIN
942 };
943 }
944
945 let jd = JD_2000_2_451_545F + (jyear - f!(2000.0)) * f!(365.25);
946 Self::from_jd_f(jd, scale)
947 }
948
949 /// Returns the **Besselian epoch year** (BYEAR) for this instant.
950 ///
951 /// Besselian years are an older astronomical convention based on a
952 /// tropical year length of approximately 365.242198781 days.
953 ///
954 /// This is the inverse of
955 /// [`Dt::from_byear`](../struct.Dt.html#method.from_byear).
956 ///
957 /// ## Important:
958 ///
959 /// - The [`Dt`] first converts itself to the time scale of its `target` field
960 /// before producing a result.
961 /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
962 /// epoch, if it's not then the output will be incorrect.
963 ///
964 /// ## Examples
965 ///
966 /// ```rust
967 /// use deep_time::{Dt, Scale};
968 ///
969 /// let x = Dt::from_ymd(2020, 1, 1, Scale::UTC, 0, 0, 0, 0);
970 ///
971 /// assert!((x.to_byear() - 2020.000335739628).abs() < 1e-12);
972 /// ```
973 #[inline]
974 pub const fn to_byear(&self) -> Real {
975 let jd_tt = self.to_jd_f();
976 f!(1900.0) + (jd_tt - f!(2415020.31352)) / f!(365.242198781)
977 }
978
979 /// Inverse of
980 /// [`Dt::to_byear`](../struct.Dt.html#method.to_byear).
981 pub const fn from_byear(byear: Real, scale: Scale) -> Dt {
982 if byear.is_nan() {
983 return Self::ZERO;
984 }
985 if byear.is_infinite() {
986 return if byear.is_sign_positive() {
987 Self::MAX
988 } else {
989 Self::MIN
990 };
991 }
992
993 let jd = f!(2415020.31352) + (byear - f!(1900.0)) * f!(365.242198781);
994 Self::from_jd_f(jd, scale)
995 }
996
997 /// Returns the **decimal year** (Gregorian calendar year + fraction of the year).
998 ///
999 /// This is the direct equivalent of Astropy’s `Time.decimalyear`:
1000 /// - Uses the *actual* length of the specific Gregorian year (365 or 366 days,
1001 /// plus any leap seconds on UTC/UtcSpice/etc.).
1002 /// - Scale-aware (TAI, TT, UTC, TDB, etc.), converts to this [`Dt`]'s target time
1003 /// scale before producing an output.
1004 /// - Exact integer arithmetic for the year boundaries, then a high-precision
1005 /// `to_sec_f` division (lossy only at the final `Real` step, same as Astropy).
1006 ///
1007 /// ## Important:
1008 ///
1009 /// - The [`Dt`] first converts itself to the time scale of its `target` field
1010 /// before producing a result.
1011 /// - This function assumes this [`Dt`] is currently from the 2000-01-01 noon
1012 /// epoch, if it's not then the output will be incorrect.
1013 ///
1014 /// ## Examples
1015 ///
1016 /// ```rust
1017 /// use deep_time::{Dt, Scale};
1018 ///
1019 /// let x = Dt::from_ymd(2020, 1, 1, Scale::TAI, 0, 0, 0, 0);
1020 /// assert_eq!(x.to_decimalyear(), 2020.0);
1021 ///
1022 /// // Also works for negative years
1023 /// let y = Dt::from_ymd(-2000, 1, 1, Scale::TAI, 0, 0, 0, 0);
1024 /// assert_eq!(y.to_decimalyear(), -2000.0);
1025 /// ```
1026 pub fn to_decimalyear(&self) -> Real {
1027 let ymd = self.to_ymd();
1028 let year = ymd.yr;
1029
1030 let start = Self::from_ymd(year, 1, 1, self.target, 0, 0, 0, 0);
1031 let next_start = Self::from_ymd(year.saturating_add(1), 1, 1, self.target, 0, 0, 0, 0);
1032
1033 let elapsed = self.to_diff_raw(start).to_sec_f();
1034 let year_length = next_start.to_diff_raw(start).to_sec_f();
1035
1036 // If start and next_start collapse (extreme / saturated years), avoid / 0.
1037 if year_length == 0.0 {
1038 return f!(year);
1039 }
1040 f!(year) + elapsed / year_length
1041 }
1042}