gie_client/common/
date_range.rs1use std::ops::RangeInclusive;
2
3use crate::error::GieError;
4
5use super::types::{GieDate, format_date};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct DateRange {
10 start: GieDate,
11 end: GieDate,
12}
13
14impl DateRange {
15 pub fn new(start: GieDate, end: GieDate) -> Result<Self, GieError> {
17 if start <= end {
18 Ok(Self { start, end })
19 } else {
20 Err(GieError::InvalidDateRangeInput(format!(
21 "from must be less than or equal to to (from={}, to={})",
22 format_date(start),
23 format_date(end)
24 )))
25 }
26 }
27
28 pub fn start(self) -> GieDate {
30 self.start
31 }
32
33 pub fn end(self) -> GieDate {
35 self.end
36 }
37
38 pub fn from(self) -> GieDate {
40 self.start()
41 }
42
43 pub fn to(self) -> GieDate {
45 self.end()
46 }
47
48 pub fn contains(self, date: GieDate) -> bool {
50 self.start <= date && date <= self.end
51 }
52
53 pub fn intersects(self, other: Self) -> bool {
55 self.start <= other.end && other.start <= self.end
56 }
57
58 pub fn is_single_day(self) -> bool {
60 self.start == self.end
61 }
62
63 pub fn into_bounds(self) -> (GieDate, GieDate) {
65 (self.start, self.end)
66 }
67
68 pub fn as_inclusive(self) -> RangeInclusive<GieDate> {
70 self.start..=self.end
71 }
72}
73
74impl TryFrom<(GieDate, GieDate)> for DateRange {
75 type Error = GieError;
76
77 fn try_from(value: (GieDate, GieDate)) -> Result<Self, Self::Error> {
78 Self::new(value.0, value.1)
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::common::types::parse_date;
86
87 fn test_date(value: &str) -> GieDate {
88 parse_date(value).unwrap()
89 }
90
91 #[test]
92 fn accepts_valid_bounds() {
93 let range = DateRange::new(test_date("2026-03-01"), test_date("2026-03-10")).unwrap();
94
95 assert_eq!(range.start(), test_date("2026-03-01"));
96 assert_eq!(range.end(), test_date("2026-03-10"));
97 }
98
99 #[test]
100 fn rejects_invalid_bounds() {
101 let error = DateRange::new(test_date("2026-03-10"), test_date("2026-03-01")).unwrap_err();
102 assert!(matches!(error, GieError::InvalidDateRangeInput(_)));
103 }
104
105 #[test]
106 fn keeps_from_to_aliases() {
107 let range = DateRange::new(test_date("2026-03-01"), test_date("2026-03-10")).unwrap();
108
109 assert_eq!(range.from(), test_date("2026-03-01"));
110 assert_eq!(range.to(), test_date("2026-03-10"));
111 }
112
113 #[test]
114 fn detects_contains_correctly() {
115 let range = DateRange::new(test_date("2026-03-01"), test_date("2026-03-10")).unwrap();
116
117 assert!(range.contains(test_date("2026-03-01")));
118 assert!(range.contains(test_date("2026-03-10")));
119 assert!(range.contains(test_date("2026-03-05")));
120 assert!(!range.contains(test_date("2026-02-28")));
121 assert!(!range.contains(test_date("2026-03-11")));
122 }
123
124 #[test]
125 fn detects_intersection_correctly() {
126 let left = DateRange::new(test_date("2026-03-01"), test_date("2026-03-10")).unwrap();
127 let overlap = DateRange::new(test_date("2026-03-10"), test_date("2026-03-15")).unwrap();
128 let disjoint = DateRange::new(test_date("2026-03-11"), test_date("2026-03-20")).unwrap();
129
130 assert!(left.intersects(overlap));
131 assert!(!left.intersects(disjoint));
132 }
133
134 #[test]
135 fn exposes_shape_helpers() {
136 let single = DateRange::new(test_date("2026-03-05"), test_date("2026-03-05")).unwrap();
137 let multiple = DateRange::new(test_date("2026-03-01"), test_date("2026-03-10")).unwrap();
138
139 assert!(single.is_single_day());
140 assert!(!multiple.is_single_day());
141 assert_eq!(
142 multiple.into_bounds(),
143 (test_date("2026-03-01"), test_date("2026-03-10"))
144 );
145 }
146
147 #[test]
148 fn can_be_built_via_try_from_tuple() {
149 let range =
150 DateRange::try_from((test_date("2026-03-01"), test_date("2026-03-10"))).unwrap();
151
152 assert_eq!(range.start(), test_date("2026-03-01"));
153 assert_eq!(range.end(), test_date("2026-03-10"));
154 }
155}