Skip to main content

timelog/cli/
args.rs

1//! Structs defining different sets of arguments supplied on command line
2//!
3//! # Description
4//!
5//! - [`DateRangeArgs`] - representation of the start/end dates for a report.
6//! - [`FilterArgs`] - representation of the start/end dates and project list for reports.
7use 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
17/// Trait specifying common functionality for the different filter arguments.
18pub trait DayFilter {
19    /// Return the start date as a [`String`]
20    fn start(&self) -> String;
21    /// Return the end date as a [`String`]
22    fn end(&self) -> String;
23    /// Return a [`Day`] object after appropriate filtering
24    fn filter_day(&self, day: Day) -> Option<Day>;
25}
26
27// Return Some [`Day`] if the supplied day is not empty.
28fn day_with_entries(day: Day) -> Option<Day> { (!day.is_empty()).then_some(day) }
29
30/// Representation of the start/end date and project list arguments for reports.
31#[derive(Debug)]
32pub struct FilterArgs {
33    range:    DateRange,
34    projects: Option<Regex>
35}
36
37// Return a [`Regex`] one of the supplied projects
38fn 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    /// Create the [`FilterArgs`] from an array of date range description strings, and an array of
53    /// projects.
54    ///
55    /// # Errors
56    ///
57    /// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
58    /// - Return [`Error::DateError`] if the start date is not before the end date
59    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    // Return an [`Option<&Regex>`] that determines how to match projects
70    fn projects(&self) -> Option<&Regex> { self.projects.as_ref() }
71}
72
73impl DayFilter for FilterArgs {
74    /// Return the start date as a [`String`]
75    fn start(&self) -> String { self.range.start().to_string() }
76
77    /// Return the end date as a [`String`]
78    fn end(&self) -> String { self.range.end().to_string() }
79
80    /// Return a [`Day`] object filtered as needed
81    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/// Representation of the start and end date arguments for reports.
91#[derive(Debug, PartialEq, Eq)]
92pub struct DateRangeArgs {
93    range: DateRange
94}
95
96impl DateRangeArgs {
97    /// Create the [`DateRangeArgs`] from an array of strings.
98    ///
99    /// # Errors
100    ///
101    /// - Return [`Error::DateError`] if the start date is not before the end date
102    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    /// Return the start date as a [`String`]
111    pub fn start(&self) -> String { self.range.start().to_string() }
112
113    /// Return the end date as a [`String`]
114    pub fn end(&self) -> String { self.range.end().to_string() }
115}
116
117impl DayFilter for DateRangeArgs {
118    /// Return the start date as a [`String`]
119    fn start(&self) -> String { self.start() }
120
121    /// Return the end date as a [`String`]
122    fn end(&self) -> String { self.end() }
123
124    /// Return a [`Day`] object filtered as needed. If the day is empty,
125    /// return None.
126    fn filter_day(&self, day: Day) -> Option<Day> { day_with_entries(day) }
127}
128
129// Only used for testing, not particularly performant.
130#[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 Filter
151
152    #[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 DateRange
255
256    #[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}