Skip to main content

deep_time/dt/
gregorian.rs

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