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 /// Returns `true` if the given year is a Gregorian leap year under proleptic rules.
310 #[inline]
311 pub const fn is_leap_yr(yr: i64) -> bool {
312 yr % 4 == 0 && (yr % 100 != 0 || yr % 400 == 0)
313 }
314
315 /// Creates a **TAI** [`Dt`] from a proleptic gregorian date which is assumed to be on
316 /// the provided time scale.
317 ///
318 /// - Equivalent to [`Dt::from`](../struct.Dt.html#method.from) for the provided date.
319 /// - Returned [`Dt`] will be on the **TAI** time scale.
320 ///
321 /// All input components are clamped to their valid ranges:
322 /// - `mo` → 1..=12 **1 based**
323 /// - `day` → 1..=31 **1 based**
324 /// - `hr` → 0..=23 **0 based**
325 /// - `min` → 0..=59 **0 based**
326 /// - `sec` → 0..=60 **0 based** (permits leap seconds)
327 /// - `attos` → values ≥ 10¹⁸ are carried into the seconds field
328 ///
329 /// ### Notes:
330 ///
331 /// - Does not perform validation on leap seconds. If 60 seconds are
332 /// provided then an extra second will be added to the resulting [`Dt`].
333 pub const fn from_ymdhms_on(
334 yr: i64,
335 mo: u8,
336 day: u8,
337 hr: u8,
338 min: u8,
339 sec: u8,
340 attos: u64,
341 scale: Scale,
342 ) -> Self {
343 let (mo, day, hr, min, sec) = Self::clamp_mdhms(yr, mo, day, hr, min, sec);
344 let carried_sec = (attos / ATTOS_PER_SEC) as i64;
345 let final_attos = attos % ATTOS_PER_SEC;
346
347 let is_exact_leap_second = sec == 60 && carried_sec == 0;
348 let s_for_unix = if is_exact_leap_second { 59 } else { sec };
349
350 let civil_unix_sec =
351 Self::ymdhms_to_unix_sec(yr, mo, day, hr, min, s_for_unix) + carried_sec;
352
353 let tp =
354 Self::from_diff_and_scale(Dt::new(civil_unix_sec, final_attos), Dt::UNIX_EPOCH, scale);
355 if is_exact_leap_second {
356 Dt::new(tp.sec.saturating_add(1), tp.attos)
357 } else {
358 tp
359 }
360 }
361
362 /// Creates a **TAI** [`Dt`] from a proleptic gregorian date which is assumed to be on
363 /// the provided time scale.
364 ///
365 /// See [`Dt::from_ymdhms_on`](../struct.Dt.html#method.from_ymdhms_on).
366 #[inline]
367 pub const fn from_ymd_on(yr: i64, mo: u8, day: u8, scale: Scale) -> Self {
368 Dt::from_ymdhms_on(yr, mo, day, 0, 0, 0, 0, scale)
369 }
370
371 /// Creates a **TAI** [`Dt`] from a proleptic gregorian **UTC** date.
372 ///
373 /// See [`Dt::from_ymdhms_on`](../struct.Dt.html#method.from_ymdhms_on).
374 #[inline]
375 pub const fn from_ymdhms(
376 yr: i64,
377 mo: u8,
378 day: u8,
379 hr: u8,
380 min: u8,
381 sec: u8,
382 attos: u64,
383 ) -> Self {
384 Dt::from_ymdhms_on(yr, mo, day, hr, min, sec, attos, Scale::UTC)
385 }
386
387 /// Creates a **TAI** [`Dt`] from a proleptic gregorian **UTC** date.
388 ///
389 /// See [`Dt::from_ymdhms_on`](../struct.Dt.html#method.from_ymdhms_on).
390 #[inline]
391 pub const fn from_ymd(yr: i64, mo: u8, day: u8) -> Self {
392 Dt::from_ymdhms_on(yr, mo, day, 0, 0, 0, 0, Scale::UTC)
393 }
394
395 /// Computes the Julian Day Number from a Gregorian year and ordinal day-of-year.
396 #[inline]
397 pub const fn ydoy_to_jd(yr: i64, day_of_yr: u16) -> i64 {
398 let jd_jan1 = Self::ymd_to_jd(yr, 1, 1);
399 jd_jan1.saturating_add(day_of_yr as i64 - 1)
400 }
401
402 /// Converts a Julian Day Number to the corresponding weekday number (0 = Sunday … 6 = Saturday).
403 #[inline]
404 pub const fn jd_to_wkday(jd: i64) -> u8 {
405 let rem = ((jd as i128) + 1) % 7;
406 let positive = if rem < 0 { rem + 7 } else { rem };
407 positive as u8
408 }
409
410 /// Computes the Julian Day Number from an ISO week date (Monday-based week).
411 pub const fn ymd_to_jd_from_iso_wk(iso_yr: i64, iso_wk: u8, wkday: Weekday) -> i64 {
412 let jan4_jd = Self::ymd_to_jd(iso_yr, 1, 4);
413 let wd_jan4 = Self::jd_to_wkday(jan4_jd);
414
415 let days_to_monday = {
416 let tmp = (wd_jan4 as i64).saturating_add(6);
417 let rem = tmp % 7;
418 if rem < 0 { rem + 7 } else { rem }
419 };
420
421 let monday_wk1 = jan4_jd.saturating_sub(days_to_monday);
422 let monday_requested =
423 monday_wk1.saturating_add(((iso_wk as i64).saturating_sub(1)).saturating_mul(7));
424
425 monday_requested.saturating_add((wkday.wk_mon() - 1) as i64)
426 }
427
428 /// Computes the Julian Day Number from a Sunday-based week-of-year (`%U`).
429 pub const fn ymd_to_jd_from_wk_sun(yr: i64, wk: u8, wkday: Weekday) -> i64 {
430 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
431 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
432
433 let days_to_first_sunday = ((7u8 - wd_jan1) % 7u8) as i64;
434 let first_sunday_jd = jan1_jd.saturating_add(days_to_first_sunday);
435
436 let sunday_of_wk =
437 first_sunday_jd.saturating_add(((wk as i64).saturating_sub(1)).saturating_mul(7));
438
439 sunday_of_wk.saturating_add(wkday.wk_sun() as i64)
440 }
441
442 /// Computes the Julian Day Number from a Monday-based week-of-year (`%W`).
443 pub const fn ymd_to_jd_from_wk_mon(yr: i64, wk: u8, wkday: Weekday) -> i64 {
444 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
445 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
446
447 let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
448 let first_monday_jd = jan1_jd.saturating_add(days_to_first_monday);
449
450 let monday_of_wk =
451 first_monday_jd.saturating_add(((wk as i64).saturating_sub(1)).saturating_mul(7));
452
453 monday_of_wk.saturating_add((wkday.wk_mon() - 1) as i64)
454 }
455
456 /// Returns `true` if the supplied values form a valid proleptic Gregorian calendar date.
457 pub const fn is_valid_ymd(yr: i64, mo: u8, day: u8) -> bool {
458 if mo < 1 || mo > 12 || day < 1 {
459 return false;
460 }
461 let days = match mo {
462 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31u8,
463 4 | 6 | 9 | 11 => 30u8,
464 2 => {
465 if Self::is_leap_yr(yr) {
466 29
467 } else {
468 28
469 }
470 }
471 _ => return false,
472 };
473 day <= days
474 }
475
476 /// Returns `true` if the given Gregorian year contains an ISO week 53.
477 pub const fn has_iso_wk_53(yr: i64) -> bool {
478 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
479 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
480 wd_jan1 == 4 || (Self::is_leap_yr(yr) && wd_jan1 == 3)
481 }
482
483 /// Returns the ordinal day of the year (1-based).
484 ///
485 /// January 1 is day `1`; December 31 is day `365` or `366` (in leap years).
486 /// Uses the proleptic Gregorian calendar.
487 pub const fn day_of_yr(&self, current: Scale, ymd: Option<(i64, u8, u8)>) -> u16 {
488 let (yr, month, day) = if let Some(ymd) = ymd {
489 ymd
490 } else {
491 let g = self.to_ymdhms(current);
492 (g.yr, g.mo, g.day)
493 };
494 let jd = Self::ymd_to_jd(yr, month, day);
495 let jd_jan1 = Self::ymd_to_jd(yr, 1, 1);
496
497 let doy = jd.saturating_sub(jd_jan1).saturating_add(1);
498 doy as u16
499 }
500
501 /// Sunday-based week number (`%U` in strftime).
502 ///
503 /// Range: `0..=53`.
504 /// - Week 0 contains the days *before* the first Sunday of the year.
505 /// - Week 1 begins on the first Sunday of the year.
506 ///
507 /// The optional `ymd` and `doy` arguments are performance optimisations
508 /// (same pattern used throughout the file for `day_of_year`, `to_iso_wk_date`, etc.).
509 /// Pass whichever you already have; the function will use the fastest path.
510 pub const fn wk_sun(&self, current: Scale, ymd: Option<(i64, u8, u8)>, doy: Option<u16>) -> u8 {
511 let (yr, _, _) = if let Some(ymd) = ymd {
512 ymd
513 } else {
514 let g = self.to_ymdhms(current);
515 (g.yr, g.mo, g.day)
516 };
517 let doy = if let Some(doy) = doy {
518 doy
519 } else {
520 self.day_of_yr(current, ymd)
521 };
522 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
523 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
524 let days_to_first_sunday = (7u8 - wd_jan1) % 7u8;
525 let first_sunday_doy = days_to_first_sunday as u16 + 1;
526 if doy < first_sunday_doy {
527 0
528 } else {
529 let days_since_first_sunday = doy.saturating_sub(first_sunday_doy);
530 ((days_since_first_sunday / 7) + 1) as u8
531 }
532 }
533
534 /// Monday-based week number (`%W` in strftime).
535 ///
536 /// Range: `0..=53`.
537 /// - Week 0 contains the days *before* the first Monday of the year.
538 /// - Week 1 begins on the first Monday of the year.
539 ///
540 /// The optional `ymd` and `doy` arguments are performance optimisations
541 /// (same pattern as `wk_sun`, `day_of_yr`, `to_iso_wk_date`, etc.).
542 pub const fn wk_mon(&self, current: Scale, ymd: Option<(i64, u8, u8)>, doy: Option<u16>) -> u8 {
543 let (yr, _, _) = if let Some(ymd) = ymd {
544 ymd
545 } else {
546 let g = self.to_ymdhms(current);
547 (g.yr, g.mo, g.day)
548 };
549 let doy = if let Some(doy) = doy {
550 doy
551 } else {
552 self.day_of_yr(current, ymd)
553 };
554 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
555 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
556 let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
557 let first_monday_doy = days_to_first_monday as u16 + 1;
558 if doy < first_monday_doy {
559 0
560 } else {
561 let days_since_first_monday = doy.saturating_sub(first_monday_doy);
562 ((days_since_first_monday / 7) + 1) as u8
563 }
564 }
565
566 /// Returns the ISO 8601 week date for this `Dt`.
567 ///
568 /// Returns `(iso_year, iso_week, weekday)` where:
569 /// - `iso_year` is the ISO week year (may differ from the Gregorian year near
570 /// year boundaries),
571 /// - `iso_week` is the week number in the range `1..=53`,
572 /// - `weekday` is a [`Weekday`] value (Monday-based week).
573 ///
574 /// Follows the ISO 8601 standard: weeks start on Monday and week 1 is the
575 /// week containing January 4.
576 ///
577 /// The optional `ymd` argument is a performance optimization. If provided,
578 /// it is used directly; otherwise [`to_gregorian_ymd`](Self::to_gregorian_ymd)
579 /// is called internally.
580 pub const fn to_iso_wk_date(
581 &self,
582 current: Scale,
583 ymd: Option<(i64, u8, u8)>,
584 ) -> (i64, u8, Weekday) {
585 let (yr, month, day) = if let Some(ymd) = ymd {
586 ymd
587 } else {
588 let g = self.to_ymdhms(current);
589 (g.yr, g.mo, g.day)
590 };
591 let jd = Self::ymd_to_jd(yr, month, day);
592 let wd = Self::jd_to_wkday(jd);
593 let wd_iso = if wd == 0 { 7 } else { wd };
594
595 let jan4_jd = Self::ymd_to_jd(yr, 1, 4);
596 let wd_jan4 = Self::jd_to_wkday(jan4_jd);
597 let days_to_monday = {
598 let tmp = (wd_jan4 as i64) + 6;
599 let rem = tmp % 7;
600 if rem < 0 { rem + 7 } else { rem }
601 };
602
603 let monday_wk1 = jan4_jd - days_to_monday;
604
605 let days_since = jd - monday_wk1;
606
607 let wk = if days_since < 0 {
608 0u8
609 } else {
610 ((days_since / 7) + 1) as u8
611 };
612
613 let iso_yr = if wk == 0 {
614 yr - 1
615 } else if wk >= 53 && !Self::has_iso_wk_53(yr) {
616 yr + 1
617 } else {
618 yr
619 };
620
621 let iso_wk = if wk == 0 {
622 if Self::has_iso_wk_53(yr - 1) { 53 } else { 52 }
623 } else if (wk == 53 && !Self::has_iso_wk_53(yr)) || wk > 53 {
624 1
625 } else {
626 wk
627 };
628 let wkday_enum = match Weekday::from_monday_one_offset(wd_iso) {
629 Some(w) => w,
630 None => Weekday::Monday,
631 };
632
633 (iso_yr, iso_wk, wkday_enum)
634 }
635
636 /// Number of days in a month under proleptic Gregorian rules.
637 #[inline]
638 pub const fn days_in_month(yr: i64, mo: u8) -> u8 {
639 match mo {
640 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
641 4 | 6 | 9 | 11 => 30,
642 2 => {
643 if Self::is_leap_yr(yr) {
644 29
645 } else {
646 28
647 }
648 }
649 _ => 0,
650 }
651 }
652
653 /// Clamps month, day, hour, minutes, and seconds values. Clamps days to what is
654 /// correct for that particular propleptic gregorian month.
655 ///
656 /// For example the year 2000 is a leap year, and February in that year has 29 days
657 /// so the days are clamped to 1-29 in that year, but 1-28 in non-leap years.
658 pub const fn clamp_mdhms(
659 yr: i64,
660 mo: u8,
661 day: u8,
662 hr: u8,
663 min: u8,
664 sec: u8,
665 ) -> (u8, u8, u8, u8, u8) {
666 let mo = Self::clamp_u8(mo, 1, 12);
667 let max_day = Self::days_in_month(yr, mo);
668 let day = Self::clamp_u8(day, 1, max_day);
669 let h = Self::clamp_u8(hr, 0, 23);
670 let m = Self::clamp_u8(min, 0, 59);
671 let s = Self::clamp_u8(sec, 0, 60);
672
673 (mo, day, h, m, s)
674 }
675}