Skip to main content

deep_time/dt/
gregorian.rs

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