deep_time/dt/gregorian.rs
1use crate::{
2 ATTOS_PER_SEC, Dt, JD_2000_2_451_545, SEC_PER_DAY_I64, Scale, Weekday, YmdHms, utc::IsLeapSec,
3};
4
5impl Dt {
6 pub(crate) const DAYS_IN_GREGORIAN_MONTHS: [u8; 12] =
7 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
8
9 // pub(crate) const DAYS_IN_GREGORIAN_MONTHS_LEAP_YR: [u8; 12] =
10 // [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
11
12 /// Returns the calendar date and time for this instant.
13 ///
14 /// Converts to this [`Dt`]s `target` time scale using the internal current
15 /// `scale` before producing a result.
16 ///
17 /// ## Returns
18 ///
19 /// A [`YmdHms`] containing:
20 ///
21 /// - `yr`, `mo`, `day` — calendar date
22 /// - `hr` (0–23), `min` (0–59), `sec` (0–60)
23 /// - `attos` — fractional second in attoseconds (`0 ≤ attos < 10¹⁸`)
24 /// - `scale` — time scale that the object is in
25 ///
26 /// ## Leap-second handling
27 ///
28 /// If:
29 ///
30 /// - The [`Dt`]'s `target` time scale is one that uses leap seconds
31 /// (`UTC`, `UtcSpice`, or `UtcHist`)
32 /// - The instant falls exactly on a leap second
33 /// - The objects current time scale is **not** UTC
34 ///
35 /// Then the returned `sec` will be `60`. In every other case `sec` is in the range
36 /// `0..=59`.
37 ///
38 /// The implementation converts internally to TAI before checking leap-second status.
39 ///
40 /// ## Examples
41 ///
42 /// ```rust
43 /// use deep_time::{Dt, Scale};
44 ///
45 /// // `from_ymd` always returns a TAI instant
46 /// let dt = Dt::from_ymd(2024, 6, 15, Scale::UTC, 12, 30, 45, 0);
47 /// let ymd = dt.to_ymd();
48 ///
49 /// assert_eq!(ymd.yr(), 2024);
50 /// assert_eq!(ymd.mo(), 6);
51 /// assert_eq!(ymd.day(), 15);
52 /// assert_eq!(ymd.hr(), 12);
53 /// assert_eq!(ymd.min(), 30);
54 /// assert_eq!(ymd.sec(), 45);
55 /// assert!(ymd.attos() == 0);
56 /// ```
57 ///
58 /// ## See also
59 ///
60 /// - [`Dt::from_ymd`](#method.from_ymd)
61 /// - [`from_ymd!`](../macro.from_ymd.html)
62 pub const fn to_ymd(&self) -> YmdHms {
63 // Attos / seconds are library-epoch offsets (J2000 noon = 0).
64 let on_target = self.to(self.target);
65 let sec_from_j2000 = on_target.to_sec64_floor();
66 let frac = on_target.to_sec_ufrac();
67
68 // Shift so 0 = midnight of 2000-01-01, then split day + time-of-day.
69 let since_midnight_j2000 = sec_from_j2000.saturating_add(43_200);
70 let day_offset = since_midnight_j2000.div_euclid(SEC_PER_DAY_I64);
71 let tod = since_midnight_j2000.rem_euclid(SEC_PER_DAY_I64);
72 let (yr, mo, day) = Self::jd_to_ymd(JD_2000_2_451_545.saturating_add(day_offset));
73
74 let hr = (tod / 3600) as u8;
75 let min = ((tod % 3600) / 60) as u8;
76 let mut sec = (tod % 60) as u8;
77 if self.target.uses_leap_seconds()
78 && let Some(i) = self.to_tai().leap_sec(false)
79 && matches!(i.is_leap_sec, IsLeapSec::Add)
80 {
81 sec += 1
82 }
83
84 YmdHms {
85 yr,
86 mo,
87 day,
88 hr,
89 min,
90 sec,
91 attos: frac,
92 dt: *self,
93 }
94 }
95
96 /// Creates a **TAI** [`Dt`] from a proleptic gregorian date which is assumed to be on
97 /// the provided time scale.
98 ///
99 /// - Equivalent to converting to `TAI` for the provided date. This means for example that
100 /// when using `Scale::UTC` leap seconds are potentially added to the returned [`Dt`].
101 /// - The returned [`Dt`] will have its `scale` field set to `TAI` and its `target` field
102 /// set to the provided time scale argument from this fn. This makes functions such as
103 /// [`Dt::to_ymd`](#method.to_ymd) more ergonomic.
104 ///
105 /// All input components are clamped to their valid ranges:
106 /// - `mo` → 1..=12 **1 based**
107 /// - `day` → 1..=31 **1 based**
108 /// - `hr` → 0..=23 **0 based**
109 /// - `min` → 0..=59 **0 based**
110 /// - `sec` → 0..=60 **0 based** (permits leap seconds)
111 /// - `attos` → 10¹⁸ **0 based** fractional seconds
112 /// (clamped to under 1 second)
113 ///
114 /// ## Examples
115 ///
116 /// ```rust
117 /// # #[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
118 /// # {
119 /// use deep_time::{Dt, Lang, Scale};
120 ///
121 /// // library zero is 2000-01-01 noon TAI
122 /// let tai = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
123 /// assert_eq!(tai, Dt::ZERO);
124 ///
125 /// // utc noon
126 /// let utc = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
127 /// // output with timezone requires jiff-tz feature
128 /// // because from_ymd used Scale::UTC, the output is converted
129 /// // back to UTC before being offset by the timezone
130 /// let s = utc.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
131 /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
132 /// # }
133 /// ```
134 ///
135 /// ## See also
136 ///
137 /// - [`Dt::to_ymd`](#method.to_ymd)
138 /// - [`from_ymd!`](../macro.from_ymd.html)
139 pub const fn from_ymd(
140 yr: i64,
141 mo: u8,
142 day: u8,
143 scale: Scale,
144 hr: u8,
145 min: u8,
146 sec: u8,
147 attos: u64,
148 ) -> Dt {
149 let (mo, day, hr, min, sec) = Dt::clamp_mdhms(yr, mo, day, hr, min, sec);
150 let attos = Dt::clamp_u64(attos, 0, ATTOS_PER_SEC - 1);
151
152 let sec_is_60 = sec == 60;
153 let s = if sec_is_60 { 59 } else { sec };
154
155 // Library-epoch seconds (J2000 noon = 0):
156 // (jd − 2451545) × 86400 + time-of-day offset from noon.
157 let jd = Self::ymd_to_jd(yr, mo, day);
158 let days_since_j2000 = jd.saturating_sub(JD_2000_2_451_545);
159 let seconds_from_noon = (hr as i64 - 12) * 3600 + (min as i64) * 60 + (s as i64);
160 let total_sec = days_since_j2000
161 .saturating_mul(SEC_PER_DAY_I64)
162 .saturating_add(seconds_from_noon);
163
164 let t = Dt::from_sec_and_frac(total_sec as i128, attos as i128, scale, scale).to_tai();
165 if sec_is_60 && scale.uses_leap_seconds() {
166 match Self::leap_sec_using_sec64(total_sec.saturating_add(1), true) {
167 Some(i) if matches!(i.is_leap_sec, IsLeapSec::Add) => t.add_sec(1),
168 _ => t,
169 }
170 } else {
171 t
172 }
173 }
174
175 /// Converts a Julian Day Number (JD) to a proleptic Gregorian calendar date.
176 ///
177 /// - Returns `(year, month, day)` where `month` ∈ [1, 12] and `day` ∈ [1, 31].
178 /// - Inverse of [`Dt::ymd_to_jd`](#method.ymd_to_jd).
179 pub const fn jd_to_ymd(jd: i64) -> (i64, u8, u8) {
180 // Epoch shift can exit i64 near i64::MIN; add 12 eras and fix the year.
181 let (z, year_adj) = match jd.checked_sub(1_721_120) {
182 Some(z) => (z, 0i64),
183 None => (jd + 32_044, -4_800i64),
184 };
185
186 // Floored era index. Avoid `z - 146096` (overflows near i64::MIN).
187 let era = if z >= 0 {
188 z / 146097
189 } else {
190 let q = z / 146097;
191 if z % 146097 == 0 { q } else { q - 1 }
192 };
193 // Widening mul so `era * 146097` cannot wrap for extreme `z`.
194 let doe = (z as i128 - era as i128 * 146097) as i64; // [0, 146096]
195 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
196 let y = yoe + era * 400;
197 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
198 let mp = (5 * doy + 2) / 153; // [0, 11]
199 let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
200 let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
201 let yr = y + if m <= 2 { 1 } else { 0 };
202
203 (yr + year_adj, m as u8, d as u8)
204 }
205
206 /// Computes the Julian Day Number (JD) for a proleptic Gregorian calendar date at noon UT.
207 /// This is the inverse of [`jd_to_ymd`](#method.jd_to_ymd).
208 ///
209 /// ## Arguments
210 ///
211 /// * `yr` - Year (any `i64`; proleptic Gregorian)
212 /// * `mo` - Month (**1-based**: `1` = January, `2` = February, ..., `12` = December)
213 /// * `day` - Day of the month (**1-based**: `1` = first day of the month)
214 ///
215 /// ## Notes
216 ///
217 /// - This function expects **1 based** `mo` and `day`. Passing `mo = 0` or `day = 0` (or other
218 /// out-of-range values) will produce incorrect results as this function does not perform
219 /// value clamping.
220 /// - Does not deal with bad inputs like February with 30 days, does not do any clamping. If you
221 /// need to sanitize a year, month, day input use
222 /// [`Dt::clamp_mdhms`](#method.clamp_mdhms) first.
223 /// - The result is the integer JD corresponding to **noon** on the given date.
224 pub const fn ymd_to_jd(yr: i64, mo: u8, day: u8) -> i64 {
225 let m = mo as i16;
226 let d = day as i16;
227
228 let a = (14 - m) / 12;
229 let m = m + 12 * a - 3;
230 let day_mo = d + (153 * m + 2) / 5;
231
232 // Fast path: shifted year `y = yr + 4800 - a` and `365*y + …` fit in i64.
233 // `Y_LIM = i64::MAX/366` leaves headroom for y/4, day_mo, and the −32045 term.
234 const Y_LIM: i64 = i64::MAX / 366;
235 if let Some(y) = yr.checked_add(4800 - a as i64)
236 && y >= -Y_LIM
237 && y <= Y_LIM
238 {
239 let y4 = y >> 2; // floor(y / 4)
240 let y100 = if y >= 0 { y / 100 } else { (y - 99) / 100 };
241 let y400 = y100 >> 2; // floor(y / 400)
242 return day_mo as i64 + 365 * y + y4 - y100 + y400 - 32045;
243 }
244
245 // Wide path: |yr| near i64 edges (or yr + 4800 overflows i64).
246 let y = yr as i128 + 4800 - a as i128;
247 let y4 = y >> 2;
248 let y100 = if y >= 0 { y / 100 } else { (y - 99) / 100 };
249 let y400 = y100 >> 2;
250 let yr_part = 365 * y + y4 - y100 + y400 - 32045;
251 Dt::to_i64(day_mo as i128 + yr_part)
252 }
253
254 /// Computes the Julian Day Number from a Gregorian year and ordinal day-of-year.
255 #[inline]
256 pub const fn ydoy_to_jd(yr: i64, day_of_yr: u16) -> i64 {
257 let jd_jan1 = Self::ymd_to_jd(yr, 1, 1);
258 jd_jan1.saturating_add(day_of_yr as i64 - 1)
259 }
260
261 /// Converts a Julian Day Number to the corresponding weekday number
262 /// (0 = Sunday … 6 = Saturday).
263 #[inline]
264 pub const fn jd_to_wkday(jd: i64) -> u8 {
265 let rem = ((jd as i128) + 1) % 7;
266 let positive = if rem < 0 { rem + 7 } else { rem };
267 positive as u8
268 }
269
270 /// Computes the Julian Day Number from an ISO week date (Monday-based week).
271 pub const fn iso_wk_to_jd(iso_yr: i64, iso_wk: u8, wkday: Weekday) -> i64 {
272 let jan4_jd = Self::ymd_to_jd(iso_yr, 1, 4);
273 let wd_jan4 = Self::jd_to_wkday(jan4_jd);
274
275 let days_to_monday = {
276 let tmp = (wd_jan4 as i64).saturating_add(6);
277 let rem = tmp % 7;
278 if rem < 0 { rem + 7 } else { rem }
279 };
280
281 let monday_wk1 = jan4_jd.saturating_sub(days_to_monday);
282 let monday_requested =
283 monday_wk1.saturating_add(((iso_wk as i64).saturating_sub(1)).saturating_mul(7));
284
285 monday_requested.saturating_add((wkday.wkday_mon_0_based()) as i64)
286 }
287
288 /// Computes the Julian Day Number from a Sunday-based week-of-year (`%U`).
289 pub const fn wk_sun_to_jd(yr: i64, wk: u8, wkday: Weekday) -> i64 {
290 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
291 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
292
293 let days_to_first_sunday = ((7u8 - wd_jan1) % 7u8) as i64;
294 let first_sunday_jd = jan1_jd.saturating_add(days_to_first_sunday);
295
296 let sunday_of_wk =
297 first_sunday_jd.saturating_add(((wk as i64).saturating_sub(1)).saturating_mul(7));
298
299 sunday_of_wk.saturating_add(wkday.wkday_sun_0_based() as i64)
300 }
301
302 /// Computes the Julian Day Number from a Monday-based week-of-year (`%W`).
303 pub const fn wk_mon_to_jd(yr: i64, wk: u8, wkday: Weekday) -> i64 {
304 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
305 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
306
307 let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
308 let first_monday_jd = jan1_jd.saturating_add(days_to_first_monday);
309
310 let monday_of_wk =
311 first_monday_jd.saturating_add(((wk as i64).saturating_sub(1)).saturating_mul(7));
312
313 monday_of_wk.saturating_add((wkday.wkday_mon_0_based()) as i64)
314 }
315
316 /// Returns `true` if the given year is a Gregorian leap year under proleptic rules.
317 #[inline(always)]
318 pub const fn is_leap_yr(yr: i64) -> bool {
319 (yr & 3 == 0) && ((yr & 15 == 0) || (yr % 25 != 0))
320 }
321
322 /// Returns `true` if the supplied values form a valid proleptic Gregorian calendar date.
323 #[inline]
324 pub const fn is_valid_ymd(yr: i64, mo: u8, day: u8) -> bool {
325 if !matches!(mo, 1..=12) || !matches!(day, 1..=31) {
326 return false;
327 }
328 // 0 = Jan, 1 = Feb, ..., 11 = Dec
329 let days = Self::DAYS_IN_GREGORIAN_MONTHS[(mo - 1) as usize];
330 if mo == 2 && Self::is_leap_yr(yr) {
331 day <= days + 1 // 28 → 29
332 } else {
333 day <= days
334 }
335 }
336
337 /// Returns `true` if the given Gregorian year contains an ISO week 53.
338 pub const fn has_iso_wk_53(yr: i64) -> bool {
339 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
340 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
341 wd_jan1 == 4 || (Self::is_leap_yr(yr) && wd_jan1 == 3)
342 }
343
344 /// Returns the ordinal day of the year (1-based).
345 ///
346 /// January 1 is day `1`; December 31 is day `365` or `366` (in leap years).
347 /// Uses the proleptic Gregorian calendar.
348 pub const fn day_of_yr(&self, ymd: Option<(i64, u8, u8)>) -> u16 {
349 let (yr, mo, day) = if let Some(ymd) = ymd {
350 ymd
351 } else {
352 let g = self.to_ymd();
353 (g.yr, g.mo, g.day)
354 };
355 Self::_day_of_yr(yr, mo, day)
356 }
357
358 pub(crate) const fn _day_of_yr(yr: i64, mo: u8, day: u8) -> u16 {
359 let jd = Self::ymd_to_jd(yr, mo, day);
360 let jd_jan1 = Self::ymd_to_jd(yr, 1, 1);
361
362 let doy = jd.saturating_sub(jd_jan1).saturating_add(1);
363 doy as u16
364 }
365
366 /// Sunday-based week number (`%U` in strftime).
367 ///
368 /// Range: `0..=53`.
369 /// - Week 0 contains the days *before* the first Sunday of the year.
370 /// - Week 1 begins on the first Sunday of the year.
371 ///
372 /// The optional `ymd` and `doy` arguments are performance optimisations
373 /// (same pattern used throughout the file for `day_of_year`, `to_iso_wk_date`, etc.).
374 /// Pass whichever you already have; the function will use the fastest path.
375 pub const fn wk_sun(&self, ymd: Option<(i64, u8, u8)>, doy: Option<u16>) -> u8 {
376 let (yr, _, _) = if let Some(ymd) = ymd {
377 ymd
378 } else {
379 let g = self.to_ymd();
380 (g.yr, g.mo, g.day)
381 };
382 let doy = if let Some(doy) = doy {
383 doy
384 } else {
385 self.day_of_yr(ymd)
386 };
387 Self::_wk_sun(yr, doy)
388 }
389
390 pub(crate) const fn _wk_sun(yr: i64, doy: u16) -> u8 {
391 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
392 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
393 let days_to_first_sunday = (7u8 - wd_jan1) % 7u8;
394 let first_sunday_doy = days_to_first_sunday as u16 + 1;
395 if doy < first_sunday_doy {
396 0
397 } else {
398 let days_since_first_sunday = doy.saturating_sub(first_sunday_doy);
399 ((days_since_first_sunday / 7) + 1) as u8
400 }
401 }
402
403 /// Monday-based week number (`%W` in strftime).
404 ///
405 /// Range: `0..=53`.
406 /// - Week 0 contains the days *before* the first Monday of the year.
407 /// - Week 1 begins on the first Monday of the year.
408 ///
409 /// The optional `ymd` and `doy` arguments are performance optimisations
410 /// (same pattern as `wk_sun`, `day_of_yr`, `to_iso_wk_date`, etc.).
411 pub const fn wk_mon(&self, ymd: Option<(i64, u8, u8)>, doy: Option<u16>) -> u8 {
412 let (yr, _, _) = if let Some(ymd) = ymd {
413 ymd
414 } else {
415 let g = self.to_ymd();
416 (g.yr, g.mo, g.day)
417 };
418 let doy = if let Some(doy) = doy {
419 doy
420 } else {
421 self.day_of_yr(ymd)
422 };
423 Self::_wk_mon(yr, doy)
424 }
425
426 pub(crate) const fn _wk_mon(yr: i64, doy: u16) -> u8 {
427 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
428 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
429 let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
430 let first_monday_doy = days_to_first_monday as u16 + 1;
431 if doy < first_monday_doy {
432 0
433 } else {
434 let days_since_first_monday = doy.saturating_sub(first_monday_doy);
435 ((days_since_first_monday / 7) + 1) as u8
436 }
437 }
438
439 /// Returns the ISO 8601 week date for this `Dt`.
440 ///
441 /// Returns `(iso_year, iso_week, weekday)` where:
442 /// - `iso_year` is the ISO week year (may differ from the Gregorian year near
443 /// year boundaries),
444 /// - `iso_week` is the week number in the range `1..=53`,
445 /// - `weekday` is a [`Weekday`] value (Monday-based week).
446 ///
447 /// Follows the ISO 8601 standard: weeks start on Monday and week 1 is the
448 /// week containing January 4.
449 ///
450 /// The optional `ymd` argument is a performance optimization. If provided,
451 /// it is used directly; otherwise [`to_ymd`](#method.to_ymd)
452 /// is called internally.
453 pub const fn to_iso_wk_date(&self, ymd: Option<(i64, u8, u8)>) -> (i64, u8, Weekday) {
454 let (yr, mo, day) = if let Some(ymd) = ymd {
455 ymd
456 } else {
457 let g = self.to_ymd();
458 (g.yr, g.mo, g.day)
459 };
460 Self::_to_iso_wk_date(yr, mo, day)
461 }
462
463 pub(crate) const fn _to_iso_wk_date(yr: i64, mo: u8, day: u8) -> (i64, u8, Weekday) {
464 let jd = Self::ymd_to_jd(yr, mo, day);
465 let wd = Self::jd_to_wkday(jd);
466 let wd_iso = if wd == 0 { 7 } else { wd };
467
468 let jan4_jd = Self::ymd_to_jd(yr, 1, 4);
469 let wd_jan4 = Self::jd_to_wkday(jan4_jd);
470 let days_to_monday = {
471 let tmp = (wd_jan4 as i64) + 6;
472 let rem = tmp % 7;
473 if rem < 0 { rem + 7 } else { rem }
474 };
475
476 let monday_wk1 = jan4_jd - days_to_monday;
477
478 let days_since = jd - monday_wk1;
479
480 let wk = if days_since < 0 {
481 0u8
482 } else {
483 ((days_since / 7) + 1) as u8
484 };
485
486 let iso_yr = if wk == 0 {
487 yr - 1
488 } else if wk >= 53 && !Self::has_iso_wk_53(yr) {
489 yr + 1
490 } else {
491 yr
492 };
493
494 let iso_wk = if wk == 0 {
495 if Self::has_iso_wk_53(yr - 1) { 53 } else { 52 }
496 } else if (wk == 53 && !Self::has_iso_wk_53(yr)) || wk > 53 {
497 1
498 } else {
499 wk
500 };
501 let wkday_enum = match Weekday::from_monday_1_based(wd_iso) {
502 Some(w) => w,
503 None => Weekday::Monday,
504 };
505
506 (iso_yr, iso_wk, wkday_enum)
507 }
508
509 /// Number of days in a month under proleptic Gregorian rules.
510 #[inline]
511 pub const fn days_in_month(yr: i64, mo: u8) -> u8 {
512 match mo {
513 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
514 4 | 6 | 9 | 11 => 30,
515 2 => {
516 if Self::is_leap_yr(yr) {
517 29
518 } else {
519 28
520 }
521 }
522 _ => 0,
523 }
524 }
525
526 /// Clamps month, day, hour, minutes, and seconds values. Clamps days to what is
527 /// correct for that particular propleptic gregorian month.
528 ///
529 /// For example the year 2000 is a leap year, and February in that year has 29 days
530 /// so the days are clamped to 1-29 in that year, but 1-28 in non-leap years.
531 pub const fn clamp_mdhms(
532 yr: i64,
533 mo: u8,
534 day: u8,
535 hr: u8,
536 min: u8,
537 sec: u8,
538 ) -> (u8, u8, u8, u8, u8) {
539 let mo = Self::clamp_u8(mo, 1, 12);
540 let max_day = Self::days_in_month(yr, mo);
541 let day = Self::clamp_u8(day, 1, max_day);
542 let h = Self::clamp_u8(hr, 0, 23);
543 let m = Self::clamp_u8(min, 0, 59);
544 let s = Self::clamp_u8(sec, 0, 60);
545
546 (mo, day, h, m, s)
547 }
548
549 /// Number of days since 1958-01-01 (proleptic Gregorian) → `(year, month, day)`.
550 /// This is the inverse of [`Dt::ymd_to_days_since_1958`].
551 #[inline]
552 pub const fn days_since_1958_to_ymd(days_since_epoch: i64) -> (i64, u8, u8) {
553 let jd_1958 = Dt::ymd_to_jd(1958, 1, 1);
554 let jd = jd_1958.saturating_add(days_since_epoch);
555 Dt::jd_to_ymd(jd)
556 }
557
558 /// Inverse of [`Dt::days_since_1958_to_ymd`].
559 #[inline]
560 pub const fn ymd_to_days_since_1958(year: i64, month: u8, day: u8) -> i64 {
561 let jd = Dt::ymd_to_jd(year, month, day);
562 let jd_1958 = Dt::ymd_to_jd(1958, 1, 1);
563 jd.saturating_sub(jd_1958)
564 }
565}