rtimelog 1.1.1

System for tracking time in a text-log-based format.
Documentation
//! Iterator filter for walking relevant task lines from an iterator
//!
//! Given an iterator, a start date, and an end date, gives a new iterator
//! that only returns the lines starting from the first on or after the start date
//! and ending before the end date.
//!
//! # Examples
//!
//! ```rust, no_run
//! use std::io::{BufRead, BufReader};
//! use timelog::task_line_iter::TaskLineIter;
//! # fn main() -> Result<(), std::io::Error> {
//! # let file = std::fs::File::open("timeline.txt")?;
//! let iter = TaskLineIter::new(
//!               BufReader::new(file).lines().take_while(|ol| ol.is_ok())
//!                                           .map(|ol| ol.unwrap()),
//!               "2021-06-06", "2021-06-08"
//!            );
//! #  Ok(())
//! # }
//! ```
//!
//! Any iterator returning strings will work. Getting lines from a file, takes a bit
//! more effort.

use std::fmt::{self, Debug};
use std::result;

use once_cell::sync::Lazy;
use regex::Regex;

#[doc(inline)]
use crate::date::DateError;
use crate::error::Error;
use crate::Entry;

// States for determining how we transition through the file.
#[derive(Debug)]
enum Stage {
    // Before the time range of interest
    Before,
    // In the time range of interest
    In,
    // After the time range of interest
    After
}

/// Iterator that walks lines of interest in the timelog file.
pub struct TaskLineIter<'a, I>
where
    I: Iterator<Item = String>
{
    /// The internal iterator for walking timelog lines
    iter:  I,
    /// What state are we in while walking lines
    stage: Stage,
    /// Criteria for the start of the lines of interest
    start: &'a str,
    /// Criteria for the end of the lines of interest
    end:   &'a str
}

impl<'a, I> Debug for TaskLineIter<'a, I>
where
    I: Iterator<Item = String>
{
    /// Debug formatting for the iterator
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> result::Result<(), fmt::Error> {
        f.debug_struct("TaskLineIter")
            .field("iter", &"Iter")
            .field("stage", &self.stage)
            .field("start", &self.start)
            .field("end", &self.end)
            .finish()
    }
}

// Should never fail, because hardcoded input.
// The expect() provides a little context if code is ever changed to break the regex.

/// Regular expression for matching the full date from a task line.
static DATE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^\d{4}-\d\d-\d\d$").expect("Date regex invalid"));

impl<'a, I> TaskLineIter<'a, I>
where
    I: Iterator<Item = String>
{
    /// Create a new iterator based on the supplied iterator limited to the
    /// date strings specified.
    ///
    /// # Errors
    ///
    /// - Return [`Error::StartDateFormat`] if the start date is not 'YYYY-MM-DD'.
    /// - Return [`Error::EndDateFormat`] if the end date is not 'YYYY-MM-DD'.
    /// - Return [`Error::DateError`] if the end date is before the start date.
    pub fn new(iter: I, start: &'a str, end: &'a str) -> crate::Result<Self> {
        if !DATE.is_match(start) {
            return Err(Error::StartDateFormat);
        }
        if !DATE.is_match(end) {
            return Err(Error::EndDateFormat);
        }
        if start >= end {
            return Err(DateError::WrongDateOrder.into());
        }

        Ok(Self { iter, stage: Stage::Before, start, end })
    }

    /// Find the last line before the point of lines of interest if any, consuming the iterator in
    /// the process.
    pub fn last_line_before(mut self) -> Option<String> {
        let mut prev: Option<String> = None;
        for line in self.iter.by_ref() {
            if let Some(date) = Entry::date_from_line(&line) {
                if self.start <= date && date < self.end {
                    return prev;
                }
                prev = Some(line);
            }
        }
        None
    }

    // Find the first line that meets the date requirements.
    fn find_first(&mut self) -> Option<String> {
        for line in self.iter.by_ref() {
            if let Some(date) = Entry::date_from_line(&line) {
                if self.start <= date && date < self.end {
                    return Some(line);
                }
            }
        }
        None
    }

    // Assuming we have met the start requirement get the next line that does not
    // exceed the end requirement.
    fn get_next(&mut self) -> Option<String> {
        if let Some(line) = self.iter.next() {
            if let Some(date) = Entry::date_from_line(&line) {
                if date < self.end {
                    return Some(line);
                }
            }
        }
        None
    }
}

