Skip to main content

deep_time/dt/
gregorian.rs

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