use chrono::{naive::NaiveDate, Duration}; use rust_decimal::Decimal; use std::str::FromStr; use crate::transaction::{Category, Transaction}; //TODO tests #[derive(Clone)] pub enum DateIsh { Absolute(NaiveDate), Relative(Duration), } impl DateIsh { pub fn parse(s: &str) -> Self { NaiveDate::parse_from_str(s, "%Y-%m-%d") .map(DateIsh::Absolute) .unwrap_or_else(|_| DateIsh::Relative(Self::parse_relative(s).unwrap())) } pub fn parse_relative(s: &str) -> Option { //TODO Month and year. Would depend on current date so maybe parse in one place. let num = s[..s.len()-1].parse().ok()?; Some(match s.chars().last()? { 'd' => Duration::days(num), 'w' => Duration::weeks(num), _ => unimplemented!(), }) } pub fn get(self) -> NaiveDate { match self { DateIsh::Absolute(date) => date, DateIsh::Relative(offset) => chrono::offset::Local::today().naive_utc() + offset } } } pub enum Comparison { Equal, Greater, GreaterOrEqual, Less, LessOrEqual, } fn parse_comparison(s: &str) -> Option<(Decimal, Comparison)> { if s.starts_with(">=") { Some((Decimal::from_str(&s[2..]).ok()?, Comparison::GreaterOrEqual)) } else if s.starts_with("<=") { Some((Decimal::from_str(&s[2..]).ok()?, Comparison::LessOrEqual)) } else if s.starts_with("=") { Some((Decimal::from_str(&s[1..]).ok()?, Comparison::Equal)) } else if s.starts_with(">") { Some((Decimal::from_str(&s[1..]).ok()?, Comparison::Greater)) } else if s.starts_with("<") { Some((Decimal::from_str(&s[1..]).ok()?, Comparison::Less)) } else { None } } pub enum Constraint { Category(Category), Before(DateIsh), After(DateIsh), On(DateIsh), AmountCompare(Decimal, Comparison), } impl Constraint { fn satisfies(&self, transaction: &Transaction) -> bool { match self { Constraint::Category(category) => &transaction.category == category, Constraint::Before(date) => transaction.date < date.clone().get(), Constraint::After(date) => transaction.date >= date.clone().get(), Constraint::On(date) => transaction.date == date.clone().get(), Constraint::AmountCompare(amount, comparison) => match comparison { Comparison::Equal => &transaction.amount == amount, Comparison::Greater => &transaction.amount > amount, Comparison::GreaterOrEqual => &transaction.amount >= amount, Comparison::Less => &transaction.amount < amount, Comparison::LessOrEqual => &transaction.amount <= amount, } } } } enum FilterType { Union(Constraint), Intersect(Constraint), Subtract(Constraint), } impl FilterType { fn apply<'s>(&self, mut search: Search<'s>) -> Search<'s> { match self { FilterType::Union(constraint) => { //TODO binary search and insert sorted for idx in search .transactions .iter() .enumerate() .filter(|(_, t)| constraint.satisfies(t)) .map(|(idx, _)| idx) { search.filtered.push(idx); } search.filtered.sort(); search.filtered.dedup(); } FilterType::Intersect(constraint) => { search.filtered = search .filtered .iter() .filter(|t| constraint.satisfies(search.transactions[**t])) .copied() .collect(); } FilterType::Subtract(constraint) => { search.filtered = search .filtered .iter() .filter(|t| !constraint.satisfies(search.transactions[**t])) .copied() .collect(); } } search } } pub struct Search<'t> { filtered: Vec, transactions: Vec<&'t Transaction>, } 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 //TODO category:"foo bar" 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)), ("on", date_ish) => Constraint::On(DateIsh::parse(date_ish)), ("amount", comparison) => { let (amount, comparison) = parse_comparison(comparison).unwrap(); Constraint::AmountCompare(amount, comparison) } _ => panic!(), }; self = filter_type(constraint).apply(self); } self } }