1#![forbid(unsafe_code)]
2use use_date::{add_days, days_between, CalendarDate};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct DateRange {
28 start: CalendarDate,
29 end: CalendarDate,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DateRangeError {
34 InvalidRange,
35}
36
37impl DateRange {
38 pub fn new(start: CalendarDate, end: CalendarDate) -> Result<Self, DateRangeError> {
39 if start > end {
40 return Err(DateRangeError::InvalidRange);
41 }
42
43 Ok(Self { start, end })
44 }
45
46 #[must_use]
47 pub fn start(&self) -> CalendarDate {
48 self.start
49 }
50
51 #[must_use]
52 pub fn end(&self) -> CalendarDate {
53 self.end
54 }
55
56 #[must_use]
57 pub fn contains(&self, date: CalendarDate) -> bool {
58 self.start <= date && date <= self.end
59 }
60
61 #[must_use]
62 pub fn duration_days(&self) -> i64 {
63 days_between(self.start, self.end)
64 }
65}
66
67pub fn date_range(
68 start: CalendarDate,
69 end: CalendarDate,
70) -> Result<Vec<CalendarDate>, DateRangeError> {
71 let range = DateRange::new(start, end)?;
72
73 Ok((0..=range.duration_days())
74 .map(|offset| add_days(start, offset))
75 .collect())
76}
77
78#[must_use]
79pub fn overlaps(a: DateRange, b: DateRange) -> bool {
80 a.start <= b.end && b.start <= a.end
81}
82
83#[must_use]
84pub fn intersection(a: DateRange, b: DateRange) -> Option<DateRange> {
85 if !overlaps(a, b) {
86 return None;
87 }
88
89 DateRange::new(a.start.max(b.start), a.end.min(b.end)).ok()
90}
91
92#[cfg(test)]
93mod tests {
94 use super::{date_range, intersection, overlaps, DateRange, DateRangeError};
95 use use_date::CalendarDate;
96
97 #[test]
98 fn checks_range_containment_and_duration() {
99 let start = CalendarDate::new(2024, 1, 1).unwrap();
100 let end = CalendarDate::new(2024, 1, 3).unwrap();
101 let range = DateRange::new(start, end).unwrap();
102
103 assert_eq!(range.start(), start);
104 assert_eq!(range.end(), end);
105 assert!(range.contains(CalendarDate::new(2024, 1, 2).unwrap()));
106 assert_eq!(range.duration_days(), 2);
107 assert_eq!(date_range(start, end).unwrap().len(), 3);
108 }
109
110 #[test]
111 fn computes_range_overlap_and_intersection() {
112 let a = DateRange::new(
113 CalendarDate::new(2024, 1, 1).unwrap(),
114 CalendarDate::new(2024, 1, 5).unwrap(),
115 )
116 .unwrap();
117 let b = DateRange::new(
118 CalendarDate::new(2024, 1, 4).unwrap(),
119 CalendarDate::new(2024, 1, 7).unwrap(),
120 )
121 .unwrap();
122 let c = DateRange::new(
123 CalendarDate::new(2024, 1, 6).unwrap(),
124 CalendarDate::new(2024, 1, 8).unwrap(),
125 )
126 .unwrap();
127
128 assert!(overlaps(a, b));
129 assert!(!overlaps(a, c));
130 assert_eq!(
131 intersection(a, b).unwrap(),
132 DateRange::new(
133 CalendarDate::new(2024, 1, 4).unwrap(),
134 CalendarDate::new(2024, 1, 5).unwrap()
135 )
136 .unwrap()
137 );
138 assert!(intersection(a, c).is_none());
139 }
140
141 #[test]
142 fn rejects_reversed_ranges() {
143 let start = CalendarDate::new(2024, 1, 5).unwrap();
144 let end = CalendarDate::new(2024, 1, 1).unwrap();
145
146 assert_eq!(
147 DateRange::new(start, end),
148 Err(DateRangeError::InvalidRange)
149 );
150 assert_eq!(date_range(start, end), Err(DateRangeError::InvalidRange));
151 }
152}