use chrono::{naive::NaiveDate, Duration}; use crate::transaction::{Category, Transaction}; pub struct Search<'t> { filtered: Vec, transactions: Vec<&'t Transaction>, } #[derive(Clone)] pub enum DateIsh { Absolute(NaiveDate), Relative(Duration), } impl DateIsh { pub fn parse(s: &str) -> Self { DateIsh::Absolute(NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()) } pub fn get(self) -> NaiveDate { match self { DateIsh::Absolute(date) => date, DateIsh::Relative(offset) => chrono::offset::Local::today().naive_utc() + offset } } } pub enum Constraint { Category(Category), Before(DateIsh), After(DateIsh), } enum FilterType { Union(Constraint), Intersect(Constraint), Subtract(Constraint), } impl FilterType { fn apply<'s>(&self, mut search: Search<'s>) -> Search<'s> { match self { FilterType::Union(_) => { //TODO binary search and insert sorted for idx in search .transactions .iter() .enumerate() .filter(|(_, t)| self.satisfies(t)) .map(|(idx, _)| idx) { search.filtered.push(idx); } search.filtered.sort(); search.filtered.dedup(); } FilterType::Intersect(_) => { search.filtered = search .filtered .iter() .filter(|t| self.satisfies(search.transactions[**t])) .copied() .collect(); } FilterType::Subtract(_) => { search.filtered = search .filtered .iter() .filter(|t| !self.satisfies(search.transactions[**t])) .copied() .collect(); } } search } fn satisfies(&self, transaction: &Transaction) -> bool { match self { // Category FilterType::Union(Constraint::Category(category)) | FilterType::Subtract(Constraint::Category(category)) | FilterType::Intersect(Constraint::Category(category)) => &transaction.category == category, FilterType::Intersect(Constraint::Before(date)) => transaction.date < date.clone().get(), FilterType::Intersect(Constraint::After(date)) => transaction.date >= date.clone().get(), FilterType::Subtract(Constraint::Before(date)) => transaction.date < date.clone().get(), FilterType::Subtract(Constraint::After(date)) => transaction.date >= date.clone().get(), FilterType::Union(Constraint::Before(date)) => transaction.date < date.clone().get(), FilterType::Union(Constraint::After(date)) => transaction.date >= date.clone().get(), } } } impl<'t> Search<'t> { pub fn new(transactions: Vec<&'t Transaction>) -> Self { Self { filtered: std::iter::successors(Some(0_usize), |n| Some(n.checked_add(1).unwrap())) .take(transactions.len()) .collect(), transactions, } } pub fn get(&self) -> Vec<&'t Transaction> { self .filtered .iter() .map(|idx| self.transactions[*idx]) .collect() } pub fn parse(mut self, rules: String) -> Self { for rule in rules.split(' ') { let (filter_type, rule): (fn(Constraint) -> FilterType, &str) = match rule.chars().nth(0).unwrap() { '-' => (FilterType::Subtract, &rule[1..]), '+' => (FilterType::Union, &rule[1..]), _ => (FilterType::Intersect, &rule[..]), }; //TODO lexing? can do a function for "spaces inside" instead // +category:a //TODO: category:"foo bar" // before:2021-01-01 => + (* -> 2020-12-31) n-incl // after:2021-01-01 => + (2021-01-01 -> *) incl // -before:2021-01-01 => - (2021-01-01 -> *) incl // -after:2021-01-01 => - (* -> 2021-12-31) n-incl //TODO: // today is 2021-01-01: // before:-1d => + (* -> 2020-12-31) n-incl // after:-1d => + (2021-01-01 -> *) incl // -before:-1d => - (2021-01-01 -> *) incl // -after:-1d => - (* -> 2020-12-31) n-incl let constraint = match rule.split_once(':').unwrap() { ("category", category) => Constraint::Category(category.to_string()), ("before", date_ish) => Constraint::Before(DateIsh::parse(date_ish)), ("after", date_ish) => Constraint::After(DateIsh::parse(date_ish)), _ => panic!(), }; self = filter_type(constraint).apply(self); } self } }