timelog/
task_line_iter.rs

1//! Iterator filter for walking relevant task lines from an iterator
2//!
3//! Given an iterator, a start date, and an end date, gives a new iterator
4//! that only returns the lines starting from the first on or after the start date
5//! and ending before the end date.
6//!
7//! # Examples
8//!
9//! ```rust, no_run
10//! use std::io::{BufRead, BufReader};
11//! use timelog::task_line_iter::TaskLineIter;
12//! # fn main() -> Result<(), std::io::Error> {
13//! # let file = std::fs::File::open("timeline.txt")?;
14//! let iter = TaskLineIter::new(
15//!               BufReader::new(file).lines().take_while(|ol| ol.is_ok())
16//!                                           .map(|ol| ol.unwrap()),
17//!               "2021-06-06", "2021-06-08"
18//!            );
19//! #  Ok(())
20//! # }
21//! ```
22//!
23//! Any iterator returning strings will work. Getting lines from a file, takes a bit
24//! more effort.
25
26use std::fmt::{self, Debug};
27use std::result;
28
29use once_cell::sync::Lazy;
30use regex::Regex;
31
32#[doc(inline)]
33use crate::date::DateError;
34use crate::error::Error;
35use crate::Entry;
36
37// States for determining how we transition through the file.
38#[derive(Debug)]
39enum Stage {
40    // Before the time range of interest
41    Before,
42    // In the time range of interest
43    In,
44    // After the time range of interest
45    After
46}
47
48/// Iterator that walks lines of interest in the timelog file.
49pub struct TaskLineIter<'a, I>
50where
51    I: Iterator<Item = String>
52{
53    /// The internal iterator for walking timelog lines
54    iter:  I,
55    /// What state are we in while walking lines
56    stage: Stage,
57    /// Criteria for the start of the lines of interest
58    start: &'a str,
59    /// Criteria for the end of the lines of interest
60    end:   &'a str
61}
62
63impl<'a, I> Debug for TaskLineIter<'a, I>
64where
65    I: Iterator<Item = String>
66{
67    /// Debug formatting for the iterator
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> result::Result<(), fmt::Error> {
69        f.debug_struct("TaskLineIter")
70            .field("iter", &"Iter")
71            .field("stage", &self.stage)
72            .field("start", &self.start)
73            .field("end", &self.end)
74            .finish()
75    }
76}
77
78// Should never fail, because hardcoded input.
79// The expect() provides a little context if code is ever changed to break the regex.
80
81/// Regular expression for matching the full date from a task line.
82static DATE: Lazy<Regex> =
83    Lazy::new(|| Regex::new(r"^\d{4}-\d\d-\d\d$").expect("Date regex invalid"));
84
85impl<'a, I> TaskLineIter<'a, I>
86where
87    I: Iterator<Item = String>
88{
89    /// Create a new iterator based on the supplied iterator limited to the
90    /// date strings specified.
91    ///
92    /// # Errors
93    ///
94    /// - Return [`Error::StartDateFormat`] if the start date is not 'YYYY-MM-DD'.
95    /// - Return [`Error::EndDateFormat`] if the end date is not 'YYYY-MM-DD'.
96    /// - Return [`Error::DateError`] if the end date is before the start date.
97    pub fn new(iter: I, start: &'a str, end: &'a str) -> crate::Result<Self> {
98        if !DATE.is_match(start) {
99            return Err(Error::StartDateFormat);
100        }
101        if !DATE.is_match(end) {
102            return Err(Error::EndDateFormat);
103        }
104        if start >= end {
105            return Err(DateError::WrongDateOrder.into());
106        }
107
108        Ok(Self { iter, stage: Stage::Before, start, end })
109    }
110
111    /// Find the last line before the point of lines of interest if any, consuming the iterator in
112    /// the process.
113    pub fn last_line_before(mut self) -> Option<String> {
114        let mut prev: Option<String> = None;
115        for line in self.iter.by_ref() {
116            if let Some(date) = Entry::date_from_line(&line) {
117                if self.start <= date && date < self.end {
118                    return prev;
119                }
120                prev = Some(line);
121            }
122        }
123        None
124    }
125
126    // Find the first line that meets the date requirements.
127    fn find_first(&mut self) -> Option<String> {
128        for line in self.iter.by_ref() {
129            if let Some(date) = Entry::date_from_line(&line) {
130                if self.start <= date && date < self.end {
131                    return Some(line);
132                }
133            }
134        }
135        None
136    }
137
138    // Assuming we have met the start requirement get the next line that does not
139    // exceed the end requirement.
140    fn get_next(&mut self) -> Option<String> {
141        if let Some(line) = self.iter.next() {
142            if let Some(date) = Entry::date_from_line(&line) {
143                if date < self.end {
144                    return Some(line);
145                }
146            }
147        }
148        None
149    }
150}
151
152impl<'a, I> Iterator for TaskLineIter<'a, I>
153where
154    I: Iterator<Item = String>
155{
156    type Item = String;
157
158    /// Iterator method returning the next line meeting the start and end requirements
159    fn next(&mut self) -> Option<Self::Item> {
160        match self.stage {
161            Stage::Before => {
162                if let Some(line) = self.find_first() {
163                    self.stage = Stage::In;
164                    return Some(line);
165                }
166            }
167            Stage::In => {
168                if let Some(line) = self.get_next() {
169                    return Some(line);
170                }
171            }
172            Stage::After => {}
173        }
174        self.stage = Stage::After;
175        None
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use spectral::prelude::*;
182
183    use super::*;
184    use crate::date::DateError;
185
186    #[test]
187    fn test_successful_new() {
188        let tasks: Vec<String> = Vec::new();
189
190        let mut e_iter = tasks.into_iter();
191        assert_that!(TaskLineIter::new(&mut e_iter, "2021-06-17", "2021-06-18")).is_ok();
192    }
193
194    #[test]
195    fn test_new_bad_start() {
196        let tasks: Vec<String> = Vec::new();
197
198        let mut e_iter = tasks.into_iter();
199        assert_that!(TaskLineIter::new(&mut e_iter, "foo", "2021-06-18"))
200            .is_err_containing(Error::StartDateFormat);
201    }
202
203    #[test]
204    fn test_new_bad_end() {
205        let tasks: Vec<String> = Vec::new();
206
207        let mut e_iter = tasks.into_iter();
208        assert_that!(TaskLineIter::new(&mut e_iter, "2021-06-17", "bar"))
209            .is_err_containing(Error::EndDateFormat);
210    }
211
212    #[test]
213    fn test_new_wrong_order() {
214        let tasks: Vec<String> = Vec::new();
215
216        let mut e_iter = tasks.into_iter();
217        assert_that!(TaskLineIter::new(&mut e_iter, "2021-06-17", "2021-06-14"))
218            .is_err_containing(Error::DateError { source: DateError::WrongDateOrder });
219    }
220
221    fn make_entity_lines() -> Vec<String> {
222        [
223            "2021-05-30 08:00:00 junk",
224            "2021-05-30 08:10:00 junk",
225            "2021-05-30 08:20:00 junk",
226            "2021-05-30 08:30:00 junk",
227            "2021-05-30 08:40:00 junk",
228            "2021-05-30 08:50:00 junk",
229            "2021-05-30 09:00:00 junk",
230            "2021-06-01 08:00:00 tuesday 1",
231            "2021-06-01 08:30:00 tuesday 2",
232            "2021-06-02 08:00:00 wednesday 1",
233            "2021-06-02 08:30:00 wednesday 2",
234            "2021-06-03 08:00:00 thursday 1",
235            "2021-06-03 08:30:00 thursday 2",
236            "2021-06-04 08:00:00 friday 1",
237            "2021-06-04 08:30:00 friday 2",
238            "2021-06-07 08:00:00 monday 1",
239            "2021-06-07 08:30:00 monday 2",
240            "2021-06-08 08:00:00 final"
241        ]
242        .iter()
243        .map(|&s| String::from(s))
244        .collect()
245    }
246
247    #[test]
248    fn test_before_tasks() {
249        let tasks = make_entity_lines();
250
251        let mut e_iter = tasks.into_iter();
252        let mut iter = TaskLineIter::new(&mut e_iter, "2021-04-17", "2021-04-18")
253            .expect("Bad iterator construction");
254        assert_that!(iter.next()).is_none();
255    }
256
257    #[test]
258    fn test_after_tasks() {
259        let tasks = make_entity_lines();
260
261        let mut e_iter = tasks.into_iter();
262        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-17", "2021-06-18")
263            .expect("Bad iterator construction");
264        assert_that!(iter.next()).is_none();
265    }
266
267    #[test]
268    fn test_skip_beginning() {
269        let tasks = make_entity_lines();
270
271        let mut e_iter = tasks.into_iter();
272        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-01", "2021-06-02")
273            .expect("Bad iterator construction");
274        assert_that!(iter.next()).contains_value(String::from("2021-06-01 08:00:00 tuesday 1"));
275        assert_that!(iter.next()).contains_value(String::from("2021-06-01 08:30:00 tuesday 2"));
276        assert_that!(iter.next()).is_none();
277    }
278
279    #[test]
280    fn test_multiple_days() {
281        let tasks = make_entity_lines();
282
283        let mut e_iter = tasks.into_iter();
284        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-01", "2021-06-04")
285            .expect("Bad iterator construction");
286        assert_that!(iter.next()).contains_value(String::from("2021-06-01 08:00:00 tuesday 1"));
287        assert_that!(iter.next()).contains_value(String::from("2021-06-01 08:30:00 tuesday 2"));
288        assert_that!(iter.next()).contains_value(String::from("2021-06-02 08:00:00 wednesday 1"));
289        assert_that!(iter.next()).contains_value(String::from("2021-06-02 08:30:00 wednesday 2"));
290        assert_that!(iter.next()).contains_value(String::from("2021-06-03 08:00:00 thursday 1"));
291        assert_that!(iter.next()).contains_value(String::from("2021-06-03 08:30:00 thursday 2"));
292        assert_that!(iter.next()).is_none();
293    }
294
295    #[test]
296    fn test_start_in_gap() {
297        let tasks = make_entity_lines();
298
299        let mut e_iter = tasks.into_iter();
300        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-05", "2021-06-08")
301            .expect("Bad iterator construction");
302        assert_that!(iter.next()).contains_value(String::from("2021-06-07 08:00:00 monday 1"));
303        assert_that!(iter.next()).contains_value(String::from("2021-06-07 08:30:00 monday 2"));
304        assert_that!(iter.next()).is_none();
305    }
306
307    #[test]
308    fn test_end_in_gap() {
309        let tasks = make_entity_lines();
310
311        let mut e_iter = tasks.into_iter();
312        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-04", "2021-06-06")
313            .expect("Bad iterator construction");
314        assert_that!(iter.next()).contains_value(String::from("2021-06-04 08:00:00 friday 1"));
315        assert_that!(iter.next()).contains_value(String::from("2021-06-04 08:30:00 friday 2"));
316        assert_that!(iter.next()).is_none();
317    }
318
319    #[test]
320    fn test_missing_in_range() {
321        let tasks = make_entity_lines();
322
323        let mut e_iter = tasks.into_iter();
324        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-06", "2021-06-07")
325            .expect("Bad iterator construction");
326        assert_that!(iter.next()).is_none();
327    }
328
329    #[test]
330    fn test_to_end() {
331        let tasks = make_entity_lines();
332
333        let mut e_iter = tasks.into_iter();
334        let mut iter = TaskLineIter::new(&mut e_iter, "2021-06-07", "2021-06-10")
335            .expect("Bad iterator construction");
336        assert_that!(iter.next()).contains_value(String::from("2021-06-07 08:00:00 monday 1"));
337        assert_that!(iter.next()).contains_value(String::from("2021-06-07 08:30:00 monday 2"));
338        assert_that!(iter.next()).contains_value(String::from("2021-06-08 08:00:00 final"));
339        assert_that!(iter.next()).is_none();
340    }
341}