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 `UTCSofa`)
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 if self.target.uses_leap_seconds() && tai.leap_sec(false).is_leap_sec {
166 sec += 1;
167 }
168
169 YmdHms {
170 unix_attosec: from_unix_epoch.to_attos(),
171 yr,
172 mo,
173 day,
174 hr,
175 min,
176 sec,
177 attos: frac,
178 scale: self.target,
179 }
180 }
181
182 /// Converts a proleptic Gregorian calendar date+time to a Unix timestamp
183 /// (seconds since 1970-01-01 00:00:00).
184 ///
185 /// - Expects **1 based** `mo` and `day`, and **0 based** `hr`, `min`, and `sec`.
186 /// - Does not perform any time scale conversions.
187 /// - Expects clamped values.
188 pub(crate) const fn ymdhms_to_unix_sec(
189 yr: i64,
190 mo: u8,
191 day: u8,
192 hr: u8,
193 min: u8,
194 sec: u8,
195 ) -> i64 {
196 let jd = Self::ymd_to_jd(yr, mo, day);
197 // 1970-01-01 00:00:00 UTC corresponds to JD 2440588
198 let days_since_1970 = jd.saturating_sub(2440588);
199 let time_of_day = (hr as i64) * 3600 + (min as i64) * 60 + (sec as i64);
200 days_since_1970
201 .saturating_mul(SEC_PER_DAYI64)
202 .saturating_add(time_of_day)
203 }
204
205 /// Converts a Julian Day Number (JD) to a proleptic Gregorian calendar date.
206 ///
207 /// - Returns `(year, month, day)` where `month` ∈ [1, 12] and `day` ∈ [1, 31]
208 /// (standard 1-based Gregorian values).
209 /// - This is the inverse of [`Dt::ymd_to_jd`](../struct.Dt.html#method.ymd_to_jd).
210 /// - Supports the full `i64` range, including negative years and year zero.
211 pub const fn jd_to_ymd(jd: i64) -> (i64, u8, u8) {
212 let j = jd as i128;
213
214 #[inline]
215 const fn floor_div_pos(a: i128, b: i128) -> i128 {
216 if a >= 0 { a / b } else { (a - (b - 1)) / b }
217 }
218
219 let a = j + 32044;
220 let b = floor_div_pos(4 * a + 3, 146097);
221 let c = a - floor_div_pos(b * 146097, 4);
222 let d = floor_div_pos(4 * c + 3, 1461);
223 let e = c - floor_div_pos(1461 * d, 4);
224 let m = floor_div_pos(5 * e + 2, 153);
225 let day = (e - floor_div_pos(153 * m + 2, 5) + 1) as u8;
226 let mo = (m + 3 - 12 * floor_div_pos(m, 10)) as u8;
227 let yr = b * 100 + d - 4800 + floor_div_pos(m, 10);
228
229 (Dt::i128_to_i64(yr), mo, day)
230 }
231
232 /// Computes the Julian Day Number (JD) for a proleptic Gregorian calendar date at noon UT.
233 /// This is the inverse of [`jd_to_ymd`].
234 ///
235 /// ## Arguments
236 ///
237 /// * `yr` - Year (any `i64`; proleptic Gregorian)
238 /// * `mo` - Month (**1-based**: `1` = January, `2` = February, ..., `12` = December)
239 /// * `day` - Day of the month (**1-based**: `1` = first day of the month)
240 ///
241 /// The algorithm matches the standard astronomical convention used throughout the library
242 /// (`ymd_to_jd(2000, 1, 1) == 2451545`).
243 ///
244 /// ## Notes
245 ///
246 /// - This function expects **1 based** `mo` and `day`. Passing `mo = 0` or `day = 0` (or other
247 /// out-of-range values) will produce incorrect results as this function does not perform
248 /// value clamping.
249 /// - Does not deal with bad inputs like February with 30 days, does not do any clamping. If you
250 /// need to sanitize a year, month, day input use
251 /// [`Dt::clamp_mdhms`](../struct.Dt.html#method.clamp_mdhms) first.
252 /// - The result is the integer JD corresponding to **noon** on the given date.
253 #[inline]
254 pub const fn ymd_to_jd(yr: i64, mo: u8, day: u8) -> i64 {
255 let y = yr as i128;
256 let m = mo as i16;
257 let d = day as i16;
258
259 let a = (14 - m) / 12;
260 let y = y + 4800 - a as i128;
261 let m = m + 12 * a - 3;
262
263 let y4 = y >> 2; // floor(y / 4) — arithmetic shift works for negatives
264
265 // floor(y / 100)
266 let y100 = if y >= 0 { y / 100 } else { (y - 99) / 100 };
267
268 let y400 = y100 >> 2; // floor(y / 400)
269
270 let day_mo = d + (153 * m + 2) / 5;
271 let yr_part = 365 * y + y4 - y100 + y400 - 32045;
272
273 Dt::i128_to_i64(day_mo as i128 + yr_part)
274 }
275
276 /// Creates a **TAI** [`Dt`] from a proleptic gregorian date which is assumed to be on
277 /// the provided time scale.
278 ///
279 /// - Equivalent to [`Dt::from`](../struct.Dt.html#method.from) for the provided date.
280 /// Except that conversion is performed prior to adding an extra second if the given
281 /// `sec` is `60`.
282 /// - Returned [`Dt`] will be on the **TAI** time scale.
283 ///
284 /// All input components are clamped to their valid ranges:
285 /// - `mo` → 1..=12 **1 based**
286 /// - `day` → 1..=31 **1 based**
287 /// - `hr` → 0..=23 **0 based**
288 /// - `min` → 0..=59 **0 based**
289 /// - `sec` → 0..=60 **0 based** (permits leap seconds)
290 /// - `attos` → 10¹⁸ **0 based** (clamped to under 1 second)
291 pub const fn from_ymd(
292 yr: i64,
293 mo: u8,
294 day: u8,
295 hr: u8,
296 min: u8,
297 sec: u8,
298 attos: u64,
299 scale: Scale,
300 ) -> Dt {
301 let (mo, day, hr, min, sec) = Dt::clamp_mdhms(yr, mo, day, hr, min, sec);
302 let attos = Dt::clamp_u64(attos, 0, ATTOS_PER_SEC - 1);
303
304 let sec_is_60 = sec == 60;
305 let s_for_unix = if sec_is_60 { 59 } else { sec };
306
307 let unix_sec = Dt::ymdhms_to_unix_sec(yr, mo, day, hr, min, s_for_unix);
308 let unix_attos = Dt::sec_to_attos(unix_sec as i128) + (attos as i128);
309
310 if sec_is_60 && scale.uses_leap_seconds() {
311 let t =
312 Dt::from_diff_and_scale(Dt::new(unix_attos, scale, scale), Dt::UNIX_EPOCH, false);
313
314 let is_leap_sec = leap_sec(t.add_sec(1).to_sec64(), false).is_leap_sec;
315
316 if is_leap_sec { t.add_sec(1) } else { t }
317 } else {
318 Dt::from_diff_and_scale(Dt::new(unix_attos, scale, scale), Dt::UNIX_EPOCH, false)
319 }
320 }
321
322 /// Computes the Julian Day Number from a Gregorian year and ordinal day-of-year.
323 #[inline]
324 pub const fn ydoy_to_jd(yr: i64, day_of_yr: u16) -> i64 {
325 let jd_jan1 = Self::ymd_to_jd(yr, 1, 1);
326 jd_jan1.saturating_add(day_of_yr as i64 - 1)
327 }
328
329 /// Converts a Julian Day Number to the corresponding weekday number (0 = Sunday … 6 = Saturday).
330 #[inline]
331 pub const fn jd_to_wkday(jd: i64) -> u8 {
332 let rem = ((jd as i128) + 1) % 7;
333 let positive = if rem < 0 { rem + 7 } else { rem };
334 positive as u8
335 }
336
337 /// Computes the Julian Day Number from an ISO week date (Monday-based week).
338 pub const fn ymd_to_jd_from_iso_wk(iso_yr: i64, iso_wk: u8, wkday: Weekday) -> i64 {
339 let jan4_jd = Self::ymd_to_jd(iso_yr, 1, 4);
340 let wd_jan4 = Self::jd_to_wkday(jan4_jd);
341
342 let days_to_monday = {
343 let tmp = (wd_jan4 as i64).saturating_add(6);
344 let rem = tmp % 7;
345 if rem < 0 { rem + 7 } else { rem }
346 };
347
348 let monday_wk1 = jan4_jd.saturating_sub(days_to_monday);
349 let monday_requested =
350 monday_wk1.saturating_add(((iso_wk as i64).saturating_sub(1)).saturating_mul(7));
351
352 monday_requested.saturating_add((wkday.wk_mon() - 1) as i64)
353 }
354
355 /// Computes the Julian Day Number from a Sunday-based week-of-year (`%U`).
356 pub const fn ymd_to_jd_from_wk_sun(yr: i64, wk: u8, wkday: Weekday) -> i64 {
357 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
358 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
359
360 let days_to_first_sunday = ((7u8 - wd_jan1) % 7u8) as i64;
361 let first_sunday_jd = jan1_jd.saturating_add(days_to_first_sunday);
362
363 let sunday_of_wk =
364 first_sunday_jd.saturating_add(((wk as i64).saturating_sub(1)).saturating_mul(7));
365
366 sunday_of_wk.saturating_add(wkday.wk_sun() as i64)
367 }
368
369 /// Computes the Julian Day Number from a Monday-based week-of-year (`%W`).
370 pub const fn ymd_to_jd_from_wk_mon(yr: i64, wk: u8, wkday: Weekday) -> i64 {
371 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
372 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
373
374 let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
375 let first_monday_jd = jan1_jd.saturating_add(days_to_first_monday);
376
377 let monday_of_wk =
378 first_monday_jd.saturating_add(((wk as i64).saturating_sub(1)).saturating_mul(7));
379
380 monday_of_wk.saturating_add((wkday.wk_mon() - 1) as i64)
381 }
382
383 /// Returns `true` if the given year is a Gregorian leap year under proleptic rules.
384 #[inline(always)]
385 pub const fn is_leap_yr(yr: i64) -> bool {
386 (yr & 3 == 0) && ((yr & 15 == 0) || (yr % 25 != 0))
387 }
388
389 const DAYS: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
390 /// Returns `true` if the supplied values form a valid proleptic Gregorian calendar date.
391 #[inline]
392 pub const fn is_valid_ymd(yr: i64, mo: u8, day: u8) -> bool {
393 if mo < 1 || mo > 12 || day < 1 {
394 return false;
395 }
396 // 0 = Jan, 1 = Feb, ..., 11 = Dec
397 let days = Self::DAYS[(mo - 1) as usize];
398 if mo == 2 && Self::is_leap_yr(yr) {
399 day <= days + 1 // 28 → 29
400 } else {
401 day <= days
402 }
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_jd = Self::ymd_to_jd(yr, 1, 1);
408 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
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 fn day_of_yr(&self, 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_ymd();
421 (g.yr, g.mo, g.day)
422 };
423 let jd = Self::ymd_to_jd(yr, month, day);
424 let jd_jan1 = Self::ymd_to_jd(yr, 1, 1);
425
426 let doy = jd.saturating_sub(jd_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 fn wk_sun(&self, 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_ymd();
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(ymd)
450 };
451 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
452 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
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 fn wk_mon(&self, 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_ymd();
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(ymd)
482 };
483 let jan1_jd = Self::ymd_to_jd(yr, 1, 1);
484 let wd_jan1 = Self::jd_to_wkday(jan1_jd);
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 fn to_iso_wk_date(&self, ymd: Option<(i64, u8, u8)>) -> (i64, u8, Weekday) {
510 let (yr, month, day) = if let Some(ymd) = ymd {
511 ymd
512 } else {
513 let g = self.to_ymd();
514 (g.yr, g.mo, g.day)
515 };
516 let jd = Self::ymd_to_jd(yr, month, day);
517 let wd = Self::jd_to_wkday(jd);
518 let wd_iso = if wd == 0 { 7 } else { wd };
519
520 let jan4_jd = Self::ymd_to_jd(yr, 1, 4);
521 let wd_jan4 = Self::jd_to_wkday(jan4_jd);
522 let days_to_monday = {
523 let tmp = (wd_jan4 as i64) + 6;
524 let rem = tmp % 7;
525 if rem < 0 { rem + 7 } else { rem }
526 };
527
528 let monday_wk1 = jan4_jd - days_to_monday;
529
530 let days_since = jd - monday_wk1;
531
532 let wk = if days_since < 0 {
533 0u8
534 } else {
535 ((days_since / 7) + 1) as u8
536 };
537
538 let iso_yr = if wk == 0 {
539 yr - 1
540 } else if wk >= 53 && !Self::has_iso_wk_53(yr) {
541 yr + 1
542 } else {
543 yr
544 };
545
546 let iso_wk = if wk == 0 {
547 if Self::has_iso_wk_53(yr - 1) { 53 } else { 52 }
548 } else if (wk == 53 && !Self::has_iso_wk_53(yr)) || wk > 53 {
549 1
550 } else {
551 wk
552 };
553 let wkday_enum = match Weekday::from_monday_one_offset(wd_iso) {
554 Some(w) => w,
555 None => Weekday::Monday,
556 };
557
558 (iso_yr, iso_wk, wkday_enum)
559 }
560
561 /// Number of days in a month under proleptic Gregorian rules.
562 #[inline]
563 pub const fn days_in_month(yr: i64, mo: u8) -> u8 {
564 match mo {
565 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
566 4 | 6 | 9 | 11 => 30,
567 2 => {
568 if Self::is_leap_yr(yr) {
569 29
570 } else {
571 28
572 }
573 }
574 _ => 0,
575 }
576 }
577
578 /// Clamps month, day, hour, minutes, and seconds values. Clamps days to what is
579 /// correct for that particular propleptic gregorian month.
580 ///
581 /// For example the year 2000 is a leap year, and February in that year has 29 days
582 /// so the days are clamped to 1-29 in that year, but 1-28 in non-leap years.
583 pub const fn clamp_mdhms(
584 yr: i64,
585 mo: u8,
586 day: u8,
587 hr: u8,
588 min: u8,
589 sec: u8,
590 ) -> (u8, u8, u8, u8, u8) {
591 let mo = Self::clamp_u8(mo, 1, 12);
592 let max_day = Self::days_in_month(yr, mo);
593 let day = Self::clamp_u8(day, 1, max_day);
594 let h = Self::clamp_u8(hr, 0, 23);
595 let m = Self::clamp_u8(min, 0, 59);
596 let s = Self::clamp_u8(sec, 0, 60);
597
598 (mo, day, h, m, s)
599 }
600}