use regex::Regex;
#[doc(inline)]
use crate::date::DateRange;
#[doc(inline)]
use crate::date::RangeParser;
#[doc(inline)]
use crate::error::Error;
use crate::Day;
pub trait DayFilter {
fn start(&self) -> String;
fn end(&self) -> String;
fn filter_day(&self, day: Day) -> Option<Day>;
}
fn day_with_entries(day: Day) -> Option<Day> { (!day.is_empty()).then_some(day) }
#[derive(Debug)]
pub struct FilterArgs {
range: DateRange,
projects: Option<Regex>
}
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 {
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)? })
}
fn projects(&self) -> Option<&Regex> { self.projects.as_ref() }
}
impl DayFilter for FilterArgs {
fn start(&self) -> String { self.range.start().to_string() }
fn end(&self) -> String { self.range.end().to_string() }
fn filter_day(&self, day: Day) -> Option<Day> {
day_with_entries(
self.projects()
.map(|re| day.filtered_by_project(re))
.unwrap_or(day)
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct DateRangeArgs {
range: DateRange
}
impl DateRangeArgs {
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 })
}
pub fn start(&self) -> String { self.range.start().to_string() }
pub fn end(&self) -> String { self.range.end().to_string() }
}
impl DayFilter for DateRangeArgs {
fn start(&self) -> String { self.start() }
fn end(&self) -> String { self.end() }
fn filter_day(&self, day: Day) -> Option<Day> { day_with_entries(day) }
}
#[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]
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]
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());
}
}