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