rtimelog 1.1.1

System for tracking time in a text-log-based format.
Documentation
//! Structs defining different sets of arguments supplied on command line
//!
//! # Description
//!
//! - [`DateRangeArgs`] - representation of the start/end dates for a report.
//! - [`FilterArgs`] - representation of the start/end dates and project list for reports.
use regex::Regex;

#[doc(inline)]
use crate::date::DateRange;
#[doc(inline)]
use crate::date::RangeParser;
#[doc(inline)]
use crate::error::Error;
use crate::Day;

/// Trait specifying common functionality for the different filter arguments.
pub trait DayFilter {
    /// Return the start date as a [`String`]
    fn start(&self) -> String;
    /// Return the end date as a [`String`]
    fn end(&self) -> String;
    /// Return a [`Day`] object after appropriate filtering
    fn filter_day(&self, day: Day) -> Option<Day>;
}

// Return Some [`Day`] if the supplied day is not empty.
fn day_with_entries(day: Day) -> Option<Day> { (!day.is_empty()).then_some(day) }

/// Representation of the start/end date and project list arguments for reports.
#[derive(Debug)]
pub struct FilterArgs {
    range:    DateRange,
    projects: Option<Regex>
}

// Return a [`Regex`] one of the supplied projects
fn regex_from_projs(projs: &[&str]) -> crate::Result<Regex> {
    Regex::new(&projs.join("|")).map_err(|_| Error::BadProjectFilter)
}

fn make_projects_regex_opt(projs: &[&str]) -> crate::Result<Option<Regex>> {
    if projs.is_empty() {
        Ok(None)
    }
    else {
        regex_from_projs(projs).map(Some)
    }
}

impl FilterArgs {
    /// Create the [`FilterArgs`] from an array of date range description strings, and an array of
    /// projects.
    ///
    /// # Errors
    ///
    /// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
    /// - Return [`Error::DateError`] if the start date is not before the end date
    pub fn new(dates: &[String], projs: &[String]) -> crate::Result<Self> {
        let project_list: Vec<&str> = projs.iter().map(String::as_str).collect();

        let mut date_iter = dates.iter().map(String::as_str);
        let parser = RangeParser::default();
        let (range, _token) = parser.parse(&mut date_iter)?;

        Ok(Self { range, projects: make_projects_regex_opt(&project_list)? })
    }

    // Return an [`Option<&Regex>`] that determines how to match projects
    fn projects(&self) -> Option<&Regex> { self.projects.as_ref() }
}

impl DayFilter for FilterArgs {
    /// Return the start date as a [`String`]
    fn start(&self) -> String { self.range.start().to_string() }

    /// Return the end date as a [`String`]
    fn end(&self) -> String { self.range.end().to_string() }

    /// Return a [`Day`] object filtered as needed
    fn filter_day(&self, day: Day) -> Option<Day> {
        day_with_entries(
            self.projects()
                .map(|re| day.filtered_by_project(re))
                .unwrap_or(day)
        )
    }
}

/// Representation of the start and end date arguments for reports.
#[derive(Debug, PartialEq, Eq)]
pub struct DateRangeArgs {
    range: DateRange
}

impl DateRangeArgs {
    /// Create the [`DateRangeArgs`] from an array of strings.
    ///
    /// # Errors
    ///
    /// - Return [`Error::DateError`] if the start date is not before the end date
    pub fn new(dates: &[String]) -> crate::Result<Self> {
        let mut date_iter = dates.iter().map(String::as_str);
        let parser = RangeParser::default();
        let (range, _token) = parser.parse(&mut date_iter)?;

        Ok(Self { range })
    }

    /// Return the start date as a [`String`]
    pub fn start(&self) -> String { self.range.start().to_string() }

    /// Return the end date as a [`String`]
    pub fn end(&self) -> String { self.range.end().to_string() }
}

impl DayFilter for DateRangeArgs {
    /// Return the start date as a [`String`]
    fn start(&self) -> String { self.start() }

    /// Return the end date as a [`String`]
    fn end(&self) -> String { self.end() }

    /// Return a [`Day`] object filtered as needed. If the day is empty,
    /// return None.
    fn filter_day(&self, day: Day) -> Option<Day> { day_with_entries(day) }
}

