1use std::sync::LazyLock;
2
3use chrono::{DateTime, Duration, Local, NaiveTime, TimeZone};
4use doing_error::{Error, Result};
5use regex::Regex;
6
7use crate::parser::chronify;
8
9pub static RANGE_SEPARATOR_RE: LazyLock<Regex> =
10 LazyLock::new(|| Regex::new(r"(?i)\s+(?:to|through|thru|until|til)\s+|\s+--+\s+|\s+-\s+").unwrap());
11
12pub fn parse_range(input: &str) -> Result<(DateTime<Local>, DateTime<Local>)> {
29 let input = input.trim();
30
31 if input.is_empty() {
32 return Err(Error::InvalidTimeExpression("empty range input".into()));
33 }
34
35 let parts: Vec<&str> = RANGE_SEPARATOR_RE.splitn(input, 2).collect();
36
37 if parts.len() == 2 {
38 let a = chronify(parts[0])?;
39 let b = chronify(parts[1])?;
40 let (start, end) = if a > b { (b, a) } else { (a, b) };
42 let end = if end.time() == NaiveTime::MIN {
44 end + Duration::days(1)
45 } else {
46 end
47 };
48 return Ok((start, end));
49 }
50
51 let parsed = chronify(input)?;
53 let naive_date = parsed.date_naive();
54 let start = Local
55 .from_local_datetime(&naive_date.and_time(NaiveTime::MIN))
56 .single()
57 .ok_or_else(|| Error::InvalidTimeExpression(format!("ambiguous local time for: {input:?}")))?;
58 let end = start + Duration::days(1);
59
60 Ok((start, end))
61}
62
63#[cfg(test)]
64mod test {
65 use super::*;
66
67 mod parse_range {
68 use chrono::{Duration, NaiveDate, NaiveTime};
69 use pretty_assertions::assert_eq;
70
71 use super::*;
72
73 #[test]
74 fn it_parses_absolute_date_range() {
75 let (start, end) = parse_range("2024-01-01 to 2024-01-31").unwrap();
76
77 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
78 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
80 }
81
82 #[test]
83 fn it_parses_combined_expressions() {
84 let (start, end) = parse_range("yesterday 3pm to today").unwrap();
85 let now = Local::now();
86
87 assert_eq!(start.date_naive(), (now - Duration::days(1)).date_naive());
88 assert_eq!(start.time(), NaiveTime::from_hms_opt(15, 0, 0).unwrap());
89 assert_eq!(end.date_naive(), (now + Duration::days(1)).date_naive());
91 }
92
93 #[test]
94 fn it_parses_relative_range() {
95 let (start, end) = parse_range("yesterday to today").unwrap();
96 let now = Local::now();
97 let expected_start = (now - Duration::days(1)).date_naive();
98
99 assert_eq!(start.date_naive(), expected_start);
100 assert_eq!(end.date_naive(), (now + Duration::days(1)).date_naive());
102 }
103
104 #[test]
105 fn it_parses_single_absolute_date() {
106 let (start, end) = parse_range("2024-01-15").unwrap();
107
108 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
109 assert_eq!(start.time(), NaiveTime::MIN);
110 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 16).unwrap());
111 assert_eq!(end.time(), NaiveTime::MIN);
112 }
113
114 #[test]
115 fn it_parses_single_relative_date() {
116 let (start, end) = parse_range("yesterday").unwrap();
117 let now = Local::now();
118 let expected_date = (now - Duration::days(1)).date_naive();
119
120 assert_eq!(start.date_naive(), expected_date);
121 assert_eq!(start.time(), NaiveTime::MIN);
122 assert_eq!(end, start + Duration::days(1));
123 }
124
125 #[test]
126 fn it_normalizes_reversed_range() {
127 let (start, end) = parse_range("2024-01-31 to 2024-01-01").unwrap();
128
129 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
130 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
131 }
132
133 #[test]
134 fn it_rejects_empty_input() {
135 let err = parse_range("").unwrap_err();
136
137 assert!(matches!(err, Error::InvalidTimeExpression(_)));
138 }
139
140 #[test]
141 fn it_rejects_invalid_date_expressions() {
142 let err = parse_range("gibberish to nonsense").unwrap_err();
143
144 assert!(matches!(err, Error::InvalidTimeExpression(_)));
145 }
146
147 #[test]
148 fn it_rejects_invalid_single_date() {
149 let err = parse_range("gibberish").unwrap_err();
150
151 assert!(matches!(err, Error::InvalidTimeExpression(_)));
152 }
153
154 #[test]
155 fn it_supports_dash_separator() {
156 let (start, end) = parse_range("2024-01-01 -- 2024-01-31").unwrap();
157
158 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
159 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
160 }
161
162 #[test]
163 fn it_supports_single_dash_with_iso_dates() {
164 let (start, end) = parse_range("2024-01-01 - 2024-01-31").unwrap();
165
166 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
167 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
168 }
169
170 #[test]
171 fn it_supports_through_separator() {
172 let (start, end) = parse_range("2024-01-01 through 2024-01-31").unwrap();
173
174 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
175 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
176 }
177
178 #[test]
179 fn it_supports_thru_separator() {
180 let (start, end) = parse_range("2024-01-01 thru 2024-01-31").unwrap();
181
182 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
183 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
184 }
185
186 #[test]
187 fn it_supports_until_separator() {
188 let (start, end) = parse_range("2024-01-01 until 2024-01-31").unwrap();
189
190 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
191 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
192 }
193 }
194}