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|-{1,})\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 start = chronify(parts[0])?;
39 let end = chronify(parts[1])?;
40 let end = if end.time() == NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
42 end + Duration::days(1)
43 } else {
44 end
45 };
46 return Ok((start, end));
47 }
48
49 let parsed = chronify(input)?;
51 let naive_date = parsed.date_naive();
52 let start = Local
53 .from_local_datetime(&naive_date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()))
54 .single()
55 .ok_or_else(|| Error::InvalidTimeExpression(format!("ambiguous local time for: {input:?}")))?;
56 let end = start + Duration::days(1);
57
58 Ok((start, end))
59}
60
61#[cfg(test)]
62mod test {
63 use super::*;
64
65 mod parse_range {
66 use chrono::{Duration, NaiveDate, NaiveTime};
67 use pretty_assertions::assert_eq;
68
69 use super::*;
70
71 #[test]
72 fn it_parses_absolute_date_range() {
73 let (start, end) = parse_range("2024-01-01 to 2024-01-31").unwrap();
74
75 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
76 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
78 }
79
80 #[test]
81 fn it_parses_combined_expressions() {
82 let (start, end) = parse_range("yesterday 3pm to today").unwrap();
83 let now = Local::now();
84
85 assert_eq!(start.date_naive(), (now - Duration::days(1)).date_naive());
86 assert_eq!(start.time(), NaiveTime::from_hms_opt(15, 0, 0).unwrap());
87 assert_eq!(end.date_naive(), (now + Duration::days(1)).date_naive());
89 }
90
91 #[test]
92 fn it_parses_relative_range() {
93 let (start, end) = parse_range("yesterday to today").unwrap();
94 let now = Local::now();
95 let expected_start = (now - Duration::days(1)).date_naive();
96
97 assert_eq!(start.date_naive(), expected_start);
98 assert_eq!(end.date_naive(), (now + Duration::days(1)).date_naive());
100 }
101
102 #[test]
103 fn it_rejects_empty_input() {
104 let err = parse_range("").unwrap_err();
105
106 assert!(matches!(err, Error::InvalidTimeExpression(_)));
107 }
108
109 #[test]
110 fn it_parses_single_absolute_date() {
111 let (start, end) = parse_range("2024-01-15").unwrap();
112
113 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
114 assert_eq!(start.time(), NaiveTime::from_hms_opt(0, 0, 0).unwrap());
115 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 16).unwrap());
116 assert_eq!(end.time(), NaiveTime::from_hms_opt(0, 0, 0).unwrap());
117 }
118
119 #[test]
120 fn it_parses_single_relative_date() {
121 let (start, end) = parse_range("yesterday").unwrap();
122 let now = Local::now();
123 let expected_date = (now - Duration::days(1)).date_naive();
124
125 assert_eq!(start.date_naive(), expected_date);
126 assert_eq!(start.time(), NaiveTime::from_hms_opt(0, 0, 0).unwrap());
127 assert_eq!(end, start + Duration::days(1));
128 }
129
130 #[test]
131 fn it_rejects_invalid_date_expressions() {
132 let err = parse_range("gibberish to nonsense").unwrap_err();
133
134 assert!(matches!(err, Error::InvalidTimeExpression(_)));
135 }
136
137 #[test]
138 fn it_rejects_invalid_single_date() {
139 let err = parse_range("gibberish").unwrap_err();
140
141 assert!(matches!(err, Error::InvalidTimeExpression(_)));
142 }
143
144 #[test]
145 fn it_supports_dash_separator() {
146 let (start, end) = parse_range("2024-01-01 -- 2024-01-31").unwrap();
147
148 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
149 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
150 }
151
152 #[test]
153 fn it_supports_through_separator() {
154 let (start, end) = parse_range("2024-01-01 through 2024-01-31").unwrap();
155
156 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
157 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
158 }
159
160 #[test]
161 fn it_supports_thru_separator() {
162 let (start, end) = parse_range("2024-01-01 thru 2024-01-31").unwrap();
163
164 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
165 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
166 }
167
168 #[test]
169 fn it_supports_until_separator() {
170 let (start, end) = parse_range("2024-01-01 until 2024-01-31").unwrap();
171
172 assert_eq!(start.date_naive(), NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
173 assert_eq!(end.date_naive(), NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
174 }
175 }
176}