// Only used for testing, not particularly performant.
#[cfg(test)]
impl PartialEq for FilterArgs {
    fn eq(&self, other: &Self) -> bool {
        (self.start() == other.start())
            && (self.end() == other.end())
            && match (self.projects(), other.projects()) {
                (None, None) => true,
                (None, _) | (_, None) => false,
                (Some(lhs), Some(rhs)) => format!("{lhs:?}") == format!("{rhs:?}")
            }
    }
}

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

    use super::*;
    use crate::Date;

    // Test Filter

    #[test]
    fn test_filter_no_args() {
        let args = vec![];
        let expected = FilterArgs {
            range:    DateRange::new(Date::today(), Date::today().succ()),
            projects: None
        };

        let_assert!(Ok(actual) = FilterArgs::new(&args, &[]));
        assert!(actual == expected);
    }

    #[test]
    fn test_filter_just_one_date() {
        let args = vec!["yesterday".to_string()];
        let expected = FilterArgs {
            range:    DateRange::new(Date::today().pred(), Date::today()),
            projects: None
        };

        let_assert!(Ok(actual) = FilterArgs::new(&args, &[]));
        assert!(actual == expected);
    }

    #[test]
    fn test_filter_just_two_dates() {
        let args = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
        let_assert!(Ok(start) = Date::new(2021, 12, 1));
        let_assert!(Ok(end) = Date::new(2021, 12, 8));
        let expected = FilterArgs {
            range:    DateRange::new(start, end),
            projects: None
        };

        let_assert!(Ok(actual) = FilterArgs::new(&args, &[]));
        assert!(actual == expected);
    }

    #[test]
    fn test_filter_just_project() {
        let dates = vec![];
        let proj = vec!["project1".to_string()];
        let_assert!(Ok(regex) = Regex::new(r"project1"));
        let expected = FilterArgs {
            range:    DateRange::new(Date::today(), Date::today().succ()),
            projects: Some(regex)
        };

        let_assert!(Ok(actual) = FilterArgs::new(&dates, &proj));
        assert!(actual == expected);
    }

    #[test]
    fn test_filter_just_multiple_projects() {
        let dates = vec![];
        let projs = vec![
            "project1".to_string(),
            "cleanup".to_string(),
            "profit".to_string(),
        ];
        let_assert!(Ok(regex) = Regex::new(r"project1|cleanup|profit"));
        let expected = FilterArgs {
            range:    DateRange::new(Date::today(), Date::today().succ()),
            projects: Some(regex)
        };

        let_assert!(Ok(actual) = FilterArgs::new(&dates, &projs));
        assert!(actual == expected);
    }

    #[test]
    fn test_filter_start_and_project() {
        let dates = vec!["2021-12-01".to_string()];
        let projs = vec!["project1".to_string()];
        let_assert!(Ok(start) = Date::new(2021, 12, 1));
        let_assert!(Ok(end) = Date::new(2021, 12, 2));
        let_assert!(Ok(regex) = Regex::new(r"project1"));
        let expected = FilterArgs {
            range:    DateRange::new(start, end),
            projects: Some(regex)
        };

        let_assert!(Ok(actual) = FilterArgs::new(&dates, &projs));
        assert!(actual == expected);
    }

    #[test]
    fn test_filter_both_dates_and_project() {
        let dates = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
        let projs = vec!["project1".to_string()];
        let_assert!(Ok(start) = Date::new(2021, 12, 1));
        let_assert!(Ok(end) = Date::new(2021, 12, 8));
        let_assert!(Ok(regex) = Regex::new(r"project1"));
        let expected = FilterArgs {
            range:    DateRange::new(start, end),
            projects: Some(regex)
        };

        let_assert!(Ok(actual) = FilterArgs::new(&dates, &projs));
        assert!(actual == expected);
    }

    // Test DateRange

    #[test]
    fn test_dates_no_args() {
        let args = vec![];
        #[rustfmt::skip]
        let expected = DateRangeArgs {
            range: DateRange::new(Date::today(), Date::today().succ())
        };

        let_assert!(Ok(actual) = DateRangeArgs::new(&args));
        assert!(actual == expected);
    }

    #[test]
    fn test_dates_just_one_date() {
        let args = vec!["yesterday".to_string()];
        #[rustfmt::skip]
        let expected = DateRangeArgs {
            range: DateRange::new(Date::today().pred(), Date::today())
        };

        let_assert!(Ok(actual) = DateRangeArgs::new(&args));
        assert!(actual == expected);
    }

    #[test]
    fn test_dates_both_dates() {
        let args = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
        let_assert!(Ok(start) = Date::new(2021, 12, 1));
        let_assert!(Ok(end) = Date::new(2021, 12, 8));
        let expected = DateRangeArgs {
            range: DateRange::new(start, end)
        };

        let_assert!(Ok(actual) = DateRangeArgs::new(&args));
        assert!(actual == expected);
    }

    fn create_day_with_data() -> crate::Result<Day> {
        use crate::entry::Entry;

        let mut day = Day::new("2021-07-02")?;
        [
            "2021-07-01 10:00:00 +project @task",
            "2021-07-01 10:05:00 +project @task2",
            "2021-07-01 10:10:00 stop",
        ].iter().for_each(|ln| {
            day.add_entry(Entry::from_line(ln).expect("entry add")).expect("entry add");
        });
        Ok(day)
    }

    fn create_empty_day() -> crate::Result<Day> {
        Day::new("2021-07-02")
    }

    #[test]
    fn test_day_with_entries() {
        let_assert!(Ok(day) = create_day_with_data());
        assert!(day_with_entries(day).is_some());
    }

    #[test]
    fn test_day_filter_some() {
        let_assert!(Ok(day) = create_day_with_data());
        let_assert!(Ok(filt) = FilterArgs::new(&[String::from("2021-07-02")], &[String::from("project")]));
        let_assert!(Some(_) = filt.filter_day(day));
    }

    #[test]
    fn test_day_filter_dates() {
        let_assert!(Ok(filt) = FilterArgs::new(&[String::from("2021-07-02")], &[String::from("project")]));
        assert!(filt.start() == String::from("2021-07-02"));
        assert!(filt.end() == String::from("2021-07-03"));
    }

    #[test]
    fn test_date_range_filter_dates() {
        let_assert!(Ok(filt) = DateRangeArgs::new(&[String::from("2021-07-02")]));
        assert!(filt.start() == String::from("2021-07-02"));
        assert!(filt.end() == String::from("2021-07-03"));
    }

    #[test]
    fn test_day_with_entries_none() {
        let_assert!(Ok(day) = create_empty_day());
        assert!(day_with_entries(day).is_none());
    }

    #[test]
    fn test_day_filter_none() {
        let_assert!(Ok(day) = create_empty_day());
        let_assert!(Ok(filt) = FilterArgs::new(&[String::from("2021-07-10")], &[String::from("project")]));
        assert!(filt.filter_day(day).is_none());
    }
}