1use std::str::FromStr;
4
5use crate::{Error, recurrence::Interval};
6
7pub use chrono::NaiveDate;
8use chrono::{Datelike, Days, Months, TimeZone, Utc};
9use nonempty::NonEmpty;
10use serde::{Deserialize, Serialize};
11use strum::{EnumCount, VariantArray};
12
13#[derive(
14 Debug,
15 Clone,
16 Copy,
17 Serialize,
18 Deserialize,
19 Default,
20 PartialEq,
21 Eq,
22 PartialOrd,
23 Ord,
24 Hash,
25 derive_more::Display,
26)]
27#[display("{}", _0)]
28#[serde(transparent)]
29pub struct Date(NaiveDate);
30
31impl Date {
32 #[must_use]
33 pub fn into_iso_string(self) -> String {
34 self.0.format(ISO_DATE_FORMAT).to_string()
35 }
36
37 #[must_use]
38 pub fn weekday(self) -> Weekday {
39 Weekday::from(Datelike::weekday(&self.0))
40 }
41
42 #[must_use]
43 pub fn month(self) -> Month {
44 Month::from(self.0)
45 }
46
47 #[must_use]
53 #[allow(clippy::expect_used)]
54 pub fn day_of_month(self) -> DayOfMonth {
55 DayOfMonth::from_value(Datelike::day(&self.0))
56 .expect("expecting all dates to have a day in a month")
57 }
58
59 #[must_use]
60 pub fn year(self) -> Year {
61 Year(self.0.year())
62 }
63
64 #[must_use]
65 pub fn day(self) -> u32 {
66 self.0.day()
67 }
68
69 #[must_use]
70 pub fn add_interval_days(self, value: Interval) -> Date {
71 self.add_days(value.get() as usize)
72 }
73
74 #[must_use]
75 pub fn add_days(self, value: usize) -> Date {
76 Self(
77 self.0
78 .checked_add_days(Days::new(value as u64))
79 .unwrap_or(self.0),
80 )
81 }
82
83 #[must_use]
84 pub fn sub_days(self, value: usize) -> Date {
85 Self(
86 self.0
87 .checked_sub_days(Days::new(value as u64))
88 .unwrap_or(self.0),
89 )
90 }
91
92 #[must_use]
93 pub const fn from_ymd_opt(year: Year, month: u32, day: u32) -> Option<Date> {
94 let year: i32 = year.into_i32();
95 let inner = NaiveDate::from_ymd_opt(year, month, day);
96 match inner {
97 Some(inner) => Some(Self(inner)),
98 None => None,
99 }
100 }
101
102 #[must_use]
103 pub fn day_after(self) -> Date {
104 self.0.succ_opt().map_or(self, Self)
105 }
106
107 #[must_use]
108 pub fn next_month(self) -> Date {
109 self.0.checked_add_months(Months::new(1)).map_or(self, Self)
110 }
111
112 #[must_use]
113 pub fn first_of_next_month(self) -> Self {
114 self.first_of_month()
115 .0
116 .checked_add_months(Months::new(1))
117 .map_or(self, Self)
118 }
119
120 #[must_use]
121 pub fn first_of_month(self) -> Self {
122 self.0.with_day(DayOfMonth::MIN).map_or(self, Self)
123 }
124
125 #[must_use]
126 pub fn first_of_year(self) -> Self {
127 self.first_of_month().0.with_month(1).map_or(self, Self)
128 }
129
130 #[must_use]
131 pub fn with_day(self, value: DayOfMonth) -> Option<Date> {
132 self.0.with_day(value.to_value()).map(Self)
133 }
134
135 pub const MIN: Self = Self(NaiveDate::MIN);
136
137 #[must_use]
138 pub fn is_valid_date_str(value: &str) -> bool {
139 Self::from_str(value).is_ok()
140 }
141
142 #[must_use]
148 #[allow(clippy::expect_used)]
149 pub fn from_str_unchecked(value: &str) -> Date {
150 Self::from_str(value)
151 .expect("expecting value {value} to be in iso format for it to be parsed")
152 }
153
154 #[must_use]
156 pub fn days_between(self, other: Date) -> usize {
157 let duration = self.0.signed_duration_since(other.0);
158 duration.num_days().abs().try_into().unwrap_or_default()
159 }
160
161 #[must_use]
163 pub fn signed_days_from(self, reference: Date) -> i64 {
164 (self.0 - reference.0).num_days()
165 }
166
167 #[must_use]
169 pub fn next_business_day(self) -> Self {
170 let next_day = self.add_days(1);
171 match next_day.weekday() {
172 Weekday::Saturday => next_day.add_days(2), Weekday::Sunday => next_day.add_days(1), _ => next_day, }
176 }
177
178 #[must_use]
179 pub fn latest_sunday(self) -> Self {
180 match self.weekday() {
181 Weekday::Monday => self.sub_days(1),
182 Weekday::Tuesday => self.sub_days(2),
183 Weekday::Wednesday => self.sub_days(3),
184 Weekday::Thursday => self.sub_days(4),
185 Weekday::Friday => self.sub_days(5),
186 Weekday::Saturday => self.sub_days(6),
187 Weekday::Sunday => self,
188 }
189 }
190
191 #[must_use]
192 pub fn next_sunday(self) -> Self {
193 let days_until_sunday = match self.weekday() {
194 Weekday::Monday => 6,
195 Weekday::Tuesday => 5,
196 Weekday::Wednesday => 4,
197 Weekday::Thursday => 3,
198 Weekday::Friday => 2,
199 Weekday::Saturday => 1,
200 Weekday::Sunday => 7, };
202 self.add_days(days_until_sunday)
203 }
204
205 #[must_use]
206 pub fn next_saturday(self) -> Self {
207 let days_until_saturday = match self.weekday() {
208 Weekday::Monday => 5,
209 Weekday::Tuesday => 4,
210 Weekday::Wednesday => 3,
211 Weekday::Thursday => 2,
212 Weekday::Friday => 1,
213 Weekday::Saturday => 7, Weekday::Sunday => 6,
215 };
216 self.add_days(days_until_saturday)
217 }
218
219 #[must_use]
220 pub fn soonest_saturday(self) -> Self {
221 if self.weekday() == Weekday::Saturday {
222 self
223 } else {
224 self.next_saturday()
225 }
226 }
227
228 #[must_use]
230 pub fn next_monday(self) -> Self {
231 let days_until_monday = match self.weekday() {
232 Weekday::Monday => 7, Weekday::Tuesday => 6,
234 Weekday::Wednesday => 5,
235 Weekday::Thursday => 4,
236 Weekday::Friday => 3,
237 Weekday::Saturday => 2,
238 Weekday::Sunday => 1,
239 };
240 self.add_days(days_until_monday)
241 }
242
243 #[must_use]
245 pub fn add_months(self, months: impl Into<u32>) -> Self {
246 use chrono::Months;
247
248 let naive_date: chrono::NaiveDate = self.into();
249 let result = naive_date
250 .checked_add_months(Months::new(months.into()))
251 .unwrap_or(naive_date);
252
253 Self::from(result)
254 }
255
256 #[must_use]
258 pub fn add_years(self, years: u32) -> Self {
259 let years = years.try_into().unwrap_or(i32::MAX);
260 let new_year = self.year().saturating_add(years);
261 Self::from_ymd_opt(new_year, self.month().to_month_number(), self.day()).unwrap_or_else(
262 || {
263 Self::from_ymd_opt(new_year, self.month().to_month_number(), 28).unwrap_or(self)
265 },
266 )
267 }
268
269 #[must_use]
271 pub fn sub_years(self, years: u32) -> Self {
272 let years = years.try_into().unwrap_or(i32::MAX);
273 let new_year = self.year().saturating_sub(years);
274 Self::from_ymd_opt(new_year, self.month().to_month_number(), self.day()).unwrap_or_else(
275 || {
276 Self::from_ymd_opt(new_year, self.month().to_month_number(), 28).unwrap_or(self)
278 },
279 )
280 }
281
282 #[must_use]
284 pub fn sub_months(self, months: u32) -> Self {
285 let naive_date: NaiveDate = self.into();
286 let result = naive_date
287 .checked_sub_months(Months::new(months))
288 .unwrap_or(naive_date);
289 Self::from(result)
290 }
291
292 #[must_use]
295 pub fn last_weekday(self, weekday: Weekday) -> Self {
296 let today_weekday = self.weekday();
297 let days_back = if today_weekday == weekday {
298 7 } else {
300 let today_num = today_weekday.num_days_from_monday();
301 let target_num = weekday.num_days_from_monday();
302 if today_num > target_num {
303 today_num - target_num
304 } else {
305 7 - (target_num - today_num)
306 }
307 };
308 self.sub_days(days_back)
309 }
310
311 #[must_use]
314 pub fn next_weekday(self, weekday: Weekday) -> Self {
315 weekday.next_occurrence_after(self)
316 }
317
318 #[must_use]
320 pub fn last_day_of_month(self) -> Self {
321 self.first_of_next_month().sub_days(1)
322 }
323
324 #[must_use]
326 pub fn last_day_of_next_month(self) -> Self {
327 self.first_of_next_month().last_day_of_month()
328 }
329
330 #[must_use]
332 pub fn next_january_first(self) -> Self {
333 let next_year = self.year().next();
334 Self::from_ymd_opt(next_year, 1, 1).unwrap_or(self)
335 }
336
337 #[must_use]
339 pub fn january_first(year: Year) -> Option<Self> {
340 Self::from_ymd_opt(year, 1, 1)
341 }
342
343 #[must_use]
344 pub fn into_naive_date(self) -> NaiveDate {
345 self.0
346 }
347
348 #[must_use]
349 pub fn from_naive_date(value: NaiveDate) -> Self {
350 Self(value)
351 }
352
353 #[must_use]
354 pub fn year_month(self) -> YearMonth {
355 YearMonth::new(self.year(), self.month())
356 }
357
358 #[must_use]
359 pub fn end_of_day_utc(self) -> chrono::DateTime<Utc> {
360 Utc.from_utc_datetime(&self.0.and_hms_opt(23, 59, 59).unwrap_or_default())
361 }
362
363 #[must_use]
364 pub fn start_of_day_utc(self) -> chrono::DateTime<Utc> {
365 Utc.from_utc_datetime(&self.0.and_hms_opt(0, 0, 0).unwrap_or_default())
366 }
367
368 #[must_use]
369 pub fn today() -> Self {
370 Self::from(chrono::Local::now().date_naive())
371 }
372
373 #[must_use]
374 pub fn last_day_of_previous_month(self) -> Self {
375 self.first_of_month().sub_days(1)
376 }
377}
378
379impl FromStr for Date {
380 type Err = Error;
381
382 fn from_str(s: &str) -> Result<Self, Error> {
383 NaiveDate::parse_from_str(s, ISO_DATE_FORMAT)
384 .map(Self)
385 .map_err(|_| Error::InvalidDate)
386 }
387}
388
389impl From<NaiveDate> for Date {
390 fn from(value: NaiveDate) -> Self {
391 Self(value)
392 }
393}
394
395impl From<Date> for NaiveDate {
396 fn from(value: Date) -> Self {
397 value.into_naive_date()
398 }
399}
400
401impl std::ops::Sub for Date {
402 type Output = crate::time::Duration;
403
404 fn sub(self, other: Date) -> Self::Output {
405 let chrono_duration = self.0.signed_duration_since(other.0);
406 let abs_duration = chrono_duration.abs();
407
408 if let Some(nanos) = abs_duration.num_nanoseconds() {
410 crate::time::Duration {
411 nanos: nanos.abs().try_into().unwrap_or_default(),
412 }
413 } else {
414 let secs = abs_duration
416 .num_seconds()
417 .abs()
418 .try_into()
419 .unwrap_or_default();
420 crate::time::Duration::from_secs(secs)
421 }
422 }
423}
424
425#[derive(
426 Debug,
427 Clone,
428 Copy,
429 Serialize,
430 Deserialize,
431 PartialEq,
432 Eq,
433 PartialOrd,
434 Ord,
435 Hash,
436 derive_more::Display,
437)]
438#[serde(transparent)]
439#[display("{_0}")]
440pub struct Year(i32);
441
442impl Year {
443 pub(crate) fn saturating_add(self, value: i32) -> Self {
444 Self(self.0.saturating_add(value))
445 }
446
447 pub(crate) fn saturating_sub(self, value: i32) -> Self {
448 Self(self.0.saturating_sub(value))
449 }
450
451 #[must_use]
452 pub fn previous(self) -> Self {
453 Self(self.0.saturating_sub(1))
454 }
455
456 pub(crate) fn next(self) -> Self {
457 self.saturating_add(1)
458 }
459
460 pub(crate) const fn into_i32(self) -> i32 {
461 self.0
462 }
463}
464
465impl From<i32> for Year {
466 fn from(value: i32) -> Self {
467 Self(value)
468 }
469}
470
471impl From<Year> for i32 {
472 fn from(value: Year) -> Self {
473 value.0
474 }
475}
476
477const ISO_DATE_FORMAT: &str = "%Y-%m-%d";
478
479#[derive(
480 Debug,
481 Clone,
482 Copy,
483 Serialize,
484 Deserialize,
485 PartialEq,
486 Eq,
487 Hash,
488 derive_more::Display,
489 derive_more::FromStr,
490 strum::VariantArray,
491 strum::EnumCount,
492)]
493pub enum Weekday {
494 #[display("Monday")]
495 Monday,
496 #[display("Tuesday")]
497 Tuesday,
498 #[display("Wednesday")]
499 Wednesday,
500 #[display("Thursday")]
501 Thursday,
502 #[display("Friday")]
503 Friday,
504 #[display("Saturday")]
505 Saturday,
506 #[display("Sunday")]
507 Sunday,
508}
509
510impl Weekday {
511 pub(crate) fn next_occurrence_after(self, date: Date) -> Date {
512 let after_weekday = date.weekday();
513
514 let days_to_add = if after_weekday == self {
515 Self::COUNT
517 } else {
518 let after_num_days = after_weekday.num_days_from_monday();
519 let self_num_days = self.num_days_from_monday();
520
521 if self_num_days > after_num_days {
522 self_num_days - after_num_days
524 } else {
525 Self::COUNT - after_num_days + self_num_days
527 }
528 };
529
530 date.add_days(days_to_add)
531 }
532
533 #[must_use]
535 pub fn num_days_from_monday(self) -> usize {
536 match self {
537 Weekday::Monday => 0,
538 Weekday::Tuesday => 1,
539 Weekday::Wednesday => 2,
540 Weekday::Thursday => 3,
541 Weekday::Friday => 4,
542 Weekday::Saturday => 5,
543 Weekday::Sunday => 6,
544 }
545 }
546
547 pub(crate) fn next_occurrence_after_days(days: &NonEmpty<Self>, date: Date) -> Date {
548 days.iter()
549 .map(|day| day.next_occurrence_after(date))
550 .min()
551 .unwrap_or(date)
552 }
553
554 pub(crate) fn is_weekend(self) -> bool {
555 self == Weekday::Saturday || self == Weekday::Sunday
556 }
557
558 pub(crate) fn is_weekday(self) -> bool {
559 !self.is_weekend()
560 }
561
562 #[must_use]
563 pub fn first_initial(&self) -> String {
564 let initial = match self {
565 Weekday::Monday => "M",
566 Weekday::Tuesday | Weekday::Thursday => "T",
567 Weekday::Wednesday => "W",
568 Weekday::Friday => "F",
569 Weekday::Saturday | Weekday::Sunday => "S",
570 };
571 initial.to_owned()
572 }
573}
574
575impl From<Weekday> for NonEmpty<Weekday> {
576 fn from(value: Weekday) -> Self {
577 NonEmpty::singleton(value)
578 }
579}
580
581#[derive(
582 Debug,
583 Clone,
584 Copy,
585 Serialize,
586 Deserialize,
587 PartialEq,
588 Eq,
589 PartialOrd,
590 Ord,
591 Hash,
592 derive_more::Display,
593 derive_more::FromStr,
594 strum::VariantArray,
595)]
596pub enum Month {
597 #[display("January")]
598 January,
599 #[display("February")]
600 February,
601 #[display("March")]
602 March,
603 #[display("April")]
604 April,
605 #[display("May")]
606 May,
607 #[display("June")]
608 June,
609 #[display("July")]
610 July,
611 #[display("August")]
612 August,
613 #[display("September")]
614 September,
615 #[display("October")]
616 October,
617 #[display("November")]
618 November,
619 #[display("December")]
620 December,
621}
622
623impl Month {
624 #[must_use]
625 pub fn to_month_number(self) -> u32 {
626 match self {
627 Month::January => 1,
628 Month::February => 2,
629 Month::March => 3,
630 Month::April => 4,
631 Month::May => 5,
632 Month::June => 6,
633 Month::July => 7,
634 Month::August => 8,
635 Month::September => 9,
636 Month::October => 10,
637 Month::November => 11,
638 Month::December => 12,
639 }
640 }
641
642 #[must_use]
644 pub fn from_number(num: u32) -> Option<Self> {
645 match num {
646 1 => Some(Month::January),
647 2 => Some(Month::February),
648 3 => Some(Month::March),
649 4 => Some(Month::April),
650 5 => Some(Month::May),
651 6 => Some(Month::June),
652 7 => Some(Month::July),
653 8 => Some(Month::August),
654 9 => Some(Month::September),
655 10 => Some(Month::October),
656 11 => Some(Month::November),
657 12 => Some(Month::December),
658 _ => None,
659 }
660 }
661
662 #[must_use]
663 pub fn short_description(&self) -> &'static str {
664 match self {
665 Month::January => "Jan",
666 Month::February => "Feb",
667 Month::March => "Mar",
668 Month::April => "Apr",
669 Month::May => "May",
670 Month::June => "Jun",
671 Month::July => "Jul",
672 Month::August => "Aug",
673 Month::September => "Sep",
674 Month::October => "Oct",
675 Month::November => "Nov",
676 Month::December => "Dec",
677 }
678 }
679
680 #[must_use]
681 pub fn months_until(value: Self) -> Vec<Self> {
682 let mut months = Vec::new();
683 for month in Month::VARIANTS {
684 months.push(*month);
685 if month == &value {
686 break;
687 }
688 }
689 months
690 }
691
692 #[must_use]
693 pub fn next(self) -> Self {
694 match self {
695 Month::January => Month::February,
696 Month::February => Month::March,
697 Month::March => Month::April,
698 Month::April => Month::May,
699 Month::May => Month::June,
700 Month::June => Month::July,
701 Month::July => Month::August,
702 Month::August => Month::September,
703 Month::September => Month::October,
704 Month::October => Month::November,
705 Month::November => Month::December,
706 Month::December => Month::January,
707 }
708 }
709}
710
711impl From<Month> for NonEmpty<Month> {
712 fn from(value: Month) -> Self {
713 NonEmpty::singleton(value)
714 }
715}
716
717impl From<NaiveDate> for Month {
718 fn from(value: NaiveDate) -> Self {
719 match value.month0() {
720 0 => Month::January,
721 1 => Month::February,
722 2 => Month::March,
723 3 => Month::April,
724 4 => Month::May,
725 5 => Month::June,
726 6 => Month::July,
727 7 => Month::August,
728 8 => Month::September,
729 9 => Month::October,
730 10 => Month::November,
731 11 => Month::December,
732 _ => unreachable!("Invalid month index from NaiveDate"),
733 }
734 }
735}
736
737impl From<Weekday> for chrono::Weekday {
738 fn from(value: Weekday) -> chrono::Weekday {
739 match value {
740 Weekday::Monday => chrono::Weekday::Mon,
741 Weekday::Tuesday => chrono::Weekday::Tue,
742 Weekday::Wednesday => chrono::Weekday::Wed,
743 Weekday::Thursday => chrono::Weekday::Thu,
744 Weekday::Friday => chrono::Weekday::Fri,
745 Weekday::Saturday => chrono::Weekday::Sat,
746 Weekday::Sunday => chrono::Weekday::Sun,
747 }
748 }
749}
750
751impl From<chrono::Weekday> for Weekday {
752 fn from(value: chrono::Weekday) -> Self {
753 match value {
754 chrono::Weekday::Mon => Self::Monday,
755 chrono::Weekday::Tue => Self::Tuesday,
756 chrono::Weekday::Wed => Self::Wednesday,
757 chrono::Weekday::Thu => Self::Thursday,
758 chrono::Weekday::Fri => Self::Friday,
759 chrono::Weekday::Sat => Self::Saturday,
760 chrono::Weekday::Sun => Self::Sunday,
761 }
762 }
763}
764
765#[derive(
766 Debug,
767 Clone,
768 Copy,
769 Serialize,
770 Deserialize,
771 PartialEq,
772 Eq,
773 PartialOrd,
774 Ord,
775 derive_more::Display,
776 strum::VariantArray,
777)]
778pub enum DayOfMonth {
779 #[display("1st")]
780 First,
781 #[display("2nd")]
782 Second,
783 #[display("3rd")]
784 Third,
785 #[display("4th")]
786 Fourth,
787 #[display("5th")]
788 Fifth,
789 #[display("6th")]
790 Sixth,
791 #[display("7th")]
792 Seventh,
793 #[display("8th")]
794 Eighth,
795 #[display("9th")]
796 Ninth,
797 #[display("10th")]
798 Tenth,
799 #[display("11th")]
800 Eleventh,
801 #[display("12th")]
802 Twelfth,
803 #[display("13th")]
804 Thirteenth,
805 #[display("14th")]
806 Fourteenth,
807 #[display("15th")]
808 Fifteenth,
809 #[display("16th")]
810 Sixteenth,
811 #[display("17th")]
812 Seventeenth,
813 #[display("18th")]
814 Eighteenth,
815 #[display("19th")]
816 Nineteenth,
817 #[display("20th")]
818 Twentieth,
819 #[display("21st")]
820 TwentyFirst,
821 #[display("22nd")]
822 TwentySecond,
823 #[display("23rd")]
824 TwentyThird,
825 #[display("24th")]
826 TwentyFourth,
827 #[display("25th")]
828 TwentyFifth,
829 #[display("26th")]
830 TwentySixth,
831 #[display("27th")]
832 TwentySeventh,
833 #[display("28th")]
834 TwentyEighth,
835 #[display("29th")]
836 TwentyNinth,
837 #[display("30th")]
838 Thirtieth,
839 #[display("31st")]
840 ThirtyFirst,
841}
842
843impl DayOfMonth {
844 pub(crate) const MIN: u32 = 1;
845 const MAX: u32 = 31;
846
847 pub(crate) fn from_value(value: u32) -> Option<Self> {
848 match value {
849 1 => Some(Self::First),
850 2 => Some(Self::Second),
851 3 => Some(Self::Third),
852 4 => Some(Self::Fourth),
853 5 => Some(Self::Fifth),
854 6 => Some(Self::Sixth),
855 7 => Some(Self::Seventh),
856 8 => Some(Self::Eighth),
857 9 => Some(Self::Ninth),
858 10 => Some(Self::Tenth),
859 11 => Some(Self::Eleventh),
860 12 => Some(Self::Twelfth),
861 13 => Some(Self::Thirteenth),
862 14 => Some(Self::Fourteenth),
863 15 => Some(Self::Fifteenth),
864 16 => Some(Self::Sixteenth),
865 17 => Some(Self::Seventeenth),
866 18 => Some(Self::Eighteenth),
867 19 => Some(Self::Nineteenth),
868 20 => Some(Self::Twentieth),
869 21 => Some(Self::TwentyFirst),
870 22 => Some(Self::TwentySecond),
871 23 => Some(Self::TwentyThird),
872 24 => Some(Self::TwentyFourth),
873 25 => Some(Self::TwentyFifth),
874 26 => Some(Self::TwentySixth),
875 27 => Some(Self::TwentySeventh),
876 28 => Some(Self::TwentyEighth),
877 29 => Some(Self::TwentyNinth),
878 30 => Some(Self::Thirtieth),
879 31 => Some(Self::ThirtyFirst),
880 _ => None,
881 }
882 }
883
884 #[must_use]
885 pub fn to_value(self) -> u32 {
886 match self {
887 Self::First => 1,
888 Self::Second => 2,
889 Self::Third => 3,
890 Self::Fourth => 4,
891 Self::Fifth => 5,
892 Self::Sixth => 6,
893 Self::Seventh => 7,
894 Self::Eighth => 8,
895 Self::Ninth => 9,
896 Self::Tenth => 10,
897 Self::Eleventh => 11,
898 Self::Twelfth => 12,
899 Self::Thirteenth => 13,
900 Self::Fourteenth => 14,
901 Self::Fifteenth => 15,
902 Self::Sixteenth => 16,
903 Self::Seventeenth => 17,
904 Self::Eighteenth => 18,
905 Self::Nineteenth => 19,
906 Self::Twentieth => 20,
907 Self::TwentyFirst => 21,
908 Self::TwentySecond => 22,
909 Self::TwentyThird => 23,
910 Self::TwentyFourth => 24,
911 Self::TwentyFifth => 25,
912 Self::TwentySixth => 26,
913 Self::TwentySeventh => 27,
914 Self::TwentyEighth => 28,
915 Self::TwentyNinth => 29,
916 Self::Thirtieth => 30,
917 Self::ThirtyFirst => 31,
918 }
919 }
920
921 pub(crate) fn range() -> std::ops::RangeInclusive<u32> {
922 Self::MIN..=Self::MAX
923 }
924}
925
926impl From<DayOfMonth> for NonEmpty<DayOfMonth> {
927 fn from(value: DayOfMonth) -> Self {
928 NonEmpty::singleton(value)
929 }
930}
931
932impl std::str::FromStr for DayOfMonth {
933 type Err = crate::Error;
934
935 fn from_str(s: &str) -> Result<Self, Self::Err> {
936 match s {
937 "1st" => Ok(Self::First),
938 "2nd" => Ok(Self::Second),
939 "3rd" => Ok(Self::Third),
940 "4th" => Ok(Self::Fourth),
941 "5th" => Ok(Self::Fifth),
942 "6th" => Ok(Self::Sixth),
943 "7th" => Ok(Self::Seventh),
944 "8th" => Ok(Self::Eighth),
945 "9th" => Ok(Self::Ninth),
946 "10th" => Ok(Self::Tenth),
947 "11th" => Ok(Self::Eleventh),
948 "12th" => Ok(Self::Twelfth),
949 "13th" => Ok(Self::Thirteenth),
950 "14th" => Ok(Self::Fourteenth),
951 "15th" => Ok(Self::Fifteenth),
952 "16th" => Ok(Self::Sixteenth),
953 "17th" => Ok(Self::Seventeenth),
954 "18th" => Ok(Self::Eighteenth),
955 "19th" => Ok(Self::Nineteenth),
956 "20th" => Ok(Self::Twentieth),
957 "21st" => Ok(Self::TwentyFirst),
958 "22nd" => Ok(Self::TwentySecond),
959 "23rd" => Ok(Self::TwentyThird),
960 "24th" => Ok(Self::TwentyFourth),
961 "25th" => Ok(Self::TwentyFifth),
962 "26th" => Ok(Self::TwentySixth),
963 "27th" => Ok(Self::TwentySeventh),
964 "28th" => Ok(Self::TwentyEighth),
965 "29th" => Ok(Self::TwentyNinth),
966 "30th" => Ok(Self::Thirtieth),
967 "31st" => Ok(Self::ThirtyFirst),
968 _ => Err(crate::Error::InvalidDate),
969 }
970 }
971}
972
973#[derive(
974 Debug,
975 Clone,
976 Copy,
977 Serialize,
978 Deserialize,
979 PartialEq,
980 Eq,
981 PartialOrd,
982 Ord,
983 Hash,
984 derive_more::Display,
985)]
986#[display("{year}-{month}")]
987pub struct YearMonth {
988 year: Year,
989 month: Month,
990}
991
992impl YearMonth {
993 pub fn new(year: impl Into<Year>, month: Month) -> Self {
994 let year = year.into();
995 Self { year, month }
996 }
997
998 #[must_use]
999 pub fn year(self) -> Year {
1000 self.year
1001 }
1002
1003 #[must_use]
1004 pub fn month(self) -> Month {
1005 self.month
1006 }
1007
1008 #[must_use]
1009 pub fn first_day(self) -> Date {
1010 Date::from_ymd_opt(self.year, self.month.to_month_number(), 1).unwrap_or_default()
1011 }
1012
1013 #[must_use]
1015 pub fn last_day(self) -> Date {
1016 self.first_day().first_of_next_month().sub_days(1)
1018 }
1019
1020 #[must_use]
1021 pub fn to_numeric_string(self) -> String {
1022 format!(
1023 "{}-{:02}",
1024 i32::from(self.year),
1025 self.month.to_month_number()
1026 )
1027 }
1028
1029 #[must_use]
1031 pub fn parse(input: &str) -> Option<YearMonth> {
1032 let input = input.trim();
1033 let parts: Vec<&str> = input.split('-').collect();
1034 if parts.len() != 2 {
1035 return None;
1036 }
1037
1038 let year: i32 = parts[0].parse().ok()?;
1039 let month_num: u32 = parts[1].parse().ok()?;
1040 let month = Month::from_number(month_num)?;
1041
1042 Some(YearMonth::new(year, month))
1043 }
1044}
1045
1046impl From<Date> for YearMonth {
1047 fn from(value: Date) -> Self {
1048 value.year_month()
1049 }
1050}
1051
1052#[derive(
1054 Debug,
1055 Clone,
1056 Copy,
1057 Serialize,
1058 Deserialize,
1059 PartialEq,
1060 Eq,
1061 PartialOrd,
1062 Ord,
1063 Hash,
1064 derive_more::Display,
1065 strum::VariantArray,
1066)]
1067pub enum Quarter {
1068 #[display("Q1")]
1069 Q1,
1070 #[display("Q2")]
1071 Q2,
1072 #[display("Q3")]
1073 Q3,
1074 #[display("Q4")]
1075 Q4,
1076}
1077
1078impl Quarter {
1079 #[must_use]
1081 pub fn first_month(self) -> Month {
1082 match self {
1083 Quarter::Q1 => Month::January,
1084 Quarter::Q2 => Month::April,
1085 Quarter::Q3 => Month::July,
1086 Quarter::Q4 => Month::October,
1087 }
1088 }
1089
1090 #[must_use]
1092 pub fn last_month(self) -> Month {
1093 match self {
1094 Quarter::Q1 => Month::March,
1095 Quarter::Q2 => Month::June,
1096 Quarter::Q3 => Month::September,
1097 Quarter::Q4 => Month::December,
1098 }
1099 }
1100
1101 #[must_use]
1103 pub fn from_number(num: u8) -> Option<Self> {
1104 match num {
1105 1 => Some(Quarter::Q1),
1106 2 => Some(Quarter::Q2),
1107 3 => Some(Quarter::Q3),
1108 4 => Some(Quarter::Q4),
1109 _ => None,
1110 }
1111 }
1112
1113 #[must_use]
1115 pub fn to_number(self) -> u8 {
1116 match self {
1117 Quarter::Q1 => 1,
1118 Quarter::Q2 => 2,
1119 Quarter::Q3 => 3,
1120 Quarter::Q4 => 4,
1121 }
1122 }
1123}
1124
1125#[derive(
1127 Debug,
1128 Clone,
1129 Copy,
1130 Serialize,
1131 Deserialize,
1132 PartialEq,
1133 Eq,
1134 PartialOrd,
1135 Ord,
1136 Hash,
1137 derive_more::Display,
1138)]
1139#[display("{year}-{quarter}")]
1140pub struct YearQuarter {
1141 year: Year,
1142 quarter: Quarter,
1143}
1144
1145impl YearQuarter {
1146 #[must_use]
1147 pub fn new(year: impl Into<Year>, quarter: Quarter) -> Self {
1148 Self {
1149 year: year.into(),
1150 quarter,
1151 }
1152 }
1153
1154 #[must_use]
1155 pub fn year(self) -> Year {
1156 self.year
1157 }
1158
1159 #[must_use]
1160 pub fn quarter(self) -> Quarter {
1161 self.quarter
1162 }
1163
1164 #[must_use]
1166 pub fn first_day(self) -> Date {
1167 Date::from_ymd_opt(self.year, self.quarter.first_month().to_month_number(), 1)
1168 .unwrap_or_default()
1169 }
1170
1171 #[must_use]
1173 pub fn last_day(self) -> Date {
1174 let (month, day) = match self.quarter {
1175 Quarter::Q1 => (3, 31), Quarter::Q2 => (6, 30), Quarter::Q3 => (9, 30), Quarter::Q4 => (12, 31), };
1180 Date::from_ymd_opt(self.year, month, day).unwrap_or_default()
1181 }
1182
1183 #[must_use]
1185 pub fn parse(input: &str) -> Option<Self> {
1186 let input = input.trim();
1187 let parts: Vec<&str> = input.split('-').collect();
1188 if parts.len() != 2 {
1189 return None;
1190 }
1191
1192 let year: i32 = parts[0].parse().ok()?;
1193 let quarter_str = parts[1].to_lowercase();
1194 let quarter_num: u8 = quarter_str.strip_prefix('q')?.parse().ok()?;
1195 let quarter = Quarter::from_number(quarter_num)?;
1196
1197 Some(Self::new(year, quarter))
1198 }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use std::str::FromStr;
1204
1205 use super::*;
1206 use claims::{assert_err, assert_ok, assert_some};
1207 use pretty_assertions::assert_eq;
1208 use rstest::rstest;
1209 use tracing_test::traced_test;
1210
1211 const SAMPLE_DATE_STR: &str = "2025-01-30";
1212 static SAMPLE_DATE: Date = assert_some!(Date::from_ymd_opt(Year(2025), 1, 30));
1213
1214 #[test]
1215 fn test_to_iso_string() {
1216 assert_eq!(SAMPLE_DATE.into_iso_string(), SAMPLE_DATE_STR);
1217 assert!(Date::is_valid_date_str(SAMPLE_DATE_STR));
1218 }
1219
1220 #[test]
1221 fn test_try_from_iso_string_valid() {
1222 let result = assert_ok!(Date::from_str(SAMPLE_DATE_STR));
1223 assert_eq!(result, SAMPLE_DATE);
1224 }
1225
1226 #[test]
1227 fn test_try_from_iso_string_invalid() {
1228 let invalid_date_str = "January 1, 2025"; assert_err!(Date::from_str(invalid_date_str));
1230 assert!(!Date::is_valid_date_str(invalid_date_str));
1231 }
1232
1233 #[rstest]
1234 #[case::same_day("2025-06-07", Weekday::Saturday, "2025-06-14")]
1235 #[case::next_day("2025-06-07", Weekday::Sunday, "2025-06-08")]
1236 #[case::monday("2025-06-07", Weekday::Monday, "2025-06-09")]
1237 #[case::tuesday("2025-06-07", Weekday::Tuesday, "2025-06-10")]
1238 #[case::wednesday("2025-06-07", Weekday::Wednesday, "2025-06-11")]
1239 #[case::thursday("2025-06-07", Weekday::Thursday, "2025-06-12")]
1240 #[case::friday("2025-06-07", Weekday::Friday, "2025-06-13")]
1241 #[case::next_month("2025-06-30", Weekday::Friday, "2025-07-04")]
1242 #[case::next_year("2025-12-29", Weekday::Friday, "2026-01-02")]
1243 fn weekday_next_occurrence_after(
1244 #[case] from: &str,
1245 #[case] day: Weekday,
1246 #[case] expected: &str,
1247 ) {
1248 let from = Date::from_str_unchecked(from);
1249 let expected = Date::from_str_unchecked(expected);
1250 let actual = day.next_occurrence_after(from);
1251
1252 assert_eq!(actual, expected);
1253 }
1254
1255 #[traced_test]
1256 #[rstest]
1257 #[case::monday(Weekday::Monday, true)]
1258 #[case::tuesday(Weekday::Tuesday, true)]
1259 #[case::wednesday(Weekday::Wednesday, true)]
1260 #[case::thursday(Weekday::Thursday, true)]
1261 #[case::friday(Weekday::Friday, true)]
1262 #[case::saturday(Weekday::Saturday, false)]
1263 #[case::sunday(Weekday::Sunday, false)]
1264 fn weekday_is_weekday(#[case] input: Weekday, #[case] expected: bool) {
1265 let actual_weekday = input.is_weekday();
1266 let actual_weekend = input.is_weekend();
1267 assert_eq!(
1268 actual_weekday, expected,
1269 "expecting {input} weekday to be {expected}"
1270 );
1271 assert_eq!(
1272 actual_weekend, !expected,
1273 "expecting {input} is weekend to {}",
1274 !expected
1275 );
1276 }
1277
1278 #[rstest]
1279 #[case::january("2025-01-15", Month::January)]
1280 #[case::february("2025-02-28", Month::February)]
1281 #[case::march("2025-03-10", Month::March)]
1282 #[case::april("2025-04-01", Month::April)]
1283 #[case::may("2025-05-31", Month::May)]
1284 #[case::june("2025-06-15", Month::June)]
1285 #[case::july("2025-07-04", Month::July)]
1286 #[case::august("2025-08-20", Month::August)]
1287 #[case::september("2025-09-30", Month::September)]
1288 #[case::october("2025-10-12", Month::October)]
1289 #[case::november("2025-11-25", Month::November)]
1290 #[case::december("2025-12-31", Month::December)]
1291 #[case::january_different_year("2024-01-01", Month::January)]
1292 #[case::february_leap_year("2024-02-29", Month::February)]
1293 #[case::december_different_year("2026-12-01", Month::December)]
1294 fn date_has_month(#[case] date: &str, #[case] expected: Month) {
1295 let date = Date::from_str_unchecked(date);
1296
1297 let actual = date.month();
1298
1299 assert_eq!(actual, expected);
1300 }
1301
1302 #[rstest]
1303 #[case::first("1st", DayOfMonth::First)]
1304 #[case::second("2nd", DayOfMonth::Second)]
1305 #[case::third("3rd", DayOfMonth::Third)]
1306 #[case::fourth("4th", DayOfMonth::Fourth)]
1307 #[case::fifth("5th", DayOfMonth::Fifth)]
1308 #[case::sixth("6th", DayOfMonth::Sixth)]
1309 #[case::seventh("7th", DayOfMonth::Seventh)]
1310 #[case::eighth("8th", DayOfMonth::Eighth)]
1311 #[case::ninth("9th", DayOfMonth::Ninth)]
1312 #[case::tenth("10th", DayOfMonth::Tenth)]
1313 #[case::eleventh("11th", DayOfMonth::Eleventh)]
1314 #[case::twelfth("12th", DayOfMonth::Twelfth)]
1315 #[case::thirteenth("13th", DayOfMonth::Thirteenth)]
1316 #[case::fourteenth("14th", DayOfMonth::Fourteenth)]
1317 #[case::fifteenth("15th", DayOfMonth::Fifteenth)]
1318 #[case::sixteenth("16th", DayOfMonth::Sixteenth)]
1319 #[case::seventeenth("17th", DayOfMonth::Seventeenth)]
1320 #[case::eighteenth("18th", DayOfMonth::Eighteenth)]
1321 #[case::nineteenth("19th", DayOfMonth::Nineteenth)]
1322 #[case::twentieth("20th", DayOfMonth::Twentieth)]
1323 #[case::twenty_first("21st", DayOfMonth::TwentyFirst)]
1324 #[case::twenty_second("22nd", DayOfMonth::TwentySecond)]
1325 #[case::twenty_third("23rd", DayOfMonth::TwentyThird)]
1326 #[case::twenty_fourth("24th", DayOfMonth::TwentyFourth)]
1327 #[case::twenty_fifth("25th", DayOfMonth::TwentyFifth)]
1328 #[case::twenty_sixth("26th", DayOfMonth::TwentySixth)]
1329 #[case::twenty_seventh("27th", DayOfMonth::TwentySeventh)]
1330 #[case::twenty_eighth("28th", DayOfMonth::TwentyEighth)]
1331 #[case::twenty_ninth("29th", DayOfMonth::TwentyNinth)]
1332 #[case::thirtieth("30th", DayOfMonth::Thirtieth)]
1333 #[case::thirty_first("31st", DayOfMonth::ThirtyFirst)]
1334 fn day_of_month_parses_and_prints(#[case] input: &str, #[case] expected: DayOfMonth) {
1335 let actual = assert_ok!(DayOfMonth::from_str(input));
1336
1337 assert_eq!(
1338 actual, expected,
1339 "expecting {input} to parse as expected {expected}"
1340 );
1341 assert_eq!(
1342 expected.to_string(),
1343 input,
1344 "expecting day of month to print back out to the same as it was entered in"
1345 );
1346 }
1347
1348 #[rstest]
1349 #[case::same_date("2025-01-30", "2025-01-30", 0)]
1350 #[case::one_day_apart("2025-01-31", "2025-01-30", 24 * 60 * 60)] #[case::three_days_apart("2025-02-02", "2025-01-30", 3 * 24 * 60 * 60)] #[case::reversed_order("2025-01-30", "2025-02-02", 3 * 24 * 60 * 60)] fn date_subtraction_yields_duration(
1354 #[case] date1: &str,
1355 #[case] date2: &str,
1356 #[case] expected_seconds: u64,
1357 ) {
1358 let date1 = Date::from_str_unchecked(date1);
1359 let date2 = Date::from_str_unchecked(date2);
1360
1361 let duration = date1 - date2;
1362
1363 assert_eq!(
1364 duration.as_secs(),
1365 expected_seconds,
1366 "expected duration of {} seconds between {} and {}",
1367 expected_seconds,
1368 date1,
1369 date2
1370 );
1371 }
1372
1373 #[rstest]
1374 #[case("2025-01-01", "2025-01-01")]
1375 #[case("2025-01-02", "2025-01-01")]
1376 #[case("2025-06-30", "2025-01-01")]
1377 #[case("2025-12-31", "2025-01-01")]
1378 #[case("2026-01-01", "2026-01-01")]
1379 fn first_of_year(#[case] date: &str, #[case] expected: &str) {
1380 let date = Date::from_str_unchecked(date);
1381 let expected = Date::from_str_unchecked(expected);
1382 let actual = date.first_of_year();
1383 assert_eq!(actual, expected);
1384 }
1385
1386 #[rstest]
1387 #[case::january(Month::January, vec![Month::January])]
1388 #[case::february(Month::February, vec![Month::January, Month::February])]
1389 #[case::march(Month::March, vec![Month::January, Month::February, Month::March])]
1390 #[case::april(Month::April, vec![Month::January, Month::February, Month::March, Month::April])]
1391 #[case::may(Month::May, vec![Month::January, Month::February, Month::March, Month::April, Month::May])]
1392 #[case::june(Month::June, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June])]
1393 #[case::july(Month::July, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June, Month::July])]
1394 #[case::august(Month::August, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June, Month::July, Month::August])]
1395 #[case::september(Month::September, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June, Month::July, Month::August, Month::September])]
1396 #[case::october(Month::October, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June, Month::July, Month::August, Month::September, Month::October])]
1397 #[case::november(Month::November, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June, Month::July, Month::August, Month::September, Month::October, Month::November])]
1398 #[case::december(Month::December, vec![Month::January, Month::February, Month::March, Month::April, Month::May, Month::June, Month::July, Month::August, Month::September, Month::October, Month::November, Month::December])]
1399 fn months_until(#[case] value: Month, #[case] expected: Vec<Month>) {
1400 let actual = Month::months_until(value);
1401 assert_eq!(actual, expected);
1402 }
1403
1404 #[rstest]
1405 #[case::january(YearMonth::new(Year(2025), Month::January), "2025-01-01")]
1406 #[case::february(YearMonth::new(Year(2025), Month::February), "2025-02-01")]
1407 #[case::march(YearMonth::new(Year(2025), Month::March), "2025-03-01")]
1408 #[case::april(YearMonth::new(Year(2025), Month::April), "2025-04-01")]
1409 #[case::may(YearMonth::new(Year(2025), Month::May), "2025-05-01")]
1410 #[case::june(YearMonth::new(Year(2025), Month::June), "2025-06-01")]
1411 #[case::july(YearMonth::new(Year(2025), Month::July), "2025-07-01")]
1412 #[case::august(YearMonth::new(Year(2025), Month::August), "2025-08-01")]
1413 #[case::september(YearMonth::new(Year(2025), Month::September), "2025-09-01")]
1414 #[case::october(YearMonth::new(Year(2025), Month::October), "2025-10-01")]
1415 #[case::november(YearMonth::new(Year(2025), Month::November), "2025-11-01")]
1416 #[case::december(YearMonth::new(Year(2025), Month::December), "2025-12-01")]
1417 fn first_day_of_month(#[case] year_month: YearMonth, #[case] expected: &str) {
1418 let actual = year_month.first_day();
1419 assert_eq!(actual.into_iso_string(), expected);
1420
1421 let actual = actual.year_month();
1422 assert_eq!(actual, year_month);
1423 }
1424
1425 #[test]
1426 fn next_year() {
1427 let starting = Year(2024);
1428 let expected = Year(2025);
1429 let actual = starting.next();
1430 assert_eq!(actual, expected);
1431 }
1432
1433 #[rstest]
1434 #[case::january(Month::January, Month::February)]
1435 #[case::february(Month::February, Month::March)]
1436 #[case::march(Month::March, Month::April)]
1437 #[case::april(Month::April, Month::May)]
1438 #[case::may(Month::May, Month::June)]
1439 #[case::june(Month::June, Month::July)]
1440 #[case::july(Month::July, Month::August)]
1441 #[case::august(Month::August, Month::September)]
1442 #[case::september(Month::September, Month::October)]
1443 #[case::october(Month::October, Month::November)]
1444 #[case::november(Month::November, Month::December)]
1445 #[case::december(Month::December, Month::January)]
1446 fn next_month(#[case] starting: Month, #[case] expected: Month) {
1447 let actual = starting.next();
1448 assert_eq!(actual, expected);
1449 }
1450
1451 #[rstest]
1452 #[case::same_date("2026-02-02", "2026-02-02", 0)]
1453 #[case::one_day_future("2026-02-03", "2026-02-02", 1)]
1454 #[case::two_days_future("2026-02-04", "2026-02-02", 2)]
1455 #[case::one_day_past("2026-02-01", "2026-02-02", -1)]
1456 #[case::two_weeks_past("2026-01-19", "2026-02-02", -14)]
1457 #[case::two_weeks_future("2026-02-16", "2026-02-02", 14)]
1458 #[case::across_years("2027-02-02", "2026-02-02", 365)]
1459 fn signed_days_from(#[case] date: &str, #[case] reference: &str, #[case] expected: i64) {
1460 let date = Date::from_str_unchecked(date);
1461 let reference = Date::from_str_unchecked(reference);
1462
1463 let actual = date.signed_days_from(reference);
1464
1465 assert_eq!(actual, expected);
1466 }
1467
1468 #[test]
1469 fn end_of_day_utc_should_return_23_59_59() {
1470 let date = Date::from_str_unchecked("2026-03-15");
1471
1472 let result = date.end_of_day_utc();
1473
1474 assert_eq!(result.to_string(), "2026-03-15 23:59:59 UTC");
1475 }
1476
1477 #[test]
1478 fn start_of_day_utc_should_return_00_00_00() {
1479 let date = Date::from_str_unchecked("2026-03-15");
1480
1481 let result = date.start_of_day_utc();
1482
1483 assert_eq!(result.to_string(), "2026-03-15 00:00:00 UTC");
1484 }
1485
1486 #[rstest]
1487 #[case::mid_month("2026-03-15", "2026-02-28")]
1488 #[case::first_of_month("2026-03-01", "2026-02-28")]
1489 #[case::january_wraps_to_december("2026-01-10", "2025-12-31")]
1490 #[case::leap_year("2024-03-15", "2024-02-29")]
1491 fn last_day_of_previous_month_should_return_correct_date(
1492 #[case] input: &str,
1493 #[case] expected: &str,
1494 ) {
1495 let date = Date::from_str_unchecked(input);
1496
1497 let actual = date.last_day_of_previous_month();
1498
1499 assert_eq!(actual, Date::from_str_unchecked(expected));
1500 }
1501
1502 #[rstest]
1503 #[case::january(2025, Month::January, "2025-01")]
1504 #[case::september(2025, Month::September, "2025-09")]
1505 #[case::december(2025, Month::December, "2025-12")]
1506 fn year_month_to_numeric_string_should_zero_pad(
1507 #[case] year: i32,
1508 #[case] month: Month,
1509 #[case] expected: &str,
1510 ) {
1511 let ym = YearMonth::new(year, month);
1512
1513 let actual = ym.to_numeric_string();
1514
1515 assert_eq!(actual, expected);
1516 }
1517}