Skip to main content

deep_time/dt/
gregorian.rs

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