1use regex::Regex;
8
9#[doc(inline)]
10use crate::date::DateRange;
11#[doc(inline)]
12use crate::date::RangeParser;
13#[doc(inline)]
14use crate::error::Error;
15use crate::Day;
16
17pub trait DayFilter {
19 fn start(&self) -> String;
21 fn end(&self) -> String;
23 fn filter_day(&self, day: Day) -> Option<Day>;
25}
26
27fn day_with_entries(day: Day) -> Option<Day> { (!day.is_empty()).then_some(day) }
29
30#[derive(Debug)]
32pub struct FilterArgs {
33 range: DateRange,
34 projects: Option<Regex>
35}
36
37fn regex_from_projs(projs: &[&str]) -> crate::Result<Regex> {
39 Regex::new(&projs.join("|")).map_err(|_| Error::BadProjectFilter)
40}
41
42fn make_projects_regex_opt(projs: &[&str]) -> crate::Result<Option<Regex>> {
43 if projs.is_empty() {
44 Ok(None)
45 }
46 else {
47 regex_from_projs(projs).map(Some)
48 }
49}
50
51impl FilterArgs {
52 pub fn new(dates: &[String], projs: &[String]) -> crate::Result<Self> {
60 let project_list: Vec<&str> = projs.iter().map(String::as_str).collect();
61
62 let mut date_iter = dates.iter().map(String::as_str);
63 let parser = RangeParser::default();
64 let (range, _token) = parser.parse(&mut date_iter)?;
65
66 Ok(Self { range, projects: make_projects_regex_opt(&project_list)? })
67 }
68
69 fn projects(&self) -> Option<&Regex> { self.projects.as_ref() }
71}
72
73impl DayFilter for FilterArgs {
74 fn start(&self) -> String { self.range.start().to_string() }
76
77 fn end(&self) -> String { self.range.end().to_string() }
79
80 fn filter_day(&self, day: Day) -> Option<Day> {
82 day_with_entries(
83 self.projects()
84 .map(|re| day.filtered_by_project(re))
85 .unwrap_or(day)
86 )
87 }
88}
89
90#[derive(Debug, PartialEq, Eq)]
92pub struct DateRangeArgs {
93 range: DateRange
94}
95
96impl DateRangeArgs {
97 pub fn new(dates: &[String]) -> crate::Result<Self> {
103 let mut date_iter = dates.iter().map(String::as_str);
104 let parser = RangeParser::default();
105 let (range, _token) = parser.parse(&mut date_iter)?;
106
107 Ok(Self { range })
108 }
109
110 pub fn start(&self) -> String { self.range.start().to_string() }
112
113 pub fn end(&self) -> String { self.range.end().to_string() }
115}
116
117impl DayFilter for DateRangeArgs {
118 fn start(&self) -> String { self.start() }
120
121 fn end(&self) -> String { self.end() }
123
124 fn filter_day(&self, day: Day) -> Option<Day> { day_with_entries(day) }
127}
128
129#[cfg(test)]
131impl PartialEq for FilterArgs {
132 fn eq(&self, other: &Self) -> bool {
133 (self.start() == other.start())
134 && (self.end() == other.end())
135 && match (self.projects(), other.projects()) {
136 (None, None) => true,
137 (None, _) | (_, None) => false,
138 (Some(lhs), Some(rhs)) => format!("{lhs:?}") == format!("{rhs:?}")
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use assert2::{assert, let_assert};
146
147 use super::*;
148 use crate::Date;
149
150 #[test]
153 fn test_filter_no_args() {
154 let args = vec![];
155 let expected = FilterArgs {
156 range: DateRange::new(Date::today(), Date::today().succ()),
157 projects: None
158 };
159
160 let_assert!(Ok(actual) = FilterArgs::new(&args, &[]));
161 assert!(actual == expected);
162 }
163
164 #[test]
165 fn test_filter_just_one_date() {
166 let args = vec!["yesterday".to_string()];
167 let expected = FilterArgs {
168 range: DateRange::new(Date::today().pred(), Date::today()),
169 projects: None
170 };
171
172 let_assert!(Ok(actual) = FilterArgs::new(&args, &[]));
173 assert!(actual == expected);
174 }
175
176 #[test]
177 fn test_filter_just_two_dates() {
178 let args = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
179 let_assert!(Ok(start) = Date::new(2021, 12, 1));
180 let_assert!(Ok(end) = Date::new(2021, 12, 8));
181 let expected = FilterArgs {
182 range: DateRange::new(start, end),
183 projects: None
184 };
185
186 let_assert!(Ok(actual) = FilterArgs::new(&args, &[]));
187 assert!(actual == expected);
188 }
189
190 #[test]
191 fn test_filter_just_project() {
192 let dates = vec![];
193 let proj = vec!["project1".to_string()];
194 let_assert!(Ok(regex) = Regex::new(r"project1"));
195 let expected = FilterArgs {
196 range: DateRange::new(Date::today(), Date::today().succ()),
197 projects: Some(regex)
198 };
199
200 let_assert!(Ok(actual) = FilterArgs::new(&dates, &proj));
201 assert!(actual == expected);
202 }
203
204 #[test]
205 fn test_filter_just_multiple_projects() {
206 let dates = vec![];
207 let projs = vec![
208 "project1".to_string(),
209 "cleanup".to_string(),
210 "profit".to_string(),
211 ];
212 let_assert!(Ok(regex) = Regex::new(r"project1|cleanup|profit"));
213 let expected = FilterArgs {
214 range: DateRange::new(Date::today(), Date::today().succ()),
215 projects: Some(regex)
216 };
217
218 let_assert!(Ok(actual) = FilterArgs::new(&dates, &projs));
219 assert!(actual == expected);
220 }
221
222 #[test]
223 fn test_filter_start_and_project() {
224 let dates = vec!["2021-12-01".to_string()];
225 let projs = vec!["project1".to_string()];
226 let_assert!(Ok(start) = Date::new(2021, 12, 1));
227 let_assert!(Ok(end) = Date::new(2021, 12, 2));
228 let_assert!(Ok(regex) = Regex::new(r"project1"));
229 let expected = FilterArgs {
230 range: DateRange::new(start, end),
231 projects: Some(regex)
232 };
233
234 let_assert!(Ok(actual) = FilterArgs::new(&dates, &projs));
235 assert!(actual == expected);
236 }
237
238 #[test]
239 fn test_filter_both_dates_and_project() {
240 let dates = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
241 let projs = vec!["project1".to_string()];
242 let_assert!(Ok(start) = Date::new(2021, 12, 1));
243 let_assert!(Ok(end) = Date::new(2021, 12, 8));
244 let_assert!(Ok(regex) = Regex::new(r"project1"));
245 let expected = FilterArgs {
246 range: DateRange::new(start, end),
247 projects: Some(regex)
248 };
249
250 let_assert!(Ok(actual) = FilterArgs::new(&dates, &projs));
251 assert!(actual == expected);
252 }
253
254 #[test]
257 fn test_dates_no_args() {
258 let args = vec![];
259 #[rustfmt::skip]
260 let expected = DateRangeArgs {
261 range: DateRange::new(Date::today(), Date::today().succ())
262 };
263
264 let_assert!(Ok(actual) = DateRangeArgs::new(&args));
265 assert!(actual == expected);
266 }
267
268 #[test]
269 fn test_dates_just_one_date() {
270 let args = vec!["yesterday".to_string()];
271 #[rustfmt::skip]
272 let expected = DateRangeArgs {
273 range: DateRange::new(Date::today().pred(), Date::today())
274 };
275
276 let_assert!(Ok(actual) = DateRangeArgs::new(&args));
277 assert!(actual == expected);
278 }
279
280 #[test]
281 fn test_dates_both_dates() {
282 let args = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
283 let_assert!(Ok(start) = Date::new(2021, 12, 1));
284 let_assert!(Ok(end) = Date::new(2021, 12, 8));
285 let expected = DateRangeArgs {
286 range: DateRange::new(start, end)
287 };
288
289 let_assert!(Ok(actual) = DateRangeArgs::new(&args));
290 assert!(actual == expected);
291 }
292
293 fn create_day_with_data() -> crate::Result<Day> {
294 use crate::entry::Entry;
295
296 let mut day = Day::new("2021-07-02")?;
297 [
298 "2021-07-01 10:00:00 +project @task",
299 "2021-07-01 10:05:00 +project @task2",
300 "2021-07-01 10:10:00 stop",
301 ].iter().for_each(|ln| {
302 day.add_entry(Entry::from_line(ln).expect("entry add")).expect("entry add");
303 });
304 Ok(day)
305 }
306
307 fn create_empty_day() -> crate::Result<Day> {
308 Day::new("2021-07-02")
309 }
310
311 #[test]
312 fn test_day_with_entries() {
313 let_assert!(Ok(day) = create_day_with_data());
314 assert!(day_with_entries(day).is_some());
315 }
316
317 #[test]
318 fn test_day_filter_some() {
319 let_assert!(Ok(day) = create_day_with_data());
320 let_assert!(Ok(filt) = FilterArgs::new(&[String::from("2021-07-02")], &[String::from("project")]));
321 let_assert!(Some(_) = filt.filter_day(day));
322 }
323
324 #[test]
325 fn test_day_filter_dates() {
326 let_assert!(Ok(filt) = FilterArgs::new(&[String::from("2021-07-02")], &[String::from("project")]));
327 assert!(filt.start() == String::from("2021-07-02"));
328 assert!(filt.end() == String::from("2021-07-03"));
329 }
330
331 #[test]
332 fn test_date_range_filter_dates() {
333 let_assert!(Ok(filt) = DateRangeArgs::new(&[String::from("2021-07-02")]));
334 assert!(filt.start() == String::from("2021-07-02"));
335 assert!(filt.end() == String::from("2021-07-03"));
336 }
337
338 #[test]
339 fn test_day_with_entries_none() {
340 let_assert!(Ok(day) = create_empty_day());
341 assert!(day_with_entries(day).is_none());
342 }
343
344 #[test]
345 fn test_day_filter_none() {
346 let_assert!(Ok(day) = create_empty_day());
347 let_assert!(Ok(filt) = FilterArgs::new(&[String::from("2021-07-10")], &[String::from("project")]));
348 assert!(filt.filter_day(day).is_none());
349 }
350}