1use std::fmt;
26use std::str::FromStr;
27use std::time::SystemTime;
28use std::time::UNIX_EPOCH;
29
30use strptime::ParseError;
31use strptime::ParseResult;
32use strptime::Parser;
33
34#[cfg(feature = "tz")]
36pub mod tz {
37 pub use tz::TimeZoneRef;
38 pub use tzdb::tz_by_name;
39
40 pub type TzResult<T> = Result<T, ::tz::error::TzError>;
45
46 pub use tzdb::time_zone::africa;
47 pub use tzdb::time_zone::america;
48 pub use tzdb::time_zone::antarctica;
49 pub use tzdb::time_zone::arctic;
50 pub use tzdb::time_zone::asia;
51 pub use tzdb::time_zone::atlantic;
52 pub use tzdb::time_zone::australia;
53 pub use tzdb::time_zone::europe;
54 pub use tzdb::time_zone::indian;
55 pub use tzdb::time_zone::us;
56}
57
58#[macro_export]
70macro_rules! date {
71 ($y:literal-$m:literal-$d:literal) => {{
72 #[allow(clippy::zero_prefixed_literal)]
73 {
74 const { $crate::Date::new($y, $m, $d) }
75 }
76 }};
77}
78
79#[cfg(feature = "diesel-pg")]
80mod diesel_pg;
81#[cfg(feature = "duckdb")]
82mod duckdb;
83mod format;
84pub mod interval;
85pub mod iter;
86#[cfg(feature = "serde")]
87mod serde;
88mod utils;
89mod weekday;
90
91pub use weekday::Weekday;
92
93#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
95#[cfg_attr(feature = "diesel-pg", derive(diesel::AsExpression, diesel::FromSqlRow))]
96#[cfg_attr(feature = "diesel-pg", diesel(sql_type = ::diesel::sql_types::Date))]
97#[repr(transparent)]
98pub struct Date(i32);
99
100impl Date {
101 pub const fn new(year: i16, month: u8, day: u8) -> Self {
119 const MONTH_DAYS: [u8; 12] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
120 assert!(month >= 1 && month <= 12, "Month out-of-bounds");
121 assert!(day >= 1 && day <= MONTH_DAYS[month as usize - 1], "Day out-of-bounds");
122 if month == 2 && day == 29 {
123 assert!(utils::is_leap_year(year), "February 29 only occurs on leap years")
124 }
125
126 let year = year as i32 - if month <= 2 { 1 } else { 0 };
130 let month = month as i32;
131 let day = day as i32;
132 let era: i32 = if year >= 0 { year } else { year - 399 } / 400;
133 let year_of_era = year - era * 400;
134 let day_of_year = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
135 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
136 Self(era * 146097 + day_of_era - 719468)
137 }
138
139 pub const fn from_timestamp(unix_timestamp: i64) -> Self {
164 let day_count = unix_timestamp.div_euclid(86_400) as i32;
165 Self(day_count)
166 }
167
168 #[cfg(feature = "tz")]
170 pub const fn from_timestamp_tz(
171 unix_timestamp: i64, tz: tz::TimeZoneRef<'static>,
172 ) -> tz::TzResult<Self> {
173 match tz.find_local_time_type(unix_timestamp) {
174 Ok(tz) => Ok(Self::from_timestamp(unix_timestamp + tz.ut_offset() as i64)),
175 Err(e) => Err(e),
176 }
177 }
178
179 pub const fn overflowing_new(year: i16, month: u8, day: u8) -> Self {
189 let mut year = year;
190 let mut month = month;
191 let mut day = day;
192
193 while month > 12 {
195 year += 1;
196 month -= 12;
197 }
198 if day == 0 {
199 if month <= 1 {
200 year -= 1;
201 month += 11;
202 } else {
203 month -= 1;
204 }
205 day = utils::days_in_month(year, month);
206 }
207 if month == 0 {
208 year -= 1;
209 month = 12;
210 }
211 while day > utils::days_in_month(year, month) {
212 day -= utils::days_in_month(year, month);
213 month += 1;
214 if month == 13 {
215 year += 1;
216 month = 1;
217 }
218 }
219
220 Self::new(year, month, day)
222 }
223
224 pub fn parse(date_str: impl AsRef<str>, date_fmt: &'static str) -> ParseResult<Date> {
226 let parser = Parser::new(date_fmt);
227 let raw_date = parser.parse(date_str)?.date()?;
228 Ok(raw_date.into())
229 }
230}
231
232impl Date {
233 pub(crate) const fn ymd(&self) -> (i16, u8, u8) {
235 let shifted = self.0 + 719468; let era = if shifted >= 0 { shifted } else { shifted - 146_096 } / 146_097;
240 let doe = shifted - era * 146_097; let year_of_era = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
242 let year = year_of_era + era * 400;
243 let day_of_year = doe - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
244 let mp = (5 * day_of_year + 2) / 153;
245 let day = day_of_year - (153 * mp + 2) / 5 + 1;
246 let month = if mp < 10 { mp + 3 } else { mp - 9 };
247 (year as i16 + if month <= 2 { 1 } else { 0 }, month as u8, day as u8)
248 }
249
250 #[inline]
252 pub const fn year(&self) -> i16 {
253 self.ymd().0
254 }
255
256 #[inline]
260 pub const fn month(&self) -> u8 {
261 self.ymd().1
262 }
263
264 #[inline]
268 pub const fn day(&self) -> u8 {
269 self.ymd().2
270 }
271
272 #[inline]
274 pub const fn day_of_year(&self) -> u16 {
275 (self.0 - Date::new(self.year() - 1, 12, 31).0) as u16
276 }
277
278 pub const fn week(&self) -> u16 {
283 let jan1 = Date::new(self.year(), 1, 1);
284 let first_sunday = jan1.0 + if self.0 % 7 == 3 { 0 } else { 7 } - (self.0 + 4) % 7;
285 ((self.0 - first_sunday).div_euclid(7) + 1) as u16
286 }
287
288 #[inline]
290 pub const fn weekday(&self) -> Weekday {
291 match (self.0 + 4) % 7 {
292 0 => Weekday::Sunday,
293 1 | -6 => Weekday::Monday,
294 2 | -5 => Weekday::Tuesday,
295 3 | -4 => Weekday::Wednesday,
296 4 | -3 => Weekday::Thursday,
297 5 | -2 => Weekday::Friday,
298 6 | -1 => Weekday::Saturday,
299 #[cfg(not(tarpaulin_include))]
300 _ => panic!("Unreachable: Anything % 7 must be within -6 to 6"),
301 }
302 }
303}
304
305impl Date {
306 pub const fn timestamp(&self) -> i64 {
318 self.0 as i64 * 86_400
319 }
320
321 #[cfg(feature = "tz")]
323 pub const fn timestamp_tz(&self, tz: tz::TimeZoneRef<'static>) -> tz::TzResult<i64> {
324 match tz.find_local_time_type(self.timestamp()) {
325 Ok(ts) => Ok(self.timestamp() - ts.ut_offset() as i64),
326 Err(e) => Err(e),
327 }
328 }
329}
330
331impl Date {
332 #[cfg(feature = "tz")]
339 pub fn today() -> Self {
340 let tz = tzdb::local_tz().expect("Could not determine local time zone");
341 let now =
342 now().duration_since(UNIX_EPOCH).expect("system time set prior to 1970").as_secs() as i64;
343 let offset = tz
344 .find_local_time_type(now)
345 .expect("Local time zone lacks information for this timestamp")
346 .ut_offset() as i64;
347 Self::from_timestamp(now + offset)
348 }
349
350 #[cfg(feature = "tz")]
352 pub fn today_tz(tz: tz::TimeZoneRef<'static>) -> tz::TzResult<Self> {
353 let now =
354 now().duration_since(UNIX_EPOCH).expect("system time set prior to 1970").as_secs() as i64;
355 let offset = tz.find_local_time_type(now)?.ut_offset() as i64;
356 Ok(Self::from_timestamp(now + offset))
357 }
358
359 pub fn today_utc() -> Self {
365 let now = now().duration_since(UNIX_EPOCH).expect("system time set prior to 1970").as_secs();
366 Self::from_timestamp(now as i64)
367 }
368}
369
370impl Date {
371 #[doc = include_str!("../support/date-format.md")]
373 #[doc = include_str!("../support/padding.md")]
375 #[doc = include_str!("../support/plain-characters.md")]
377 pub const fn format(self, format_str: &str) -> self::format::FormattedDate<'_> {
378 format::FormattedDate { date: self, format: format_str }
379 }
380}
381
382impl Date {
383 pub fn iter_through(&self, end: Date) -> iter::DateIterator {
386 iter::DateIterator::new(self, end)
387 }
388}
389
390impl Date {
391 pub const MAX: Self = Date::new(32767, 12, 31);
393 pub const MIN: Self = Date::new(-32768, 1, 1);
395}
396
397#[cfg(feature = "easter")]
398impl Date {
399 pub const fn easter(year: i16) -> Self {
401 assert!(year >= 1583 || year <= 9999, "Year out of bounds");
402 let a = year % 19;
403 let b = year / 100;
404 let c = year % 100;
405 let d = b / 4;
406 let e = b % 4;
407 let f = (b + 8) / 25;
408 let g = (b - f + 1) / 3;
409 let h = (19 * a + b - d - g + 15) % 30;
410 let i = c / 4;
411 let j = c % 4;
412 let k = (32 + 2 * e + 2 * i - h - j) % 7;
413 let l = (a + 11 * h + 22 * k) / 451;
414 let month = (h + k - 7 * l + 114) / 31;
415 let day = (h + k - 7 * l + 114) % 31 + 1;
416 Self::new(year, month as u8, day as u8)
417 }
418}
419
420impl fmt::Debug for Date {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 write!(f, "{}", self.format("%Y-%m-%d"))
423 }
424}
425
426impl fmt::Display for Date {
427 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 write!(f, "{}", self.format("%Y-%m-%d"))
429 }
430}
431
432#[cfg(feature = "log")]
433impl log::kv::ToValue for Date {
434 fn to_value(&self) -> log::kv::Value<'_> {
435 log::kv::Value::from_debug(self)
436 }
437}
438
439impl FromStr for Date {
440 type Err = ParseError;
441
442 fn from_str(s: &str) -> ParseResult<Self> {
443 Self::parse(s, "%Y-%m-%d")
444 }
445}
446
447impl From<strptime::RawDate> for Date {
448 fn from(value: strptime::RawDate) -> Self {
449 Self::new(value.year(), value.month(), value.day())
450 }
451}
452
453#[cfg(not(test))]
454fn now() -> SystemTime {
455 SystemTime::now()
456}
457
458#[cfg(test)]
459use tests::now;
460
461#[cfg(test)]
462mod tests {
463 use std::cell::RefCell;
464
465 use assert2::check;
466
467 use super::*;
468
469 thread_local! {
470 static MOCK_TIME: RefCell<Option<SystemTime>> = const { RefCell::new(None) };
471 }
472
473 fn set_now(time: SystemTime) {
474 MOCK_TIME.with(|cell| *cell.borrow_mut() = Some(time));
475 }
476
477 fn clear_now() {
478 MOCK_TIME.with(|cell| *cell.borrow_mut() = None);
479 }
480
481 pub(super) fn now() -> SystemTime {
482 MOCK_TIME.with(|cell| cell.borrow().as_ref().cloned().unwrap_or_else(SystemTime::now))
483 }
484
485 #[test]
486 fn test_internal_repr() {
487 check!(date! { 1969-12-31 }.0 == -1);
488 check!(date! { 1970-01-01 }.0 == 0);
489 check!(date! { 1970-01-02 }.0 == 1);
490 }
491
492 #[test]
493 fn test_ymd_readback() {
494 for year in [2020, 2022, 2100] {
495 for month in 1..=12 {
496 let days = match month {
497 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
498 4 | 6 | 9 | 11 => 30,
499 2 => match utils::is_leap_year(year) {
500 true => 29,
501 false => 28,
502 },
503 #[cfg(not(tarpaulin_include))]
504 _ => panic!("Unreachable"),
505 };
506 for day in 1..=days {
507 let date = Date::new(year, month, day);
508 check!(date.year() == year);
509 check!(date.month() == month);
510 check!(date.day() == day);
511 }
512 }
513 }
514 }
515
516 #[test]
517 #[should_panic]
518 fn test_overflow_panic_day() {
519 Date::new(2012, 4, 31);
520 }
521
522 #[test]
523 #[should_panic]
524 fn test_overflow_panic_month() {
525 Date::new(2012, 13, 1);
526 }
527
528 #[test]
529 #[should_panic]
530 fn test_overflow_panic_ly() {
531 Date::new(2100, 2, 29);
532 }
533
534 #[test]
535 #[allow(clippy::zero_prefixed_literal)]
536 fn test_ymd_overflow() {
537 macro_rules! overflows_to {
538 ($y1:literal-$m1:literal-$d1:literal
539 == $y2:literal-$m2:literal-$d2:literal) => {
540 let date1 = Date::overflowing_new($y1, $m1, $d1);
541 let date2 = Date::new($y2, $m2, $d2);
542 check!(date1 == date2);
543 };
544 }
545 overflows_to! { 2022-01-32 == 2022-02-01 };
546 overflows_to! { 2022-02-29 == 2022-03-01 };
547 overflows_to! { 2022-03-32 == 2022-04-01 };
548 overflows_to! { 2022-04-31 == 2022-05-01 };
549 overflows_to! { 2022-05-32 == 2022-06-01 };
550 overflows_to! { 2022-06-31 == 2022-07-01 };
551 overflows_to! { 2022-07-32 == 2022-08-01 };
552 overflows_to! { 2022-08-32 == 2022-09-01 };
553 overflows_to! { 2022-09-31 == 2022-10-01 };
554 overflows_to! { 2022-10-32 == 2022-11-01 };
555 overflows_to! { 2022-11-31 == 2022-12-01 };
556 overflows_to! { 2022-12-32 == 2023-01-01 };
557 overflows_to! { 2022-00-00 == 2021-11-30 };
558 overflows_to! { 2022-01-00 == 2021-12-31 };
559 overflows_to! { 2022-02-00 == 2022-01-31 };
560 overflows_to! { 2022-03-00 == 2022-02-28 };
561 overflows_to! { 2022-04-00 == 2022-03-31 };
562 overflows_to! { 2022-05-00 == 2022-04-30 };
563 overflows_to! { 2022-06-00 == 2022-05-31 };
564 overflows_to! { 2022-07-00 == 2022-06-30 };
565 overflows_to! { 2022-08-00 == 2022-07-31 };
566 overflows_to! { 2022-09-00 == 2022-08-31 };
567 overflows_to! { 2022-10-00 == 2022-09-30 };
568 overflows_to! { 2022-11-00 == 2022-10-31 };
569 overflows_to! { 2022-12-00 == 2022-11-30 };
570 overflows_to! { 2020-02-30 == 2020-03-01 };
571 overflows_to! { 2020-03-00 == 2020-02-29 };
572 overflows_to! { 2022-01-45 == 2022-02-14 };
573 overflows_to! { 2022-13-15 == 2023-01-15 };
574 overflows_to! { 2022-00-15 == 2021-12-15 };
575 }
576
577 #[test]
578 fn test_display() {
579 check!(date! { 2012-04-21 }.to_string() == "2012-04-21");
580 check!(format!("{:?}", date! { 2012-04-21 }) == "2012-04-21");
581 }
582
583 #[test]
584 fn test_week() {
585 check!(date! { 2022-01-01 }.week() == 0); check!(date! { 2022-01-02 }.week() == 1); check!(date! { 2023-01-01 }.week() == 1); check!(date! { 2023-12-31 }.week() == 53); check!(date! { 2024-01-01 }.week() == 0); check!(date! { 2024-01-07 }.week() == 1); check!(date! { 2024-01-08 }.week() == 1); check!(date! { 2024-01-14 }.week() == 2); }
594
595 #[test]
596 fn test_today() {
597 set_now(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86_400));
598 check!(Date::today_utc() == date! { 1970-01-02 });
599 clear_now();
600 }
601
602 #[cfg(feature = "tz")]
603 #[test]
604 fn test_today_tz() -> tz::TzResult<()> {
605 set_now(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86_400));
606 check!([date! { 1970-01-01 }, date! { 1970-01-02 }].contains(&Date::today()));
607 check!(Date::today_tz(tz::us::EASTERN)? == date! { 1970-01-01 });
608 clear_now();
609 Ok(())
610 }
611
612 #[cfg(feature = "tz")]
613 #[test]
614 fn test_timestamp_tz() -> tz::TzResult<()> {
615 check!(Date::from_timestamp_tz(1335020400, tz::us::EASTERN)? == date! { 2012-04-21 });
616 check!(Date::from_timestamp_tz(0, tz::us::PACIFIC)? == date! { 1969-12-31 });
617 check!(date! { 2012-04-21 }.timestamp_tz(tz::us::EASTERN)? == 1334980800);
618 Ok(())
619 }
620
621 #[cfg(feature = "easter")]
622 #[test]
623 fn test_easter() {
624 check!(Date::easter(2013) == date! { 2013-03-31 });
625 check!(Date::easter(2014) == date! { 2014-04-20 });
626 check!(Date::easter(2015) == date! { 2015-04-05 });
627 check!(Date::easter(2016) == date! { 2016-03-27 });
628 check!(Date::easter(2017) == date! { 2017-04-16 });
629 check!(Date::easter(2018) == date! { 2018-04-01 });
630 check!(Date::easter(2019) == date! { 2019-04-21 });
631 check!(Date::easter(2020) == date! { 2020-04-12 });
632 check!(Date::easter(2021) == date! { 2021-04-04 });
633 check!(Date::easter(2022) == date! { 2022-04-17 });
634 check!(Date::easter(2023) == date! { 2023-04-09 });
635 check!(Date::easter(2024) == date! { 2024-03-31 });
636 check!(Date::easter(2025) == date! { 2025-04-20 });
637 check!(Date::easter(2026) == date! { 2026-04-05 });
638 check!(Date::easter(2027) == date! { 2027-03-28 });
639 check!(Date::easter(2028) == date! { 2028-04-16 });
640 check!(Date::easter(2029) == date! { 2029-04-01 });
641 check!(Date::easter(2030) == date! { 2030-04-21 });
642 check!(Date::easter(2031) == date! { 2031-04-13 });
643 check!(Date::easter(2032) == date! { 2032-03-28 });
644 check!(Date::easter(2033) == date! { 2033-04-17 });
645 check!(Date::easter(2034) == date! { 2034-04-09 });
646 check!(Date::easter(2035) == date! { 2035-03-25 });
647 }
648
649 #[test]
650 fn test_from_str() -> ParseResult<()> {
651 check!("2012-04-21".parse::<Date>()? == date! { 2012-04-21 });
652 check!("2012-4-21".parse::<Date>().is_err());
653 check!("04/21/2012".parse::<Date>().is_err());
654 check!("12-04-21".parse::<Date>().is_err());
655 check!("foo".parse::<Date>().map_err(|e| e.to_string()).unwrap_err().contains("foo"));
656 Ok(())
657 }
658
659 #[test]
660 fn test_parse() -> ParseResult<()> {
661 check!(Date::parse("04/21/12", "%m/%d/%y")? == date! { 2012-04-21 });
662 check!(Date::parse("Saturday, April 21, 2012", "%A, %B %-d, %Y")? == date! { 2012-04-21 });
663 Ok(())
664 }
665}