1use std::cmp::Ordering;
26use std::fmt;
27use std::str::FromStr;
28use std::time::SystemTime;
29use std::time::UNIX_EPOCH;
30
31use strptime::ParseError;
32use strptime::ParseResult;
33use strptime::Parser;
34
35#[cfg(feature = "tz")]
37pub mod tz {
38 pub use tz::TimeZoneRef;
39 pub use tzdb::tz_by_name;
40
41 pub type TzResult<T> = Result<T, ::tz::error::TzError>;
46
47 pub use tzdb::time_zone::africa;
48 pub use tzdb::time_zone::america;
49 pub use tzdb::time_zone::antarctica;
50 pub use tzdb::time_zone::arctic;
51 pub use tzdb::time_zone::asia;
52 pub use tzdb::time_zone::atlantic;
53 pub use tzdb::time_zone::australia;
54 pub use tzdb::time_zone::europe;
55 pub use tzdb::time_zone::indian;
56 pub use tzdb::time_zone::us;
57}
58
59#[macro_export]
71macro_rules! date {
72 ($y:literal-$m:literal-$d:literal) => {{
73 #[allow(clippy::zero_prefixed_literal)]
74 {
75 const { $crate::Date::new($y, $m, $d) }
76 }
77 }};
78}
79
80#[cfg(feature = "diesel-pg")]
81mod diesel_pg;
82#[cfg(feature = "duckdb")]
83mod duckdb;
84mod format;
85pub mod interval;
86pub mod iter;
87#[cfg(feature = "serde")]
88mod serde;
89mod utils;
90mod weekday;
91
92pub use weekday::Weekday;
93
94#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
96#[cfg_attr(feature = "diesel-pg", derive(diesel::AsExpression, diesel::FromSqlRow))]
97#[cfg_attr(feature = "diesel-pg", diesel(sql_type = ::diesel::sql_types::Date))]
98#[repr(transparent)]
99pub struct Date(i32);
100
101impl Date {
102 pub const fn new(year: i16, month: u8, day: u8) -> Self {
120 const MONTH_DAYS: [u8; 12] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
121 assert!(month >= 1 && month <= 12, "Month out-of-bounds");
122 assert!(day >= 1 && day <= MONTH_DAYS[month as usize - 1], "Day out-of-bounds");
123 if month == 2 && day == 29 {
124 assert!(utils::is_leap_year(year), "February 29 only occurs on leap years")
125 }
126
127 let year = year as i32 - if month <= 2 { 1 } else { 0 };
131 let month = month as i32;
132 let day = day as i32;
133 let era: i32 = if year >= 0 { year } else { year - 399 } / 400;
134 let year_of_era = year - era * 400;
135 let day_of_year = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
136 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
137 Self(era * 146097 + day_of_era - 719468)
138 }
139
140 pub const fn from_timestamp(unix_timestamp: i64) -> Self {
165 let day_count = unix_timestamp.div_euclid(86_400) as i32;
166 Self(day_count)
167 }
168
169 #[cfg(feature = "ord")]
181 pub const fn from_ord(ord: i32) -> Self {
182 Self(ord)
183 }
184
185 #[cfg(feature = "tz")]
187 pub const fn from_timestamp_tz(
188 unix_timestamp: i64, tz: tz::TimeZoneRef<'static>,
189 ) -> tz::TzResult<Self> {
190 match tz.find_local_time_type(unix_timestamp) {
191 Ok(tz) => Ok(Self::from_timestamp(unix_timestamp + tz.ut_offset() as i64)),
192 Err(e) => Err(e),
193 }
194 }
195
196 pub const fn overflowing_new(year: i16, month: u8, day: u8) -> Self {
206 let mut year = year;
207 let mut month = month;
208 let mut day = day;
209
210 while month > 12 {
212 year += 1;
213 month -= 12;
214 }
215 if day == 0 {
216 if month <= 1 {
217 year -= 1;
218 month += 11;
219 } else {
220 month -= 1;
221 }
222 day = utils::days_in_month(year, month);
223 }
224 if month == 0 {
225 year -= 1;
226 month = 12;
227 }
228 while day > utils::days_in_month(year, month) {
229 day -= utils::days_in_month(year, month);
230 month += 1;
231 if month == 13 {
232 year += 1;
233 month = 1;
234 }
235 }
236
237 Self::new(year, month, day)
239 }
240
241 pub fn parse(date_str: impl AsRef<str>, date_fmt: &'static str) -> ParseResult<Date> {
243 let parser = Parser::new(date_fmt);
244 let raw_date = parser.parse(date_str)?.date()?;
245 Ok(raw_date.into())
246 }
247}
248
249impl Date {
250 pub(crate) const fn ymd(&self) -> (i16, u8, u8) {
252 let shifted = self.0 + 719468; let era = if shifted >= 0 { shifted } else { shifted - 146_096 } / 146_097;
257 let doe = shifted - era * 146_097; let year_of_era = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
259 let year = year_of_era + era * 400;
260 let day_of_year = doe - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
261 let mp = (5 * day_of_year + 2) / 153;
262 let day = day_of_year - (153 * mp + 2) / 5 + 1;
263 let month = if mp < 10 { mp + 3 } else { mp - 9 };
264 (year as i16 + if month <= 2 { 1 } else { 0 }, month as u8, day as u8)
265 }
266
267 #[inline]
269 #[cfg(feature = "ord")]
270 pub const fn ord(&self) -> i32 {
271 self.0
272 }
273
274 #[inline]
276 pub const fn year(&self) -> i16 {
277 self.ymd().0
278 }
279
280 #[inline]
284 pub const fn month(&self) -> u8 {
285 self.ymd().1
286 }
287
288 #[inline]
292 pub const fn day(&self) -> u8 {
293 self.ymd().2
294 }
295
296 #[inline]
298 pub const fn day_of_year(&self) -> u16 {
299 (self.0 - Date::new(self.year() - 1, 12, 31).0) as u16
300 }
301
302 pub const fn week(&self) -> u16 {
307 let jan1 = Date::new(self.year(), 1, 1);
308 let first_sunday = jan1.0 + if self.0 % 7 == 3 { 0 } else { 7 } - (self.0 + 4) % 7;
309 ((self.0 - first_sunday).div_euclid(7) + 1) as u16
310 }
311
312 #[inline]
314 pub const fn weekday(&self) -> Weekday {
315 match (self.0 + 4) % 7 {
316 0 => Weekday::Sunday,
317 1 | -6 => Weekday::Monday,
318 2 | -5 => Weekday::Tuesday,
319 3 | -4 => Weekday::Wednesday,
320 4 | -3 => Weekday::Thursday,
321 5 | -2 => Weekday::Friday,
322 6 | -1 => Weekday::Saturday,
323 #[cfg(not(tarpaulin_include))]
324 _ => panic!("Unreachable: Anything % 7 must be within -6 to 6"),
325 }
326 }
327}
328
329impl Date {
330 pub const fn timestamp(&self) -> i64 {
342 self.0 as i64 * 86_400
343 }
344
345 #[cfg(feature = "tz")]
347 pub const fn timestamp_tz(&self, tz: tz::TimeZoneRef<'static>) -> tz::TzResult<i64> {
348 match tz.find_local_time_type(self.timestamp()) {
349 Ok(ts) => Ok(self.timestamp() - ts.ut_offset() as i64),
350 Err(e) => Err(e),
351 }
352 }
353}
354
355impl Date {
356 #[cfg(feature = "tz")]
363 pub fn today() -> Self {
364 let tz = tzdb::local_tz().expect("Could not determine local time zone");
365 let now =
366 now().duration_since(UNIX_EPOCH).expect("system time set prior to 1970").as_secs() as i64;
367 let offset = tz
368 .find_local_time_type(now)
369 .expect("Local time zone lacks information for this timestamp")
370 .ut_offset() as i64;
371 Self::from_timestamp(now + offset)
372 }
373
374 #[cfg(feature = "tz")]
376 pub fn today_tz(tz: tz::TimeZoneRef<'static>) -> tz::TzResult<Self> {
377 let now =
378 now().duration_since(UNIX_EPOCH).expect("system time set prior to 1970").as_secs() as i64;
379 let offset = tz.find_local_time_type(now)?.ut_offset() as i64;
380 Ok(Self::from_timestamp(now + offset))
381 }
382
383 pub fn today_utc() -> Self {
389 let now = now().duration_since(UNIX_EPOCH).expect("system time set prior to 1970").as_secs();
390 Self::from_timestamp(now as i64)
391 }
392}
393
394impl Date {
395 #[doc = include_str!("../support/date-format.md")]
397 #[doc = include_str!("../support/padding.md")]
399 #[doc = include_str!("../support/plain-characters.md")]
401 pub const fn format(self, format_str: &str) -> self::format::FormattedDate<'_> {
402 format::FormattedDate { date: self, format: format_str }
403 }
404}
405
406impl Date {
407 pub fn iter_through(&self, end: Date) -> iter::DateIterator {
410 iter::DateIterator::new(self, end)
411 }
412}
413
414impl Date {
415 pub const MAX: Self = Date::new(32767, 12, 31);
417 pub const MIN: Self = Date::new(-32768, 1, 1);
419}
420
421#[cfg(feature = "easter")]
422impl Date {
423 pub const fn easter(year: i16) -> Self {
425 assert!(year >= 1583 && year <= 9999, "Year out of bounds");
426 let a = year % 19;
427 let b = year / 100;
428 let c = year % 100;
429 let d = b / 4;
430 let e = b % 4;
431 let f = (b + 8) / 25;
432 let g = (b - f + 1) / 3;
433 let h = (19 * a + b - d - g + 15) % 30;
434 let i = c / 4;
435 let j = c % 4;
436 let k = (32 + 2 * e + 2 * i - h - j) % 7;
437 let l = (a + 11 * h + 22 * k) / 451;
438 let month = (h + k - 7 * l + 114) / 31;
439 let day = (h + k - 7 * l + 114) % 31 + 1;
440 Self::new(year, month as u8, day as u8)
441 }
442}
443
444impl fmt::Debug for Date {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 write!(f, "{}", self.format("%Y-%m-%d"))
447 }
448}
449
450impl fmt::Display for Date {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 write!(f, "{}", self.format("%Y-%m-%d"))
453 }
454}
455
456#[cfg(feature = "log")]
457impl log::kv::ToValue for Date {
458 fn to_value(&self) -> log::kv::Value<'_> {
459 log::kv::Value::from_debug(self)
460 }
461}
462
463impl FromStr for Date {
464 type Err = ParseError;
465
466 fn from_str(s: &str) -> ParseResult<Self> {
467 let b = s.as_bytes();
468 if b.len() == 10
469 && b[4] == b'-'
470 && b[7] == b'-'
471 && b.iter().enumerate().all(|(i, c)| matches!(i, 4 | 7) || c.is_ascii_digit())
472 {
473 let year = (b[0] - b'0') as i16 * 1000
474 + (b[1] - b'0') as i16 * 100
475 + (b[2] - b'0') as i16 * 10
476 + (b[3] - b'0') as i16;
477 let month = (b[5] - b'0') * 10 + (b[6] - b'0');
478 let day = (b[8] - b'0') * 10 + (b[9] - b'0');
479 if (1..=12).contains(&month) && day >= 1 && day <= utils::days_in_month(year, month) {
480 return Ok(Self::new(year, month, day));
481 }
482 }
483 Self::parse(s, "%Y-%m-%d")
484 }
485}
486
487impl From<strptime::RawDate> for Date {
488 fn from(value: strptime::RawDate) -> Self {
489 Self::new(value.year(), value.month(), value.day())
490 }
491}
492
493impl PartialEq<Date> for &Date {
494 fn eq(&self, other: &Date) -> bool {
495 *self == other
496 }
497}
498
499impl PartialOrd<Date> for &Date {
500 fn partial_cmp(&self, other: &Date) -> Option<Ordering> {
501 (*self).partial_cmp(other)
502 }
503}
504
505#[cfg(not(test))]
506fn now() -> SystemTime {
507 SystemTime::now()
508}
509
510#[cfg(test)]
511use tests::now;
512
513#[cfg(test)]
514mod tests {
515 use std::cell::RefCell;
516
517 use assert2::check;
518
519 use super::*;
520
521 thread_local! {
522 static MOCK_TIME: RefCell<Option<SystemTime>> = const { RefCell::new(None) };
523 }
524
525 fn set_now(time: SystemTime) {
526 MOCK_TIME.with(|cell| *cell.borrow_mut() = Some(time));
527 }
528
529 fn clear_now() {
530 MOCK_TIME.with(|cell| *cell.borrow_mut() = None);
531 }
532
533 pub(super) fn now() -> SystemTime {
534 MOCK_TIME.with(|cell| cell.borrow().as_ref().cloned().unwrap_or_else(SystemTime::now))
535 }
536
537 #[test]
538 fn test_internal_repr() {
539 check!(date! { 1969-12-31 }.0 == -1);
540 check!(date! { 1970-01-01 }.0 == 0);
541 check!(date! { 1970-01-02 }.0 == 1);
542 }
543
544 #[test]
545 fn test_ymd_readback() {
546 for year in [2020, 2022, 2100] {
547 for month in 1..=12 {
548 let days = match month {
549 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
550 4 | 6 | 9 | 11 => 30,
551 2 => match utils::is_leap_year(year) {
552 true => 29,
553 false => 28,
554 },
555 #[cfg(not(tarpaulin_include))]
556 _ => panic!("Unreachable"),
557 };
558 for day in 1..=days {
559 let date = Date::new(year, month, day);
560 check!(date.year() == year);
561 check!(date.month() == month);
562 check!(date.day() == day);
563 }
564 }
565 }
566 }
567
568 #[test]
569 #[should_panic]
570 fn test_overflow_panic_day() {
571 Date::new(2012, 4, 31);
572 }
573
574 #[test]
575 #[should_panic]
576 fn test_overflow_panic_month() {
577 Date::new(2012, 13, 1);
578 }
579
580 #[test]
581 #[should_panic]
582 fn test_overflow_panic_ly() {
583 Date::new(2100, 2, 29);
584 }
585
586 #[test]
587 #[allow(clippy::zero_prefixed_literal)]
588 fn test_ymd_overflow() {
589 macro_rules! overflows_to {
590 ($y1:literal-$m1:literal-$d1:literal
591 == $y2:literal-$m2:literal-$d2:literal) => {
592 let date1 = Date::overflowing_new($y1, $m1, $d1);
593 let date2 = Date::new($y2, $m2, $d2);
594 check!(date1 == date2);
595 };
596 }
597 overflows_to! { 2022-01-32 == 2022-02-01 };
598 overflows_to! { 2022-02-29 == 2022-03-01 };
599 overflows_to! { 2022-03-32 == 2022-04-01 };
600 overflows_to! { 2022-04-31 == 2022-05-01 };
601 overflows_to! { 2022-05-32 == 2022-06-01 };
602 overflows_to! { 2022-06-31 == 2022-07-01 };
603 overflows_to! { 2022-07-32 == 2022-08-01 };
604 overflows_to! { 2022-08-32 == 2022-09-01 };
605 overflows_to! { 2022-09-31 == 2022-10-01 };
606 overflows_to! { 2022-10-32 == 2022-11-01 };
607 overflows_to! { 2022-11-31 == 2022-12-01 };
608 overflows_to! { 2022-12-32 == 2023-01-01 };
609 overflows_to! { 2022-00-00 == 2021-11-30 };
610 overflows_to! { 2022-01-00 == 2021-12-31 };
611 overflows_to! { 2022-02-00 == 2022-01-31 };
612 overflows_to! { 2022-03-00 == 2022-02-28 };
613 overflows_to! { 2022-04-00 == 2022-03-31 };
614 overflows_to! { 2022-05-00 == 2022-04-30 };
615 overflows_to! { 2022-06-00 == 2022-05-31 };
616 overflows_to! { 2022-07-00 == 2022-06-30 };
617 overflows_to! { 2022-08-00 == 2022-07-31 };
618 overflows_to! { 2022-09-00 == 2022-08-31 };
619 overflows_to! { 2022-10-00 == 2022-09-30 };
620 overflows_to! { 2022-11-00 == 2022-10-31 };
621 overflows_to! { 2022-12-00 == 2022-11-30 };
622 overflows_to! { 2020-02-30 == 2020-03-01 };
623 overflows_to! { 2020-03-00 == 2020-02-29 };
624 overflows_to! { 2022-01-45 == 2022-02-14 };
625 overflows_to! { 2022-13-15 == 2023-01-15 };
626 overflows_to! { 2022-00-15 == 2021-12-15 };
627 }
628
629 #[test]
630 fn test_display() {
631 check!(date! { 2012-04-21 }.to_string() == "2012-04-21");
632 check!(format!("{:?}", date! { 2012-04-21 }) == "2012-04-21");
633 }
634
635 #[test]
636 fn test_week() {
637 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); }
646
647 #[test]
648 fn test_today() {
649 set_now(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86_400));
650 check!(Date::today_utc() == date! { 1970-01-02 });
651 clear_now();
652 }
653
654 #[cfg(feature = "tz")]
655 #[test]
656 fn test_today_tz() -> tz::TzResult<()> {
657 set_now(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86_400));
658 check!([date! { 1970-01-01 }, date! { 1970-01-02 }].contains(&Date::today()));
659 check!(Date::today_tz(tz::us::EASTERN)? == date! { 1970-01-01 });
660 clear_now();
661 Ok(())
662 }
663
664 #[cfg(feature = "tz")]
665 #[test]
666 fn test_timestamp_tz() -> tz::TzResult<()> {
667 check!(Date::from_timestamp_tz(1335020400, tz::us::EASTERN)? == date! { 2012-04-21 });
668 check!(Date::from_timestamp_tz(0, tz::us::PACIFIC)? == date! { 1969-12-31 });
669 check!(date! { 2012-04-21 }.timestamp_tz(tz::us::EASTERN)? == 1334980800);
670 Ok(())
671 }
672
673 #[cfg(feature = "easter")]
674 #[test]
675 fn test_easter() {
676 check!(Date::easter(2013) == date! { 2013-03-31 });
677 check!(Date::easter(2014) == date! { 2014-04-20 });
678 check!(Date::easter(2015) == date! { 2015-04-05 });
679 check!(Date::easter(2016) == date! { 2016-03-27 });
680 check!(Date::easter(2017) == date! { 2017-04-16 });
681 check!(Date::easter(2018) == date! { 2018-04-01 });
682 check!(Date::easter(2019) == date! { 2019-04-21 });
683 check!(Date::easter(2020) == date! { 2020-04-12 });
684 check!(Date::easter(2021) == date! { 2021-04-04 });
685 check!(Date::easter(2022) == date! { 2022-04-17 });
686 check!(Date::easter(2023) == date! { 2023-04-09 });
687 check!(Date::easter(2024) == date! { 2024-03-31 });
688 check!(Date::easter(2025) == date! { 2025-04-20 });
689 check!(Date::easter(2026) == date! { 2026-04-05 });
690 check!(Date::easter(2027) == date! { 2027-03-28 });
691 check!(Date::easter(2028) == date! { 2028-04-16 });
692 check!(Date::easter(2029) == date! { 2029-04-01 });
693 check!(Date::easter(2030) == date! { 2030-04-21 });
694 check!(Date::easter(2031) == date! { 2031-04-13 });
695 check!(Date::easter(2032) == date! { 2032-03-28 });
696 check!(Date::easter(2033) == date! { 2033-04-17 });
697 check!(Date::easter(2034) == date! { 2034-04-09 });
698 check!(Date::easter(2035) == date! { 2035-03-25 });
699 }
700
701 #[test]
702 fn test_from_str() -> ParseResult<()> {
703 check!("2012-04-21".parse::<Date>()? == date! { 2012-04-21 });
704 check!("2012-4-21".parse::<Date>().is_err());
705 check!("04/21/2012".parse::<Date>().is_err());
706 check!("12-04-21".parse::<Date>().is_err());
707 check!("foo".parse::<Date>().map_err(|e| e.to_string()).unwrap_err().contains("foo"));
708 Ok(())
709 }
710
711 #[test]
712 fn test_parse() -> ParseResult<()> {
713 check!(Date::parse("04/21/12", "%m/%d/%y")? == date! { 2012-04-21 });
714 check!(Date::parse("Saturday, April 21, 2012", "%A, %B %-d, %Y")? == date! { 2012-04-21 });
715 Ok(())
716 }
717
718 #[test]
719 #[cfg(feature = "ord")]
720 fn test_ord() {
721 let date = date! { 1970-01-06 };
722 check!(date.ord() == 5);
723 }
724
725 #[test]
726 fn test_ref_eq() {
727 let date = date! { 2012-04-21 };
728 check!(&date == date);
729 check!(&date > date! { 2012-04-20 });
730 }
731}