impl<'a, I> Iterator for TaskLineIter<'a, I>
where
    I: Iterator<Item = String>
{
    type Item = String;

    /// Iterator method returning the next line meeting the start and end requirements
    fn next(&mut self) -> Option<Self::Item> {
        match self.stage {
            Stage::Before => {
                if let Some(line) = self.find_first() {
                    self.stage = Stage::In;
                    return Some(line);
                }
            }
            Stage::In => {
                if let Some(line) = self.get_next() {
                    return Some(line);
                }
            }
            Stage::After => {}
        }
        self.stage = Stage::After;
        None
    }
}

#[cfg(test)]
mod tests {
    use assert2::{assert, let_assert};

    use super::*;
    use crate::date::DateError;

    #[test]
    fn test_successful_new() {
        let tasks: Vec<String> = Vec::new();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(_) = TaskLineIter::new(&mut e_iter, "2021-06-17", "2021-06-18"));
    }

    #[test]
    fn test_new_bad_start() {
        let tasks: Vec<String> = Vec::new();

        let mut e_iter = tasks.into_iter();
        let_assert!(Err(err) = TaskLineIter::new(&mut e_iter, "foo", "2021-06-18"));
        assert!(err == Error::StartDateFormat);
    }

    #[test]
    fn test_new_bad_end() {
        let tasks: Vec<String> = Vec::new();

        let mut e_iter = tasks.into_iter();
        let_assert!(Err(err) = TaskLineIter::new(&mut e_iter, "2021-06-17", "bar"));
        assert!(err == Error::EndDateFormat);
    }

    #[test]
    fn test_new_wrong_order() {
        let tasks: Vec<String> = Vec::new();

        let mut e_iter = tasks.into_iter();
        let_assert!(Err(err) = TaskLineIter::new(&mut e_iter, "2021-06-17", "2021-06-14"));
        assert!(err == Error::DateError { source: DateError::WrongDateOrder });
    }

    fn make_entity_lines() -> Vec<String> {
        [
            "2021-05-30 08:00:00 junk",
            "2021-05-30 08:10:00 junk",
            "2021-05-30 08:20:00 junk",
            "2021-05-30 08:30:00 junk",
            "2021-05-30 08:40:00 junk",
            "2021-05-30 08:50:00 junk",
            "2021-05-30 09:00:00 junk",
            "2021-06-01 08:00:00 tuesday 1",
            "2021-06-01 08:30:00 tuesday 2",
            "2021-06-02 08:00:00 wednesday 1",
            "2021-06-02 08:30:00 wednesday 2",
            "2021-06-03 08:00:00 thursday 1",
            "2021-06-03 08:30:00 thursday 2",
            "2021-06-04 08:00:00 friday 1",
            "2021-06-04 08:30:00 friday 2",
            "2021-06-07 08:00:00 monday 1",
            "2021-06-07 08:30:00 monday 2",
            "2021-06-08 08:00:00 final"
        ]
        .iter()
        .map(|&s| String::from(s))
        .collect()
    }

    #[test]
    fn test_before_tasks() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-04-17", "2021-04-18"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_after_tasks() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-17", "2021-06-18"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_skip_beginning() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-01", "2021-06-02"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-01 08:00:00 tuesday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-01 08:30:00 tuesday 2"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_multiple_days() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-01", "2021-06-04"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-01 08:00:00 tuesday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-01 08:30:00 tuesday 2"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-02 08:00:00 wednesday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-02 08:30:00 wednesday 2"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-03 08:00:00 thursday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-03 08:30:00 thursday 2"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_start_in_gap() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-05", "2021-06-08"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-07 08:00:00 monday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-07 08:30:00 monday 2"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_end_in_gap() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-04", "2021-06-06"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-04 08:00:00 friday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-04 08:30:00 friday 2"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_missing_in_range() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-06", "2021-06-07"));
        assert!(iter.next() == None);
    }

    #[test]
    fn test_to_end() {
        let tasks = make_entity_lines();

        let mut e_iter = tasks.into_iter();
        let_assert!(Ok(mut iter) = TaskLineIter::new(&mut e_iter, "2021-06-07", "2021-06-10"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-07 08:00:00 monday 1"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-07 08:30:00 monday 2"));
        let_assert!(Some(line) = iter.next());
        assert!(line == String::from("2021-06-08 08:00:00 final"));
        assert!(iter.next() == None);
    }
}