From 5c3d8e0a644e7a5cea8be63eb1ab22b73dd0e979 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 20 Feb 2021 19:59:53 +0100 Subject: [PATCH 01/21] add support for comparisons and regexes in the grammar of expressions --- ...value_expression.pest => expressions.pest} | 24 ++++++++++--------- src/lib/parser/value_expr.rs | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) rename src/{grammar/value_expression.pest => expressions.pest} (87%) diff --git a/src/grammar/value_expression.pest b/src/expressions.pest similarity index 87% rename from src/grammar/value_expression.pest rename to src/expressions.pest index e380f4f..525173e 100644 --- a/src/grammar/value_expression.pest +++ b/src/expressions.pest @@ -4,20 +4,15 @@ value_expr = {"(" ~ ws* ~ expr ~ ws* ~ ")"} term = _{ variable | money | number } money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) } currency = { LETTER+ | ("\"" ~ (!"\"" ~ ANY)+ ~ "\"")} +regex = { "/" ~ (!"\\/" ~ ANY )* ~ "/" } variable = _{ date | amount | total_amount | cost | value | gain | depth | posting_number | posting_count | cleared | real | not_automated | running_total } number = { "-"? ~ bigint ~ ("." ~ bigint)? } bigint = _{ ASCII_DIGIT+ } ws = _{" "} -/* Not yet implemented -ternary = { expr ~ ws* ~ "?" ~ ws* ~ expr ~ ws* ~ ":" ~ ws* ~ expr} -binary = { expr ~ ws* ~ - ("<=" | ">=" | "<" | ">" | "&" | "|" ) - ~ ws* ~ expr} -*/ - -expr = { or_expr } +expr = { comparison_expr } +comparison_expr = { or_expr ~ ws* ~ ( comparison ~ ws* ~ or_expr ) * } or_expr = { and_expr ~ ws* ~ ( or ~ ws* ~ and_expr ) * } and_expr = { additive_expr ~ ws* ~ ( or ~ ws* ~ additive_expr ) * } additive_expr = { multiplicative_expr ~ ws* ~ ( add ~ ws* ~ multiplicative_expr ) * } @@ -31,10 +26,17 @@ primary = { add = { "+" | "-" } mult = { "*" | "/" } -and = {"&"} -or = {"|"} +and = {"&" | "and"} +or = {"|" | "or" } unary = { "-" | "!"} unary_function = { "abs" } +comparison = { eq | ne | ge | gt | le | lt } +eq = { "=~" } +ne = { "!=" } +gt = { ">" } +ge = { ">=" } +le = { "<=" } +lt = { "<" } total_amount = {"T"} // A posting’s date, as the number of seconds past the epoch. This is always “today” for an account. @@ -62,4 +64,4 @@ not_automated = {"Z"} running_total = {"O"} // The total count of postings affecting an account and all its children. -posting_count = {"N"} \ No newline at end of file +posting_count = {"N"} diff --git a/src/lib/parser/value_expr.rs b/src/lib/parser/value_expr.rs index a6ac4d1..1dc39d8 100644 --- a/src/lib/parser/value_expr.rs +++ b/src/lib/parser/value_expr.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use std::str::FromStr; #[derive(Parser)] -#[grammar = "grammar/value_expression.pest"] +#[grammar = "expressions.pest"] pub struct ValueExpressionParser; pub fn eval_value_expression( @@ -201,7 +201,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { let rule = pair.as_rule(); match rule { Rule::expr => build_ast_from_expr(pair.into_inner().next().unwrap()), - Rule::or_expr | Rule::and_expr | Rule::additive_expr | Rule::multiplicative_expr => { + Rule::comparison_expr | Rule::or_expr | Rule::and_expr | Rule::additive_expr | Rule::multiplicative_expr => { let mut pair = pair.into_inner(); let lhspair = pair.next().unwrap(); let lhs = build_ast_from_expr(lhspair); From ab1ee9e90477746a77b711e146459c3818197d84 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 21 Feb 2021 02:19:16 +0100 Subject: [PATCH 02/21] Regex everywhere #26, #27 The command line query now gets expanded to regex queries, which are the same used in automated transactions. --- src/expressions.pest | 15 ++--- src/lib/commands/balance.rs | 4 +- src/lib/commands/register.rs | 4 +- src/lib/filter.rs | 107 +++++++++++++++++++++++++++++++--- src/lib/mod.rs | 2 +- src/lib/models/account.rs | 5 ++ src/lib/models/mod.rs | 7 ++- src/lib/models/transaction.rs | 84 +++++++++++++++++--------- src/lib/parser/value_expr.rs | 73 ++++++++++++++++++----- 9 files changed, 237 insertions(+), 64 deletions(-) diff --git a/src/expressions.pest b/src/expressions.pest index 525173e..6532e2b 100644 --- a/src/expressions.pest +++ b/src/expressions.pest @@ -1,11 +1,11 @@ // Grammar specification for value expressions - value_expr = {"(" ~ ws* ~ expr ~ ws* ~ ")"} term = _{ variable | money | number } money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) } currency = { LETTER+ | ("\"" ~ (!"\"" ~ ANY)+ ~ "\"")} -regex = { "/" ~ (!"\\/" ~ ANY )* ~ "/" } -variable = _{ date | amount | total_amount | cost | value | gain | depth | posting_number | posting_count | cleared | real | not_automated | running_total } +//regex = { "/" ~ (!"\\/" ~ ANY )+ ~ "/" } +regex = { "/" ~ (!"/" ~ ANY)* ~ "/"} +variable = _{ account | date | amount | total_amount | cost | value | gain | depth | posting_number | posting_count | cleared | real | not_automated | running_total } number = { "-"? ~ bigint ~ ("." ~ bigint)? } bigint = _{ ASCII_DIGIT+ } @@ -18,9 +18,9 @@ and_expr = { additive_expr ~ ws* ~ ( or ~ ws* ~ additive_expr ) * } additive_expr = { multiplicative_expr ~ ws* ~ ( add ~ ws* ~ multiplicative_expr ) * } multiplicative_expr = { primary ~ ws* ~ ( mult ~ ws* ~ primary )* } primary = { - ("(" ~ ws* ~ expr ~ ws* ~ ")") | - term | - (unary ~ ws* ~ primary) | + ("(" ~ ws* ~ expr ~ ws* ~ ")") | + term | regex | + (unary ~ ws* ~ primary) | (unary_function ~ ws* ~ "(" ~ expr ~ ws* ~ ")") } @@ -29,7 +29,7 @@ mult = { "*" | "/" } and = {"&" | "and"} or = {"|" | "or" } unary = { "-" | "!"} -unary_function = { "abs" } +unary_function = { "abs" | "has_tag" } comparison = { eq | ne | ge | gt | le | lt } eq = { "=~" } ne = { "!=" } @@ -38,6 +38,7 @@ ge = { ">=" } le = { "<=" } lt = { "<" } +account = {"account"} total_amount = {"T"} // A posting’s date, as the number of seconds past the epoch. This is always “today” for an account. date = {"d"} diff --git a/src/lib/commands/balance.rs b/src/lib/commands/balance.rs index 4c8ae77..8010e28 100644 --- a/src/lib/commands/balance.rs +++ b/src/lib/commands/balance.rs @@ -20,13 +20,13 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), let no_balance_check = options.no_balance_check; let mut tokenizer: Tokenizer = Tokenizer::from(&path); let items = tokenizer.tokenize()?; - let ledger = items.to_ledger(no_balance_check)?; + let mut ledger = items.to_ledger(no_balance_check)?; let mut balances: HashMap, Balance> = HashMap::new(); for t in ledger.transactions.iter() { for p in t.postings_iter() { - if !filter::filter(&options, t, p) { + if !filter::filter(&options, t, p, &mut ledger.commodities)? { continue; } let mut cur_bal = balances diff --git a/src/lib/commands/register.rs b/src/lib/commands/register.rs index ee66e9d..3aa08ac 100644 --- a/src/lib/commands/register.rs +++ b/src/lib/commands/register.rs @@ -13,7 +13,7 @@ pub fn execute(options: &CommonOpts) -> Result<(), Error> { // Now work let mut tokenizer: Tokenizer = Tokenizer::from(&path); let items = tokenizer.tokenize()?; - let ledger = items.to_ledger(no_balance_check)?; + let mut ledger = items.to_ledger(no_balance_check)?; let mut balance = Balance::new(); @@ -36,7 +36,7 @@ pub fn execute(options: &CommonOpts) -> Result<(), Error> { for t in ledger.transactions.iter() { let mut counter = 0; for p in t.postings_iter() { - if !filter::filter(&options, t, p) { + if !filter::filter(&options, t, p, &mut ledger.commodities)? { continue; } counter += 1; diff --git a/src/lib/filter.rs b/src/lib/filter.rs index 593b1f2..b769590 100644 --- a/src/lib/filter.rs +++ b/src/lib/filter.rs @@ -1,16 +1,23 @@ -use crate::models::{HasName, Posting, PostingType, Transaction}; -use crate::CommonOpts; +use crate::models::{Currency, HasName, Posting, PostingType, Transaction}; +use crate::parser::value_expr::{eval_expression, EvalResult}; +use crate::{CommonOpts, Error, List}; +use colored::Colorize; -pub fn filter(options: &CommonOpts, transaction: &Transaction, posting: &Posting) -> bool { +pub fn filter( + options: &CommonOpts, + transaction: &Transaction, + posting: &Posting, + commodities: &mut List, +) -> Result { // Get what's needed - let predicate = &options.query; + let predicate = preprocess_query(&options.query); let real = options.real; // Check for real postings if real { if let PostingType::Real = posting.kind { } else { - return false; + return Ok(false); } } @@ -18,17 +25,19 @@ pub fn filter(options: &CommonOpts, transaction: &Transaction, posting: // todo should do this at the posting level if let Some(date) = options.end { if transaction.date.unwrap() >= date { - return false; + return Ok(false); } } if let Some(date) = options.begin { if transaction.date.unwrap() < date { - return false; + return Ok(false); } } - return filter_predicate(predicate, posting); + + filter_predicate(predicate.as_str(), posting, transaction, commodities) } -pub fn filter_predicate(predicate: &Vec, posting: &Posting) -> bool { + +pub fn filter_predicate_old(predicate: &Vec, posting: &Posting) -> bool { let name = posting.account.get_name().to_lowercase(); if predicate.len() == 0 { return true; @@ -52,3 +61,83 @@ pub fn filter_predicate(predicate: &Vec, posting: &Posting) -> bool { } false } + +pub fn filter_predicate( + predicate: &str, + posting: &Posting, + transaction: &Transaction, + commodities: &mut List, +) -> Result { + if predicate.len() == 0 { + return Ok(true); + } + let result = eval_expression(predicate, posting, transaction, commodities); + match result { + EvalResult::Boolean(b) => Ok(b), + _ => Err(Error { + message: vec![predicate.red().bold(), "should return a boolean".normal()], + }), + } +} + +/// Create search expression from Strings +/// +/// The command line arguments provide syntactic sugar which save time when querying the journal. +/// This expands it to an actual query +/// +/// # Examples +/// ```rust +/// # use dinero::filter::preprocess_query; +/// let params:Vec = vec!["@payee", "savings" , "and", "checking", "and", "expr", "/aeiou/"].iter().map(|x| x.to_string()).collect(); +/// let processed = preprocess_query(¶ms); +/// assert_eq!(processed, "(@payee) or (account =~ /(?i)savings/) and (account =~ /(?i)checking/) or (/aeiou/)") +/// ``` +pub fn preprocess_query(query: &Vec) -> String { + let mut expression = String::new(); + let mut and = false; + let mut first = true; + let mut expr = false; + for raw_term in query.iter() { + let term = raw_term.trim(); + if term.len() == 0 { + continue; + } + if (term == "and") | (term == "expr") { + and = term == "and"; + expr = term == "expr"; + continue; + } + let join_term = if !first { + if and { + " and (" + } else { + " or (" + } + } else { + "(" + }; + expression.push_str(join_term); + if expr { + expression.push_str(term); + } else if let Some(c) = term.chars().next() { + match c { + '@' => expression.push_str(term), + '%' => { + expression.push_str("has_tag(/(?i)"); // case insensitive + expression.push_str(&term.to_string()[1..]); + expression.push_str("/)") + } + _ => { + expression.push_str("account =~ /(?i)"); // case insensitive + expression.push_str(term); + expression.push_str("/") + } + } + } + expression.push_str(")"); + and = false; + expr = false; + first = false; + } + expression +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs index ea5a94f..b6629b8 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -4,7 +4,7 @@ extern crate pest_derive; pub mod commands; mod error; -mod filter; +pub mod filter; mod list; mod main; pub mod models; diff --git a/src/lib/models/account.rs b/src/lib/models/account.rs index 8cb4687..1981aa4 100644 --- a/src/lib/models/account.rs +++ b/src/lib/models/account.rs @@ -1,4 +1,5 @@ use crate::models::{FromDirective, HasAliases, HasName, Origin}; +use regex::Regex; use std::collections::hash_map::RandomState; use std::collections::HashSet; use std::fmt; @@ -121,4 +122,8 @@ impl Account { } } } + + pub fn is_match(&self, regex: Regex) -> bool { + regex.is_match(self.get_name()) + } } diff --git a/src/lib/models/mod.rs b/src/lib/models/mod.rs index f010ee5..8e7a724 100644 --- a/src/lib/models/mod.rs +++ b/src/lib/models/mod.rs @@ -180,7 +180,12 @@ impl ParsedLedger { let mut extra_virtual_postings = vec![]; let mut extra_virtual_postings_balance = vec![]; for p in t.postings_iter() { - if filter_predicate(&vec![automated.description.clone()], p) { + if filter_predicate( + automated.clone().get_filter_query().as_str(), + p, + t, + &mut self.commodities, + )? { for comment in t.comments.iter() { p.to_owned().tags.append(&mut comment.get_tags()); } diff --git a/src/lib/models/transaction.rs b/src/lib/models/transaction.rs index 9dda1e1..5dde0e4 100644 --- a/src/lib/models/transaction.rs +++ b/src/lib/models/transaction.rs @@ -15,6 +15,8 @@ use std::fmt; use std::fmt::{Display, Formatter}; use super::Tag; +use crate::filter::preprocess_query; +use regex::Regex; #[derive(Debug, Clone)] pub struct Transaction { @@ -31,6 +33,21 @@ pub struct Transaction { pub comments: Vec, pub transaction_type: TransactionType, pub tags: Vec, + filter_query: Option, +} +impl Transaction { + pub fn get_filter_query(&mut self) -> String { + match self.filter_query.clone() { + None => { + let mut parts: Vec = + self.description.split(" ").map(|x| x.to_string()).collect(); + let res = preprocess_query(&parts); + self.filter_query = Some(res.clone()); + res + } + Some(x) => x, + } + } } #[derive(Debug, Copy, Clone)] @@ -85,6 +102,14 @@ impl Posting { pub fn set_amount(&mut self, money: Money) { self.amount = Some(money) } + pub fn has_tag(&self, regex: Regex) -> bool { + for t in self.tags.iter() { + if regex.is_match(t.get_name()) { + return true; + } + } + false + } } #[derive(Debug, Clone)] @@ -109,6 +134,7 @@ impl Transaction { comments: vec![], transaction_type: t_type, tags: vec![], + filter_query: None, } } /// Iterator over all the postings, including the virtual ones @@ -208,7 +234,7 @@ impl Transaction { // If it has money, update the balance if let Some(money) = &p.amount { let expected_balance = balances.get(p.account.deref()).unwrap().clone() // What we had - + Balance::from(money.clone()); // What we add + + Balance::from(money.clone()); // What we add if !skip_balance_check { if let Some(balance) = &p.balance { if Balance::from(balance.clone()) != expected_balance { @@ -229,36 +255,36 @@ impl Transaction { // Update the balance of the transaction transaction_balance = transaction_balance // What we had + match &p.cost { - None => Balance::from(money.clone()), + None => Balance::from(money.clone()), // If it has a cost, the secondary currency is added for the balance - Some(cost) => match cost { - Cost::Total { amount } => { - if p.amount.as_ref().unwrap().is_negative() { - Balance::from(-amount.clone()) - } else { - Balance::from(amount.clone()) - } - } - Cost::PerUnit { amount } => { - let currency = match amount { - Money::Zero => panic!("Cost has no currency"), - Money::Money { currency, .. } => currency, - }; - let units = match amount { - Money::Zero => BigRational::from(BigInt::from(0)), - Money::Money { amount, .. } => amount.clone(), - } * match p.amount.as_ref().unwrap() { - Money::Zero => BigRational::from(BigInt::from(0)), - Money::Money { amount, .. } => amount.clone(), - }; - let money = Money::Money { - amount: units, - currency: currency.clone(), - }; - Balance::from(money) + Some(cost) => match cost { + Cost::Total { amount } => { + if p.amount.as_ref().unwrap().is_negative() { + Balance::from(-amount.clone()) + } else { + Balance::from(amount.clone()) } - }, - }; + } + Cost::PerUnit { amount } => { + let currency = match amount { + Money::Zero => panic!("Cost has no currency"), + Money::Money { currency, .. } => currency, + }; + let units = match amount { + Money::Zero => BigRational::from(BigInt::from(0)), + Money::Money { amount, .. } => amount.clone(), + } * match p.amount.as_ref().unwrap() { + Money::Zero => BigRational::from(BigInt::from(0)), + Money::Money { amount, .. } => amount.clone(), + }; + let money = Money::Money { + amount: units, + currency: currency.clone(), + }; + Balance::from(money) + } + }, + }; // Add the posting postings.push(Posting { diff --git a/src/lib/parser/value_expr.rs b/src/lib/parser/value_expr.rs index 1dc39d8..9896f74 100644 --- a/src/lib/parser/value_expr.rs +++ b/src/lib/parser/value_expr.rs @@ -1,20 +1,20 @@ -use crate::models::{Currency, Money, Posting, Transaction}; +use crate::models::{Account, Currency, Money, Posting, Transaction}; use crate::pest::Parser; use crate::List; use num::{abs, BigInt, BigRational}; +use regex::Regex; use std::rc::Rc; use std::str::FromStr; #[derive(Parser)] #[grammar = "expressions.pest"] pub struct ValueExpressionParser; - -pub fn eval_value_expression( +pub fn eval_expression( expression: &str, posting: &Posting, transaction: &Transaction, commodities: &mut List, -) -> Money { +) -> EvalResult { let parsed = ValueExpressionParser::parse(Rule::value_expr, expression) .expect("unsuccessful parse") // unwrap the parse result .next() @@ -26,7 +26,15 @@ pub fn eval_value_expression( // Build the abstract syntax tree let root = build_ast_from_expr(parsed); - match eval(&root, posting, transaction, commodities) { + eval(&root, posting, transaction, commodities) +} +pub fn eval_value_expression( + expression: &str, + posting: &Posting, + transaction: &Transaction, + commodities: &mut List, +) -> Money { + match eval_expression(expression, posting, transaction, commodities) { EvalResult::Number(n) => posting.amount.clone().unwrap() * n, EvalResult::Money(m) => m, _ => panic!("Should be money"), @@ -34,8 +42,9 @@ pub fn eval_value_expression( } #[derive(Clone)] -enum Node { +pub enum Node { Amount, + Account, Number(BigRational), Money { currency: String, @@ -50,15 +59,18 @@ enum Node { lhs: Box, rhs: Box, }, + Regex(Regex), } - -enum EvalResult { +#[derive(Debug)] +pub enum EvalResult { Number(BigRational), Money(Money), Boolean(bool), + Account(Rc), + Regex(Regex), } -fn eval( +pub fn eval( node: &Node, posting: &Posting, transaction: &Transaction, @@ -66,6 +78,8 @@ fn eval( ) -> EvalResult { match node { Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()), + Node::Account => EvalResult::Account(posting.account.clone()), + Node::Regex(r) => EvalResult::Regex(r.clone()), Node::Number(n) => EvalResult::Number(n.clone()), Node::Money { currency, amount } => { let cur = match commodities.get(¤cy) { @@ -86,6 +100,7 @@ fn eval( EvalResult::Number(n) => EvalResult::Number(-n), EvalResult::Money(money) => EvalResult::Money(-money), EvalResult::Boolean(b) => EvalResult::Boolean(!b), + x => panic!("Can't do neg of {:?}", x), }, Unary::Abs => match res { EvalResult::Number(n) => EvalResult::Number(abs(n)), @@ -94,6 +109,11 @@ fn eval( Money::Money { amount, currency } => Money::from((currency, abs(amount))), }), EvalResult::Boolean(_b) => panic!("Can't do abs of boolean"), + x => panic!("Can't do abs of {:?}", x), + }, + Unary::HasTag => match res { + EvalResult::Regex(r) => EvalResult::Boolean(posting.has_tag(r)), + x => panic!("Expected regex. Found {:?}", x), }, } } @@ -101,6 +121,17 @@ fn eval( let left = eval(lhs, posting, transaction, commodities); let right = eval(rhs, posting, transaction, commodities); match op { + Binary::Eq => { + if let EvalResult::Account(lhs) = left { + if let EvalResult::Regex(rhs) = right { + EvalResult::Boolean(lhs.is_match(rhs)) + } else { + panic!("Expected regex") + } + } else { + panic!("Expected account") + } + } Binary::Add | Binary::Subtract => { if let EvalResult::Number(lhs) = left { if let EvalResult::Number(rhs) = right { @@ -178,30 +209,36 @@ fn eval( } #[derive(Clone)] -enum Unary { +pub enum Unary { Not, Neg, Abs, + HasTag, } #[derive(Clone)] -enum Binary { +pub enum Binary { Add, Subtract, Mult, Div, Or, And, + Eq, } #[derive(Clone)] -enum Ternary {} +pub enum Ternary {} fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { let rule = pair.as_rule(); match rule { Rule::expr => build_ast_from_expr(pair.into_inner().next().unwrap()), - Rule::comparison_expr | Rule::or_expr | Rule::and_expr | Rule::additive_expr | Rule::multiplicative_expr => { + Rule::comparison_expr + | Rule::or_expr + | Rule::and_expr + | Rule::additive_expr + | Rule::multiplicative_expr => { let mut pair = pair.into_inner(); let lhspair = pair.next().unwrap(); let lhs = build_ast_from_expr(lhspair); @@ -216,6 +253,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { "-" => Binary::Subtract, "*" => Binary::Mult, "/" => Binary::Div, + "=~" => Binary::Eq, x => unreachable!("{}", x), }, }; @@ -233,6 +271,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { let op = match first.as_str() { "abs" => Unary::Abs, "-" => Unary::Neg, + "has_tag" => Unary::HasTag, unknown => panic!("Unknown expr: {:?}", unknown), }; parse_unary_expr(op, build_ast_from_expr(inner.next().unwrap())) @@ -254,6 +293,14 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { } } Rule::number => Node::Number(parse_big_rational(first.as_str())), + Rule::regex => { + let full = first.as_str().to_string(); + let n = full.len() - 1; + let slice = &full[1..n]; + let regex = Regex::new(slice).unwrap(); + Node::Regex(regex) + } + Rule::account => Node::Account, unknown => panic!("Unknown rule: {:?}", unknown), } From b7ec05cfee1300922b36c66fa26ef3035565266c Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 21 Feb 2021 02:32:53 +0100 Subject: [PATCH 03/21] remove unused function --- src/lib/filter.rs | 27 +-------------------------- src/lib/models/transaction.rs | 2 +- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/lib/filter.rs b/src/lib/filter.rs index b769590..870211f 100644 --- a/src/lib/filter.rs +++ b/src/lib/filter.rs @@ -1,4 +1,4 @@ -use crate::models::{Currency, HasName, Posting, PostingType, Transaction}; +use crate::models::{Currency, Posting, PostingType, Transaction}; use crate::parser::value_expr::{eval_expression, EvalResult}; use crate::{CommonOpts, Error, List}; use colored::Colorize; @@ -37,31 +37,6 @@ pub fn filter( filter_predicate(predicate.as_str(), posting, transaction, commodities) } -pub fn filter_predicate_old(predicate: &Vec, posting: &Posting) -> bool { - let name = posting.account.get_name().to_lowercase(); - if predicate.len() == 0 { - return true; - } - for pred in predicate { - let p = pred.trim(); - if p.starts_with("%") { - // look in the posting tags - for tag in posting.tags.iter() { - match tag.name.to_lowercase().find(&p.to_lowercase()[1..]) { - None => continue, - Some(_) => return true, - } - } - } else { - match name.find(&p.to_lowercase()) { - None => continue, - Some(_) => return true, - } - } - } - false -} - pub fn filter_predicate( predicate: &str, posting: &Posting, diff --git a/src/lib/models/transaction.rs b/src/lib/models/transaction.rs index 5dde0e4..ec34c46 100644 --- a/src/lib/models/transaction.rs +++ b/src/lib/models/transaction.rs @@ -39,7 +39,7 @@ impl Transaction { pub fn get_filter_query(&mut self) -> String { match self.filter_query.clone() { None => { - let mut parts: Vec = + let parts: Vec = self.description.split(" ").map(|x| x.to_string()).collect(); let res = preprocess_query(&parts); self.filter_query = Some(res.clone()); From 0664b905b283e69c4238edbd5334c9d0d1378177 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 21 Feb 2021 02:39:02 +0100 Subject: [PATCH 04/21] refactor, move files around --- Cargo.toml | 4 ++-- src/{lib/main.rs => app.rs} | 0 src/{lib => }/commands/accounts.rs | 0 src/{lib => }/commands/balance.rs | 0 src/{lib => }/commands/check.rs | 0 src/{lib => }/commands/commodities.rs | 0 src/{lib => }/commands/mod.rs | 0 src/{lib => }/commands/prices.rs | 0 src/{lib => }/commands/register.rs | 0 src/{lib => }/error.rs | 0 src/{lib => }/filter.rs | 0 src/{ => grammar}/expressions.pest | 0 src/{lib => }/list.rs | 0 src/{lib => }/mod.rs | 4 ++-- src/{lib => }/models/account.rs | 0 src/{lib => }/models/balance.rs | 0 src/{lib => }/models/comment.rs | 0 src/{lib => }/models/currency.rs | 0 src/{lib => }/models/mod.rs | 0 src/{lib => }/models/models.rs | 0 src/{lib => }/models/money.rs | 0 src/{lib => }/models/payee.rs | 0 src/{lib => }/models/price.rs | 0 src/{lib => }/models/transaction.rs | 0 src/{lib => }/parser/chars.rs | 0 src/{lib => }/parser/include.rs | 0 src/{lib => }/parser/mod.rs | 0 src/{lib => }/parser/tokenizers/account.rs | 0 src/{lib => }/parser/tokenizers/comment.rs | 0 src/{lib => }/parser/tokenizers/commodity.rs | 0 src/{lib => }/parser/tokenizers/mod.rs | 0 src/{lib => }/parser/tokenizers/payee.rs | 0 src/{lib => }/parser/tokenizers/price.rs | 0 src/{lib => }/parser/tokenizers/tag.rs | 0 src/{lib => }/parser/tokenizers/transaction.rs | 0 src/{lib => }/parser/value_expr.rs | 2 +- 36 files changed, 5 insertions(+), 5 deletions(-) rename src/{lib/main.rs => app.rs} (100%) rename src/{lib => }/commands/accounts.rs (100%) rename src/{lib => }/commands/balance.rs (100%) rename src/{lib => }/commands/check.rs (100%) rename src/{lib => }/commands/commodities.rs (100%) rename src/{lib => }/commands/mod.rs (100%) rename src/{lib => }/commands/prices.rs (100%) rename src/{lib => }/commands/register.rs (100%) rename src/{lib => }/error.rs (100%) rename src/{lib => }/filter.rs (100%) rename src/{ => grammar}/expressions.pest (100%) rename src/{lib => }/list.rs (100%) rename src/{lib => }/mod.rs (82%) rename src/{lib => }/models/account.rs (100%) rename src/{lib => }/models/balance.rs (100%) rename src/{lib => }/models/comment.rs (100%) rename src/{lib => }/models/currency.rs (100%) rename src/{lib => }/models/mod.rs (100%) rename src/{lib => }/models/models.rs (100%) rename src/{lib => }/models/money.rs (100%) rename src/{lib => }/models/payee.rs (100%) rename src/{lib => }/models/price.rs (100%) rename src/{lib => }/models/transaction.rs (100%) rename src/{lib => }/parser/chars.rs (100%) rename src/{lib => }/parser/include.rs (100%) rename src/{lib => }/parser/mod.rs (100%) rename src/{lib => }/parser/tokenizers/account.rs (100%) rename src/{lib => }/parser/tokenizers/comment.rs (100%) rename src/{lib => }/parser/tokenizers/commodity.rs (100%) rename src/{lib => }/parser/tokenizers/mod.rs (100%) rename src/{lib => }/parser/tokenizers/payee.rs (100%) rename src/{lib => }/parser/tokenizers/price.rs (100%) rename src/{lib => }/parser/tokenizers/tag.rs (100%) rename src/{lib => }/parser/tokenizers/transaction.rs (100%) rename src/{lib => }/parser/value_expr.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index 991c76e..3e9797b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,10 @@ repository = "https://github.com/frosklis/dinero-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "dinero" -path = "src/lib/mod.rs" +path = "src/mod.rs" [[bin]] name = "dinero" -test = false +test = true bench = false path = "src/main.rs" diff --git a/src/lib/main.rs b/src/app.rs similarity index 100% rename from src/lib/main.rs rename to src/app.rs diff --git a/src/lib/commands/accounts.rs b/src/commands/accounts.rs similarity index 100% rename from src/lib/commands/accounts.rs rename to src/commands/accounts.rs diff --git a/src/lib/commands/balance.rs b/src/commands/balance.rs similarity index 100% rename from src/lib/commands/balance.rs rename to src/commands/balance.rs diff --git a/src/lib/commands/check.rs b/src/commands/check.rs similarity index 100% rename from src/lib/commands/check.rs rename to src/commands/check.rs diff --git a/src/lib/commands/commodities.rs b/src/commands/commodities.rs similarity index 100% rename from src/lib/commands/commodities.rs rename to src/commands/commodities.rs diff --git a/src/lib/commands/mod.rs b/src/commands/mod.rs similarity index 100% rename from src/lib/commands/mod.rs rename to src/commands/mod.rs diff --git a/src/lib/commands/prices.rs b/src/commands/prices.rs similarity index 100% rename from src/lib/commands/prices.rs rename to src/commands/prices.rs diff --git a/src/lib/commands/register.rs b/src/commands/register.rs similarity index 100% rename from src/lib/commands/register.rs rename to src/commands/register.rs diff --git a/src/lib/error.rs b/src/error.rs similarity index 100% rename from src/lib/error.rs rename to src/error.rs diff --git a/src/lib/filter.rs b/src/filter.rs similarity index 100% rename from src/lib/filter.rs rename to src/filter.rs diff --git a/src/expressions.pest b/src/grammar/expressions.pest similarity index 100% rename from src/expressions.pest rename to src/grammar/expressions.pest diff --git a/src/lib/list.rs b/src/list.rs similarity index 100% rename from src/lib/list.rs rename to src/list.rs diff --git a/src/lib/mod.rs b/src/mod.rs similarity index 82% rename from src/lib/mod.rs rename to src/mod.rs index b6629b8..5233a0c 100644 --- a/src/lib/mod.rs +++ b/src/mod.rs @@ -2,14 +2,14 @@ extern crate pest; #[macro_use] extern crate pest_derive; +mod app; pub mod commands; mod error; pub mod filter; mod list; -mod main; pub mod models; pub mod parser; +pub use app::{run_app, CommonOpts}; pub(crate) use error::{Error, LedgerError, ParserError}; pub use list::List; -pub use main::{run_app, CommonOpts}; diff --git a/src/lib/models/account.rs b/src/models/account.rs similarity index 100% rename from src/lib/models/account.rs rename to src/models/account.rs diff --git a/src/lib/models/balance.rs b/src/models/balance.rs similarity index 100% rename from src/lib/models/balance.rs rename to src/models/balance.rs diff --git a/src/lib/models/comment.rs b/src/models/comment.rs similarity index 100% rename from src/lib/models/comment.rs rename to src/models/comment.rs diff --git a/src/lib/models/currency.rs b/src/models/currency.rs similarity index 100% rename from src/lib/models/currency.rs rename to src/models/currency.rs diff --git a/src/lib/models/mod.rs b/src/models/mod.rs similarity index 100% rename from src/lib/models/mod.rs rename to src/models/mod.rs diff --git a/src/lib/models/models.rs b/src/models/models.rs similarity index 100% rename from src/lib/models/models.rs rename to src/models/models.rs diff --git a/src/lib/models/money.rs b/src/models/money.rs similarity index 100% rename from src/lib/models/money.rs rename to src/models/money.rs diff --git a/src/lib/models/payee.rs b/src/models/payee.rs similarity index 100% rename from src/lib/models/payee.rs rename to src/models/payee.rs diff --git a/src/lib/models/price.rs b/src/models/price.rs similarity index 100% rename from src/lib/models/price.rs rename to src/models/price.rs diff --git a/src/lib/models/transaction.rs b/src/models/transaction.rs similarity index 100% rename from src/lib/models/transaction.rs rename to src/models/transaction.rs diff --git a/src/lib/parser/chars.rs b/src/parser/chars.rs similarity index 100% rename from src/lib/parser/chars.rs rename to src/parser/chars.rs diff --git a/src/lib/parser/include.rs b/src/parser/include.rs similarity index 100% rename from src/lib/parser/include.rs rename to src/parser/include.rs diff --git a/src/lib/parser/mod.rs b/src/parser/mod.rs similarity index 100% rename from src/lib/parser/mod.rs rename to src/parser/mod.rs diff --git a/src/lib/parser/tokenizers/account.rs b/src/parser/tokenizers/account.rs similarity index 100% rename from src/lib/parser/tokenizers/account.rs rename to src/parser/tokenizers/account.rs diff --git a/src/lib/parser/tokenizers/comment.rs b/src/parser/tokenizers/comment.rs similarity index 100% rename from src/lib/parser/tokenizers/comment.rs rename to src/parser/tokenizers/comment.rs diff --git a/src/lib/parser/tokenizers/commodity.rs b/src/parser/tokenizers/commodity.rs similarity index 100% rename from src/lib/parser/tokenizers/commodity.rs rename to src/parser/tokenizers/commodity.rs diff --git a/src/lib/parser/tokenizers/mod.rs b/src/parser/tokenizers/mod.rs similarity index 100% rename from src/lib/parser/tokenizers/mod.rs rename to src/parser/tokenizers/mod.rs diff --git a/src/lib/parser/tokenizers/payee.rs b/src/parser/tokenizers/payee.rs similarity index 100% rename from src/lib/parser/tokenizers/payee.rs rename to src/parser/tokenizers/payee.rs diff --git a/src/lib/parser/tokenizers/price.rs b/src/parser/tokenizers/price.rs similarity index 100% rename from src/lib/parser/tokenizers/price.rs rename to src/parser/tokenizers/price.rs diff --git a/src/lib/parser/tokenizers/tag.rs b/src/parser/tokenizers/tag.rs similarity index 100% rename from src/lib/parser/tokenizers/tag.rs rename to src/parser/tokenizers/tag.rs diff --git a/src/lib/parser/tokenizers/transaction.rs b/src/parser/tokenizers/transaction.rs similarity index 100% rename from src/lib/parser/tokenizers/transaction.rs rename to src/parser/tokenizers/transaction.rs diff --git a/src/lib/parser/value_expr.rs b/src/parser/value_expr.rs similarity index 99% rename from src/lib/parser/value_expr.rs rename to src/parser/value_expr.rs index 9896f74..f665bbe 100644 --- a/src/lib/parser/value_expr.rs +++ b/src/parser/value_expr.rs @@ -7,7 +7,7 @@ use std::rc::Rc; use std::str::FromStr; #[derive(Parser)] -#[grammar = "expressions.pest"] +#[grammar = "grammar/expressions.pest"] pub struct ValueExpressionParser; pub fn eval_expression( expression: &str, From 20eaeff15ffc4e36dd3db8426f9b91a24b81bb57 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 21 Feb 2021 13:25:19 +0100 Subject: [PATCH 05/21] add payees command --- src/app.rs | 12 ++++++++++-- src/commands/payees.rs | 21 +++++++++++++++++++++ src/models/payee.rs | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/commands/payees.rs diff --git a/src/app.rs b/src/app.rs index 0311be4..af358b3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,7 @@ use two_timer; use lazy_static::lazy_static; use regex::Regex; -use crate::commands::{accounts, balance, check, commodities, prices, register}; +use crate::commands::{accounts, balance, check, commodities, payees, prices, register}; use crate::Error; use chrono::NaiveDate; use colored::Colorize; @@ -35,7 +35,8 @@ enum Command { /// List the accounts Accounts(CommonOpts), // Codes, - // Payees, + /// List the payees + Payees(CommonOpts), /// Show the exchange rates Prices(CommonOpts), /// List commodities @@ -203,6 +204,13 @@ pub fn run_app(mut args: Vec) -> Result<(), ()> { commodities::execute(options.input_file, options.no_balance_check) } + Command::Payees(options) => { + if options.force_color { + env::set_var("CLICOLOR_FORCE", "1"); + } + + payees::execute(options.input_file, options.no_balance_check) + } Command::Prices(options) => prices::execute(options.input_file, options.no_balance_check), Command::Accounts(options) => { if options.force_color { diff --git a/src/commands/payees.rs b/src/commands/payees.rs new file mode 100644 index 0000000..e23cd98 --- /dev/null +++ b/src/commands/payees.rs @@ -0,0 +1,21 @@ +use crate::models::{Currency, HasName, Payee}; +use crate::parser::Tokenizer; +use crate::Error; +use std::ops::Deref; +use std::path::PathBuf; + +pub fn execute(path: PathBuf, no_balance_check: bool) -> Result<(), Error> { + let mut tokenizer: Tokenizer = Tokenizer::from(&path); + let items = tokenizer.tokenize()?; + let ledger = items.to_ledger(no_balance_check)?; + let mut payees = ledger + .payees + .iter() + .map(|x| x.1.deref().to_owned()) + .collect::>(); + payees.sort_by(|a, b| a.get_name().cmp(b.get_name())); + for payee in payees { + println!("{}", payee); + } + Ok(()) +} diff --git a/src/models/payee.rs b/src/models/payee.rs index 12754b2..77d3abc 100644 --- a/src/models/payee.rs +++ b/src/models/payee.rs @@ -1,6 +1,9 @@ use crate::models::{FromDirective, HasAliases, HasName, Origin}; +use regex::Regex; use std::collections::hash_map::RandomState; use std::collections::HashSet; +use std::fmt; +use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; #[derive(Debug, Clone)] @@ -19,6 +22,12 @@ impl PartialEq for Payee { } } +impl Display for Payee { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + impl HasName for Payee { fn get_name(&self) -> &str { self.name.as_str() @@ -45,3 +54,8 @@ impl Hash for Payee { self.name.hash(state); } } +impl Payee { + pub fn is_match(&self, regex: Regex) -> bool { + regex.is_match(self.get_name()) + } +} From 84deb785783331cb2f8a6e207db0a8710893c0f9 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 21 Feb 2021 22:16:37 +0100 Subject: [PATCH 06/21] Most search and automated transactions work but #31, #26, #27 ... The ```payees``` command is slow. Need to look into that. --- src/commands/mod.rs | 1 + src/commands/payees.rs | 2 +- src/filter.rs | 8 +++-- src/grammar/expressions.pest | 23 +++++++------- src/models/mod.rs | 32 ++++++++++++++----- src/models/payee.rs | 11 +++++++ src/models/transaction.rs | 29 ++++++++++++++++-- src/parser/mod.rs | 2 +- src/parser/tokenizers/payee.rs | 4 +-- src/parser/tokenizers/transaction.rs | 46 ++++++++++++++++++++-------- src/parser/value_expr.rs | 21 ++++++++----- 11 files changed, 132 insertions(+), 47 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 68c50f2..2310ee2 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,5 +2,6 @@ pub mod accounts; pub mod balance; pub mod check; pub mod commodities; +pub mod payees; pub mod prices; pub mod register; diff --git a/src/commands/payees.rs b/src/commands/payees.rs index e23cd98..033675e 100644 --- a/src/commands/payees.rs +++ b/src/commands/payees.rs @@ -1,4 +1,4 @@ -use crate::models::{Currency, HasName, Payee}; +use crate::models::{HasName, Payee}; use crate::parser::Tokenizer; use crate::Error; use std::ops::Deref; diff --git a/src/filter.rs b/src/filter.rs index 870211f..0506f09 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -65,7 +65,7 @@ pub fn filter_predicate( /// # use dinero::filter::preprocess_query; /// let params:Vec = vec!["@payee", "savings" , "and", "checking", "and", "expr", "/aeiou/"].iter().map(|x| x.to_string()).collect(); /// let processed = preprocess_query(¶ms); -/// assert_eq!(processed, "(@payee) or (account =~ /(?i)savings/) and (account =~ /(?i)checking/) or (/aeiou/)") +/// assert_eq!(processed, "(payee =~ /(?i)payee/) or (account =~ /(?i)savings/) and (account =~ /(?i)checking/) or (/aeiou/)") /// ``` pub fn preprocess_query(query: &Vec) -> String { let mut expression = String::new(); @@ -96,7 +96,11 @@ pub fn preprocess_query(query: &Vec) -> String { expression.push_str(term); } else if let Some(c) = term.chars().next() { match c { - '@' => expression.push_str(term), + '@' => { + expression.push_str("payee =~ /(?i)"); // case insensitive + expression.push_str(&term.to_string()[1..]); + expression.push_str("/") + } '%' => { expression.push_str("has_tag(/(?i)"); // case insensitive expression.push_str(&term.to_string()[1..]); diff --git a/src/grammar/expressions.pest b/src/grammar/expressions.pest index 6532e2b..275aad7 100644 --- a/src/grammar/expressions.pest +++ b/src/grammar/expressions.pest @@ -1,16 +1,5 @@ // Grammar specification for value expressions value_expr = {"(" ~ ws* ~ expr ~ ws* ~ ")"} -term = _{ variable | money | number } -money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) } -currency = { LETTER+ | ("\"" ~ (!"\"" ~ ANY)+ ~ "\"")} -//regex = { "/" ~ (!"\\/" ~ ANY )+ ~ "/" } -regex = { "/" ~ (!"/" ~ ANY)* ~ "/"} -variable = _{ account | date | amount | total_amount | cost | value | gain | depth | posting_number | posting_count | cleared | real | not_automated | running_total } - -number = { "-"? ~ bigint ~ ("." ~ bigint)? } -bigint = _{ ASCII_DIGIT+ } -ws = _{" "} - expr = { comparison_expr } comparison_expr = { or_expr ~ ws* ~ ( comparison ~ ws* ~ or_expr ) * } or_expr = { and_expr ~ ws* ~ ( or ~ ws* ~ and_expr ) * } @@ -24,6 +13,17 @@ primary = { (unary_function ~ ws* ~ "(" ~ expr ~ ws* ~ ")") } +term = _{ variable | money | number } +money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) } +currency = { LETTER+ | ("\"" ~ (!"\"" ~ ANY)+ ~ "\"")} +regex = { "/" ~ (!"/" ~ ANY)* ~ "/"} +variable = _{ account | payee | date | amount | total_amount | cost | value | gain | depth | posting_number | posting_count | cleared | real | not_automated | running_total } + +number = { "-"? ~ bigint ~ ("." ~ bigint)? } +bigint = _{ ASCII_DIGIT+ } +ws = _{" "} + + add = { "+" | "-" } mult = { "*" | "/" } and = {"&" | "and"} @@ -39,6 +39,7 @@ le = { "<=" } lt = { "<" } account = {"account"} +payee = {"payee"} total_amount = {"T"} // A posting’s date, as the number of seconds past the epoch. This is always “today” for an account. date = {"d"} diff --git a/src/models/mod.rs b/src/models/mod.rs index 8e7a724..1d942fb 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -37,7 +37,7 @@ pub struct Ledger { pub(crate) commodities: List, pub(crate) transactions: Vec>, pub(crate) prices: Vec, - payees: List, + pub(crate) payees: List, } impl Ledger { @@ -65,7 +65,7 @@ impl ParsedLedger { for transaction in self.transactions.iter() { for p in transaction.postings.iter() { account_strs.insert(p.account.clone()); - + payee_strs.insert(p.payee.clone()); // Currencies if let Some(c) = &p.money_currency { commodity_strs.insert(c.clone()); @@ -100,7 +100,14 @@ impl ParsedLedger { Err(_) => self.accounts.insert(Account::from(alias.as_str())), } } - // TODO payees + + // Payees + for alias in payee_strs { + match self.payees.get(&alias) { + Ok(_) => {} // do nothing + Err(_) => self.payees.insert(Payee::from(alias.as_str())), + } + } // 3. Prices from price statements let mut prices: Vec = Vec::new(); @@ -129,7 +136,7 @@ impl ParsedLedger { let mut transactions = Vec::new(); let mut automated_transactions = Vec::new(); for parsed in self.transactions.iter() { - let (mut t, mut auto, mut new_prices) = self._transaction_to_ledger(parsed)?; + let (mut t, mut auto, mut new_prices) = self.clone()._transaction_to_ledger(parsed)?; transactions.append(&mut t); automated_transactions.append(&mut auto); prices.append(&mut new_prices); @@ -191,13 +198,19 @@ impl ParsedLedger { } for auto_posting in automated.postings_iter() { let account_alias = auto_posting.account.clone(); + let payee_alias = auto_posting.payee.clone(); match self.accounts.get(&account_alias) { Ok(_) => {} // do nothing Err(_) => { self.accounts.insert(Account::from(account_alias.as_str())) } } + match self.payees.get(&payee_alias) { + Ok(_) => {} // do nothing + Err(_) => self.payees.insert(Payee::from(payee_alias.as_str())), + } let account = self.accounts.get(&account_alias).unwrap(); + let payee = self.payees.get(&payee_alias).unwrap(); let money = match &auto_posting.money_currency { None => Some(value_expr::eval_value_expression( auto_posting.amount_expr.clone().unwrap().as_str(), @@ -226,6 +239,7 @@ impl ParsedLedger { } } }; + let posting = Posting { account: account.clone(), amount: money, @@ -233,6 +247,7 @@ impl ParsedLedger { cost: None, kind: auto_posting.kind, tags: vec![], + payee: payee.clone(), }; // println!("{:?}", posting); match auto_posting.kind { @@ -297,11 +312,11 @@ impl ParsedLedger { fn _transaction_to_ledger( &self, - parsed: &Transaction, + parsed: &Transaction, ) -> Result< ( Vec>, - Vec>, + Vec>, Vec, ), Error, @@ -323,12 +338,13 @@ impl ParsedLedger { // Go posting by posting for p in parsed.postings.iter() { let account = self.accounts.get(&p.account)?; - - let mut posting: Posting = Posting::new(account, p.kind); + let payee = self.payees.get(&p.payee)?; + let mut posting: Posting = Posting::new(account, p.kind, payee); posting.tags = transaction.tags.clone(); for comment in p.comments.iter() { posting.tags.append(&mut comment.get_tags()); } + // Modify posting with amounts if let Some(c) = &p.money_currency { posting.amount = Some(Money::from(( diff --git a/src/models/payee.rs b/src/models/payee.rs index 77d3abc..248a216 100644 --- a/src/models/payee.rs +++ b/src/models/payee.rs @@ -59,3 +59,14 @@ impl Payee { regex.is_match(self.get_name()) } } + +impl From<&str> for Payee { + fn from(name: &str) -> Self { + Payee { + name: String::from(name), + note: None, + alias: Default::default(), + origin: Origin::FromTransaction, + } + } +} diff --git a/src/models/transaction.rs b/src/models/transaction.rs index ec34c46..862e42c 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -8,8 +8,8 @@ use chrono::NaiveDate; use num::rational::BigRational; use crate::models::balance::Balance; -use crate::models::{Account, Comment, HasName, Money}; -use crate::LedgerError; +use crate::models::{Account, Comment, HasName, Money, Origin, Payee}; +use crate::{LedgerError, List}; use num::BigInt; use std::fmt; use std::fmt::{Display, Formatter}; @@ -35,6 +35,7 @@ pub struct Transaction { pub tags: Vec, filter_query: Option, } + impl Transaction { pub fn get_filter_query(&mut self) -> String { match self.filter_query.clone() { @@ -48,6 +49,21 @@ impl Transaction { Some(x) => x, } } + pub fn get_payee(&self, payees: &mut List) -> Rc { + match payees.get(&self.description) { + Ok(x) => x.clone(), + Err(_) => { + let payee = Payee { + name: self.description.clone(), + note: None, + alias: Default::default(), + origin: Origin::FromTransaction, + }; + payees.insert(payee); + self.get_payee(payees) + } + } + } } #[derive(Debug, Copy, Clone)] @@ -86,10 +102,11 @@ pub struct Posting { pub cost: Option, pub kind: PostingType, pub tags: Vec, + pub payee: Rc, } impl Posting { - pub fn new(account: &Account, kind: PostingType) -> Posting { + pub fn new(account: &Account, kind: PostingType, payee: &Payee) -> Posting { Posting { account: Rc::new(account.clone()), amount: None, @@ -97,6 +114,7 @@ impl Posting { cost: None, kind: kind, tags: vec![], + payee: Rc::new(payee.clone()), } } pub fn set_amount(&mut self, money: Money) { @@ -229,6 +247,7 @@ impl Transaction { // 1. Iterate over postings let mut fill_account = &Rc::new(Account::from("this will never be used")); + let mut fill_payee = &Rc::new(Payee::from("this will never be used")); let mut postings: Vec = Vec::new(); for p in self.postings.iter() { // If it has money, update the balance @@ -294,6 +313,7 @@ impl Transaction { cost: p.cost.clone(), kind: PostingType::Real, tags: self.tags.clone(), + payee: p.payee.clone(), }); } else if &p.balance.is_some() & !skip_balance_check { // There is a balance @@ -313,10 +333,12 @@ impl Transaction { cost: p.cost.clone(), kind: PostingType::Real, tags: p.tags.clone(), + payee: p.payee.clone(), }); } else { // We do nothing, but this is the account for the empty post fill_account = &p.account; + fill_payee = &p.payee; } } @@ -347,6 +369,7 @@ impl Transaction { cost: None, kind: PostingType::Real, tags: self.tags.clone(), + payee: fill_payee.clone(), }); } self.postings = postings; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cce4c0f..c72d623 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -29,7 +29,7 @@ pub struct ParsedLedger { pub accounts: List, pub payees: List, pub commodities: List, - pub transactions: Vec>, + pub transactions: Vec>, pub prices: Vec, pub comments: Vec, pub tags: Vec, diff --git a/src/parser/tokenizers/payee.rs b/src/parser/tokenizers/payee.rs index 34e2cfc..1835f9f 100644 --- a/src/parser/tokenizers/payee.rs +++ b/src/parser/tokenizers/payee.rs @@ -12,7 +12,7 @@ use crate::ParserError; pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { lazy_static! { static ref RE: Regex = Regex::new(format!("{}{}{}", - r"(payee) +" , // directive commodity + r"(payee) +" , // payee directive r"(.*)" , // description r"( ;.*)?" , // note ).as_str()).unwrap(); @@ -31,7 +31,7 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { Some(m) => { match i { 1 => - // commodity + // payee { detected = true; } diff --git a/src/parser/tokenizers/transaction.rs b/src/parser/tokenizers/transaction.rs index 04a3e9e..6ed1e31 100644 --- a/src/parser/tokenizers/transaction.rs +++ b/src/parser/tokenizers/transaction.rs @@ -1,4 +1,6 @@ -use crate::models::{Cleared, Comment, PostingType, PriceType, Transaction, TransactionType}; +use crate::models::{ + Cleared, Comment, HasName, PostingType, PriceType, Transaction, TransactionType, +}; use crate::parser::chars::LineType; use crate::parser::tokenizers::comment; use crate::parser::{chars, Tokenizer}; @@ -10,18 +12,18 @@ use num::BigInt; use regex::Regex; use std::str::FromStr; -pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result, Error> { +pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result, Error> { parse_generic(tokenizer, true) } pub(crate) fn parse_automated_transaction( tokenizer: &mut Tokenizer, -) -> Result, Error> { +) -> Result, Error> { parse_generic(tokenizer, false) } /// Parses a transaction -fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result, Error> { +fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result, Error> { lazy_static! { static ref RE_REAL: Regex = Regex::new(format!("{}{}{}{}{}{}", r"(\d{4}[/-]\d{2}[/-]\d{2})" , // date @@ -42,7 +44,7 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result RE_AUTOMATED.captures(mystr.as_str()).unwrap(), }; - let mut transaction = Transaction::::new(match real { + let mut transaction = Transaction::::new(match real { true => TransactionType::Real, false => TransactionType::Automated, }); @@ -56,7 +58,9 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result transaction.date = Some(parse_date(m.as_str())), - false => transaction.description = m.as_str().to_string(), + false => { + transaction.description = m.as_str().to_string(); + } } } 2 => @@ -81,7 +85,7 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result // description { - transaction.description = m.as_str().to_string() + transaction.description = m.as_str().to_string(); } 6 => // note @@ -105,9 +109,20 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result { let len = transaction.postings.len(); + + for tag in comment.get_tags().iter() { + if tag.get_name().to_lowercase() == "payee" { + if let Some(payee) = &tag.value { + transaction.postings[len - 1].payee = payee.clone(); + } + break; + } + } transaction.postings[len - 1].comments.push(comment); } - false => transaction.comments.push(comment), + false => { + transaction.comments.push(comment); + } } } c if c.is_numeric() => { @@ -116,7 +131,11 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result { - match parse_posting(tokenizer, transaction.transaction_type) { + match parse_posting( + tokenizer, + transaction.transaction_type, + transaction.description.clone(), + ) { // Although here we already know the kind of the posting (virtual, real), // we deal with that in the next phase of parsing Ok(posting) => transaction.postings.push(posting), @@ -134,7 +153,7 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result, pub money_currency: Option, @@ -146,6 +165,7 @@ pub struct Posting { pub comments: Vec, pub amount_expr: Option, pub kind: PostingType, + pub payee: String, } /// Parses a posting @@ -153,7 +173,8 @@ pub struct Posting { fn parse_posting( tokenizer: &mut Tokenizer, transaction_type: TransactionType, -) -> Result { + default_payee: String, +) -> Result { let mut account = String::new(); let mut posting_type = PostingType::Real; let mut finished = false; @@ -210,7 +231,7 @@ fn parse_posting( } } - let mut posting = Posting { + let mut posting = RawPosting { account, money_amount: None, money_currency: None, @@ -222,6 +243,7 @@ fn parse_posting( comments: Vec::new(), amount_expr: None, kind: posting_type, + payee: default_payee.clone(), }; if finished { return Ok(posting); diff --git a/src/parser/value_expr.rs b/src/parser/value_expr.rs index f665bbe..2da9f55 100644 --- a/src/parser/value_expr.rs +++ b/src/parser/value_expr.rs @@ -1,4 +1,4 @@ -use crate::models::{Account, Currency, Money, Posting, Transaction}; +use crate::models::{Account, Currency, Money, Payee, Posting, Transaction}; use crate::pest::Parser; use crate::List; use num::{abs, BigInt, BigRational}; @@ -9,6 +9,7 @@ use std::str::FromStr; #[derive(Parser)] #[grammar = "grammar/expressions.pest"] pub struct ValueExpressionParser; + pub fn eval_expression( expression: &str, posting: &Posting, @@ -28,6 +29,7 @@ pub fn eval_expression( eval(&root, posting, transaction, commodities) } + pub fn eval_value_expression( expression: &str, posting: &Posting, @@ -45,6 +47,7 @@ pub fn eval_value_expression( pub enum Node { Amount, Account, + Payee, Number(BigRational), Money { currency: String, @@ -61,12 +64,14 @@ pub enum Node { }, Regex(Regex), } + #[derive(Debug)] pub enum EvalResult { Number(BigRational), Money(Money), Boolean(bool), Account(Rc), + Payee(Rc), Regex(Regex), } @@ -79,6 +84,7 @@ pub fn eval( match node { Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()), Node::Account => EvalResult::Account(posting.account.clone()), + Node::Payee => EvalResult::Payee(posting.payee.clone()), Node::Regex(r) => EvalResult::Regex(r.clone()), Node::Number(n) => EvalResult::Number(n.clone()), Node::Money { currency, amount } => { @@ -122,14 +128,14 @@ pub fn eval( let right = eval(rhs, posting, transaction, commodities); match op { Binary::Eq => { - if let EvalResult::Account(lhs) = left { - if let EvalResult::Regex(rhs) = right { - EvalResult::Boolean(lhs.is_match(rhs)) - } else { - panic!("Expected regex") + if let EvalResult::Regex(rhs) = right { + match left { + EvalResult::Account(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + EvalResult::Payee(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + x => panic!("Found {:?}", x), } } else { - panic!("Expected account") + panic!("Expected regex"); } } Binary::Add | Binary::Subtract => { @@ -301,6 +307,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { Node::Regex(regex) } Rule::account => Node::Account, + Rule::payee => Node::Payee, unknown => panic!("Unknown rule: {:?}", unknown), } From 8efc786cf58152794a38884c87ddb1440858ec3e Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 02:43:11 +0100 Subject: [PATCH 07/21] do not test by default as the CI pipeline does that --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c73da3d..4d0ee50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "dinero" path = "src/mod.rs" [[bin]] name = "dinero" -test = true +test = false bench = false path = "src/main.rs" From 31106ab99fd7d2f3db7969d37f1e1fc82faae742 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 02:55:35 +0100 Subject: [PATCH 08/21] works but it is too slow --- src/commands/balance.rs | 4 ++-- src/models/transaction.rs | 2 ++ src/parser/tokenizers/transaction.rs | 10 ++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/commands/balance.rs b/src/commands/balance.rs index 1e1aefa..5eeb17d 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -140,7 +140,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), let mut text = account.split(":").last().unwrap().to_string(); // This is where it gets tricky, we need to collapse while we can let mut collapse = true; - 'outer: loop { + loop { if (index + 1) >= num_bal { break; } @@ -153,7 +153,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), break; } let this_depth = name.split(":").count(); - if (this_depth == n + 1) { + if this_depth == n + 1 { collapse = false; break; } diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 862e42c..6d0c293 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -27,6 +27,7 @@ pub struct Transaction { pub code: Option, pub description: String, pub note: Option, + pub payee: Option, pub postings: Vec, pub virtual_postings: Vec, pub virtual_postings_balance: Vec, @@ -146,6 +147,7 @@ impl Transaction { code: None, description: "".to_string(), note: None, + payee: None, postings: vec![], virtual_postings: vec![], virtual_postings_balance: vec![], diff --git a/src/parser/tokenizers/transaction.rs b/src/parser/tokenizers/transaction.rs index 6ed1e31..ef57001 100644 --- a/src/parser/tokenizers/transaction.rs +++ b/src/parser/tokenizers/transaction.rs @@ -25,13 +25,14 @@ pub(crate) fn parse_automated_transaction( /// Parses a transaction fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result, Error> { lazy_static! { - static ref RE_REAL: Regex = Regex::new(format!("{}{}{}{}{}{}", + static ref RE_REAL: Regex = Regex::new(format!("{}{}{}{}{}{}{}", r"(\d{4}[/-]\d{2}[/-]\d{2})" , // date r"(= ?\d{4}[/-]\d{2}[/-]\d{2})? +" , // effective_date r"([\*!])? +" , // cleared r"(\(.*\) )?" , // code r"(.*)" , // description - r"( ;.*)?" , // note + r"( |.*)" , // payee + r"( ;.*)?" , // note ).as_str()).unwrap(); static ref RE_AUTOMATED: Regex = Regex::new(format!("{}",r"=(.*)" ).as_str()).unwrap(); } @@ -89,6 +90,11 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result // note + { + transaction.payee = Some(m.as_str().to_string()) + } + 7 => + // note { transaction.code = Some(m.as_str().to_string()) } From 3df38006f723d4870a9dd217fcc440e17b0cb460 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 19:05:14 +0100 Subject: [PATCH 09/21] looks like automated transactions are working, #27, #26 --- src/app.rs | 2 +- src/filter.rs | 17 +++-- src/grammar/expressions.pest | 83 ++++++++++++------------ src/models/mod.rs | 10 +-- src/models/transaction.rs | 38 ++++++++++- src/parser/value_expr.rs | 118 ++++++++++++++++++++++++++++++----- 6 files changed, 198 insertions(+), 70 deletions(-) diff --git a/src/app.rs b/src/app.rs index af358b3..3bff06c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -231,7 +231,7 @@ pub fn run_app(mut args: Vec) -> Result<(), ()> { } /// A parser for date expressions -fn date_parser(date: &str) -> Result { +pub fn date_parser(date: &str) -> Result { lazy_static! { static ref RE_MONTH: Regex = Regex::new(r"(\d{4})[/-](\d\d?)$").unwrap(); static ref RE_DATE: Regex = Regex::new(r"(\d{4})[/-](\d\d?)[/-](\d\d?)$").unwrap(); diff --git a/src/filter.rs b/src/filter.rs index 0506f09..5b8c783 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -77,9 +77,14 @@ pub fn preprocess_query(query: &Vec) -> String { if term.len() == 0 { continue; } - if (term == "and") | (term == "expr") { - and = term == "and"; - expr = term == "expr"; + if term == "and" { + and = true; + continue; + } else if term == "or" { + and = false; + continue; + } else if term == "expr" { + expr = true; continue; } let join_term = if !first { @@ -106,6 +111,10 @@ pub fn preprocess_query(query: &Vec) -> String { expression.push_str(&term.to_string()[1..]); expression.push_str("/)") } + '/' => { + expression.push_str("account =~ "); // case insensitive + expression.push_str(term); + } _ => { expression.push_str("account =~ /(?i)"); // case insensitive expression.push_str(term); @@ -118,5 +127,5 @@ pub fn preprocess_query(query: &Vec) -> String { expr = false; first = false; } - expression + format!("({})", expression) } diff --git a/src/grammar/expressions.pest b/src/grammar/expressions.pest index 275aad7..0c5ee12 100644 --- a/src/grammar/expressions.pest +++ b/src/grammar/expressions.pest @@ -1,24 +1,53 @@ // Grammar specification for value expressions + +// A value expression is an expression between parenthesis value_expr = {"(" ~ ws* ~ expr ~ ws* ~ ")"} -expr = { comparison_expr } -comparison_expr = { or_expr ~ ws* ~ ( comparison ~ ws* ~ or_expr ) * } + +// Then the expression builds up in terms of increasing preference +expr = { or_expr } or_expr = { and_expr ~ ws* ~ ( or ~ ws* ~ and_expr ) * } -and_expr = { additive_expr ~ ws* ~ ( or ~ ws* ~ additive_expr ) * } +and_expr = { comparison_expr ~ ws* ~ ( and ~ ws* ~ comparison_expr ) * } +comparison_expr = { additive_expr ~ ws* ~ ( comparison ~ ws* ~ additive_expr ) * } additive_expr = { multiplicative_expr ~ ws* ~ ( add ~ ws* ~ multiplicative_expr ) * } multiplicative_expr = { primary ~ ws* ~ ( mult ~ ws* ~ primary )* } primary = { ("(" ~ ws* ~ expr ~ ws* ~ ")") | - term | regex | - (unary ~ ws* ~ primary) | - (unary_function ~ ws* ~ "(" ~ expr ~ ws* ~ ")") + (unary ~ ws* ~ expr) | + term | + (function ~ ws* ~ "(" ~ ws* ~ expr ~ ws* ~ ("," ~ ws* ~ expr ~ ws*)* ~ ")") } -term = _{ variable | money | number } + +term = _{ variable | money | number | regex | string } money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) } currency = { LETTER+ | ("\"" ~ (!"\"" ~ ANY)+ ~ "\"")} regex = { "/" ~ (!"/" ~ ANY)* ~ "/"} -variable = _{ account | payee | date | amount | total_amount | cost | value | gain | depth | posting_number | posting_count | cleared | real | not_automated | running_total } +string = { "'" ~ (!"'" ~ ANY)* ~ "'"} +variable = { + "account" | + "payee" | + "date" | + "note" | + "amount" | + "total_amount" | + "cost" | + "value" | + "gain" | + "depth" | + "posting_number" | + "posting_count" | + "cleared" | + "real" | + "not_automated" | + "running_total" | + "note" | + // Abbreviations go later + "T" | "N" | "O" | "Z" | "R" | "X" | + "n" | "l" | "g" | "v" | "b" +} + +// helpers number = { "-"? ~ bigint ~ ("." ~ bigint)? } bigint = _{ ASCII_DIGIT+ } ws = _{" "} @@ -28,42 +57,12 @@ add = { "+" | "-" } mult = { "*" | "/" } and = {"&" | "and"} or = {"|" | "or" } -unary = { "-" | "!"} -unary_function = { "abs" | "has_tag" } +unary = { "-" | "!" | "not" } +function = { "abs" | "has_tag" | "to_date" | "any" | "tag" } comparison = { eq | ne | ge | gt | le | lt } -eq = { "=~" } +eq = { "=~" | "=="} ne = { "!=" } gt = { ">" } ge = { ">=" } le = { "<=" } -lt = { "<" } - -account = {"account"} -payee = {"payee"} -total_amount = {"T"} -// A posting’s date, as the number of seconds past the epoch. This is always “today” for an account. -date = {"d"} -// The posting’s amount; the balance of an account, without considering children. -amount = {"amount" | "t"} -// The cost of a posting; the cost of an account, without its children. -cost = {"b"} -// The market value of a posting or an account, without its children. -value = {"v"} -// The net gain (market value minus cost basis), for a posting or an account, without its children. It is the same as ‘v-b’. -gain = {"g"} -// The depth (“level”) of an account. If an account has one parent, its depth is one. -depth = {"l"} -// The index of a posting, or the count of postings affecting an account. -posting_number = {"n"} -// ‘1’ if a posting’s transaction has been cleared, ‘0’ otherwise. -cleared = {"X"} -// ‘1’ if a posting is not virtual, ‘0’ otherwise. -real = {"R"} -// ‘1’ if a posting is not automated, ‘0’ otherwise. -not_automated = {"Z"} - -// The total of all postings seen so far, or the total of an account and all its children. -running_total = {"O"} - -// The total count of postings affecting an account and all its children. -posting_count = {"N"} +lt = { "<" } \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 1d942fb..c143274 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -59,9 +59,8 @@ impl ParsedLedger { let mut account_strs = HashSet::::new(); let mut payee_strs = HashSet::::new(); - // // 1. Populate the directive lists - // + for transaction in self.transactions.iter() { for p in transaction.postings.iter() { account_strs.insert(p.account.clone()); @@ -135,8 +134,9 @@ impl ParsedLedger { // let mut transactions = Vec::new(); let mut automated_transactions = Vec::new(); + for parsed in self.transactions.iter() { - let (mut t, mut auto, mut new_prices) = self.clone()._transaction_to_ledger(parsed)?; + let (mut t, mut auto, mut new_prices) = self._transaction_to_ledger(parsed)?; transactions.append(&mut t); automated_transactions.append(&mut auto); prices.append(&mut new_prices); @@ -181,14 +181,14 @@ impl ParsedLedger { // 5. Go over the transactions again and see if there is something we need to do with them if automated_transactions.len() > 0 { - for automated in automated_transactions.iter() { + for automated in automated_transactions.iter_mut() { for t in transactions.iter_mut() { let mut extra_postings = vec![]; let mut extra_virtual_postings = vec![]; let mut extra_virtual_postings_balance = vec![]; for p in t.postings_iter() { if filter_predicate( - automated.clone().get_filter_query().as_str(), + automated.get_filter_query().as_str(), p, t, &mut self.commodities, diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 6d0c293..5569962 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -41,8 +41,26 @@ impl Transaction { pub fn get_filter_query(&mut self) -> String { match self.filter_query.clone() { None => { - let parts: Vec = - self.description.split(" ").map(|x| x.to_string()).collect(); + let mut parts: Vec = vec![]; + let mut current = String::new(); + let mut in_regex = false; + let mut in_string = false; + for c in self.description.chars() { + if (c == ' ') & !in_string & !in_regex { + parts.push(current.clone()); + current = String::new(); + } + if c == '"' { + in_string = !in_string; + } else if c == '/' { + in_regex = !in_regex; + current.push(c); + } else { + current.push(c) + } + } + parts.push(current.clone()); + //self.description.split(" ").map(|x| x.to_string()).collect(); let res = preprocess_query(&parts); self.filter_query = Some(res.clone()); res @@ -129,6 +147,22 @@ impl Posting { } false } + pub fn get_tag(&self, regex: Regex) -> Option { + for t in self.tags.iter() { + if regex.is_match(t.get_name()) { + return t.value.clone(); + } + } + None + } + pub fn get_exact_tag(&self, regex: String) -> Option { + for t in self.tags.iter() { + if regex.as_str() == t.get_name() { + return t.value.clone(); + } + } + None + } } #[derive(Debug, Clone)] diff --git a/src/parser/value_expr.rs b/src/parser/value_expr.rs index 2da9f55..133551e 100644 --- a/src/parser/value_expr.rs +++ b/src/parser/value_expr.rs @@ -1,6 +1,8 @@ +use crate::app; use crate::models::{Account, Currency, Money, Payee, Posting, Transaction}; use crate::pest::Parser; use crate::List; +use chrono::NaiveDate; use num::{abs, BigInt, BigRational}; use regex::Regex; use std::rc::Rc; @@ -48,6 +50,8 @@ pub enum Node { Amount, Account, Payee, + Note, + Date, Number(BigRational), Money { currency: String, @@ -63,6 +67,7 @@ pub enum Node { rhs: Box, }, Regex(Regex), + String(String), } #[derive(Debug)] @@ -73,6 +78,8 @@ pub enum EvalResult { Account(Rc), Payee(Rc), Regex(Regex), + String(Option), + Date(NaiveDate), } pub fn eval( @@ -85,7 +92,10 @@ pub fn eval( Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()), Node::Account => EvalResult::Account(posting.account.clone()), Node::Payee => EvalResult::Payee(posting.payee.clone()), + Node::Note => EvalResult::String(transaction.note.clone()), + Node::Date => EvalResult::Date(transaction.date.clone().unwrap()), Node::Regex(r) => EvalResult::Regex(r.clone()), + Node::String(r) => EvalResult::String(Some(r.clone())), Node::Number(n) => EvalResult::Number(n.clone()), Node::Money { currency, amount } => { let cur = match commodities.get(¤cy) { @@ -101,7 +111,24 @@ pub fn eval( Node::UnaryExpr { op, child } => { let res = eval(child, posting, transaction, commodities); match op { - Unary::Not => EvalResult::Boolean(false), + Unary::Not => match res { + EvalResult::Boolean(b) => EvalResult::Boolean(!b), + x => panic!("Can't do neg of {:?}", x), + }, + Unary::Any => { + let mut res = false; + for p in transaction.postings_iter() { + if let EvalResult::Boolean(b) = eval(child, p, transaction, commodities) { + if b { + res = true; + break; + } + } else { + panic!("Should evaluate to boolean") + } + } + EvalResult::Boolean(res) + } Unary::Neg => match res { EvalResult::Number(n) => EvalResult::Number(-n), EvalResult::Money(money) => EvalResult::Money(-money), @@ -121,21 +148,55 @@ pub fn eval( EvalResult::Regex(r) => EvalResult::Boolean(posting.has_tag(r)), x => panic!("Expected regex. Found {:?}", x), }, + Unary::Tag => match res { + EvalResult::Regex(r) => EvalResult::String(posting.get_tag(r)), + EvalResult::String(r) => EvalResult::String(posting.get_exact_tag(r.unwrap())), + x => panic!("Expected regex. Found {:?}", x), + }, + Unary::ToDate => match res { + EvalResult::String(r) => { + EvalResult::Date(app::date_parser(r.unwrap().as_str()).unwrap()) + } + x => panic!("Expected String. Found {:?}", x), + }, } } Node::BinaryExpr { op, lhs, rhs } => { let left = eval(lhs, posting, transaction, commodities); let right = eval(rhs, posting, transaction, commodities); match op { - Binary::Eq => { - if let EvalResult::Regex(rhs) = right { - match left { - EvalResult::Account(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), - EvalResult::Payee(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + Binary::Eq => match right { + EvalResult::Regex(rhs) => match left { + EvalResult::Account(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + EvalResult::Payee(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + EvalResult::String(lhs) => match lhs { + Some(lhs) => EvalResult::Boolean(rhs.is_match(lhs.as_str())), + None => EvalResult::Boolean(false), + }, + x => panic!("Found {:?}", x), + }, + EvalResult::Money(rhs) => match left { + EvalResult::Money(lhs) => EvalResult::Boolean(lhs == rhs), + EvalResult::Number(lhs) => EvalResult::Boolean(lhs == rhs.get_amount()), + + unknown => panic!("Don't know what to do with {:?}", unknown), + }, + unknown => panic!("Don't know what to do with {:?}", unknown), + }, + Binary::Lt | Binary::Gt | Binary::Ge | Binary::Le => { + if let EvalResult::Date(lhs) = left { + match right { + EvalResult::Date(rhs) => match op { + Binary::Lt => EvalResult::Boolean(lhs < rhs), + Binary::Gt => EvalResult::Boolean(lhs > rhs), + Binary::Ge => EvalResult::Boolean(lhs >= rhs), + Binary::Le => EvalResult::Boolean(lhs <= rhs), + x => panic!("Found {:?}", x), + }, x => panic!("Found {:?}", x), } } else { - panic!("Expected regex"); + panic!("Expected Date"); } } Binary::Add | Binary::Subtract => { @@ -209,6 +270,7 @@ pub fn eval( panic!("Should be booleans") } } + unknown => panic!("Not implemented: {:?}", unknown), } } } @@ -219,10 +281,13 @@ pub enum Unary { Not, Neg, Abs, + Any, HasTag, + Tag, + ToDate, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Binary { Add, Subtract, @@ -231,6 +296,10 @@ pub enum Binary { Or, And, Eq, + Ge, + Gt, + Le, + Lt, } #[derive(Clone)] @@ -259,7 +328,11 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { "-" => Binary::Subtract, "*" => Binary::Mult, "/" => Binary::Div, - "=~" => Binary::Eq, + "=~" | "==" => Binary::Eq, + "<" => Binary::Lt, + ">" => Binary::Gt, + "<=" => Binary::Le, + ">=" => Binary::Ge, x => unreachable!("{}", x), }, }; @@ -273,16 +346,19 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { let mut inner = pair.into_inner(); let first = inner.next().unwrap(); match first.as_rule() { - Rule::unary_function | Rule::unary => { + Rule::function | Rule::unary => { let op = match first.as_str() { "abs" => Unary::Abs, "-" => Unary::Neg, "has_tag" => Unary::HasTag, + "tag" => Unary::Tag, + "to_date" => Unary::ToDate, + "not" => Unary::Not, + "any" => Unary::Any, unknown => panic!("Unknown expr: {:?}", unknown), }; parse_unary_expr(op, build_ast_from_expr(inner.next().unwrap())) } - Rule::amount => Node::Amount, Rule::money => { let mut money = first.into_inner(); let child = money.next().unwrap(); @@ -299,15 +375,25 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { } } Rule::number => Node::Number(parse_big_rational(first.as_str())), - Rule::regex => { + Rule::regex | Rule::string => { let full = first.as_str().to_string(); let n = full.len() - 1; let slice = &full[1..n]; - let regex = Regex::new(slice).unwrap(); - Node::Regex(regex) + match first.as_rule() { + Rule::regex => Node::Regex(Regex::new(slice).unwrap()), + Rule::string => Node::String(slice.to_string()), + unknown => unreachable!("This cannot happen {:?}", unknown), + } } - Rule::account => Node::Account, - Rule::payee => Node::Payee, + Rule::variable => match first.as_str() { + "account" => Node::Account, + "amount" => Node::Amount, + "payee" => Node::Payee, + "note" => Node::Note, + "date" => Node::Date, + unknown => panic!("Unknown variable: {:?}", unknown), + }, + Rule::expr => build_ast_from_expr(first), unknown => panic!("Unknown rule: {:?}", unknown), } From 80fe262ab2cd6d24f4f539cd5b2632e3685f00d9 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 19:31:14 +0100 Subject: [PATCH 10/21] fix failing tests --- CHANGELOG.md | 3 +++ src/filter.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f267c..8cdb0a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog Changelog file for dinero-rs project, a command line application for managing finances. +## [0.12.0] - 2021-02-xx (planned) +### Added +- support for (some of the) automated transaction syntax, what Claudio uses in his personal ledger ## [0.11.1] - 2021-02-22 ### Fixed - Fixed bug in balance report diff --git a/src/filter.rs b/src/filter.rs index 5b8c783..dcf4aa6 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -43,7 +43,7 @@ pub fn filter_predicate( transaction: &Transaction, commodities: &mut List, ) -> Result { - if predicate.len() == 0 { + if (predicate.len() == 0) | (predicate == "()") { return Ok(true); } let result = eval_expression(predicate, posting, transaction, commodities); @@ -65,7 +65,7 @@ pub fn filter_predicate( /// # use dinero::filter::preprocess_query; /// let params:Vec = vec!["@payee", "savings" , "and", "checking", "and", "expr", "/aeiou/"].iter().map(|x| x.to_string()).collect(); /// let processed = preprocess_query(¶ms); -/// assert_eq!(processed, "(payee =~ /(?i)payee/) or (account =~ /(?i)savings/) and (account =~ /(?i)checking/) or (/aeiou/)") +/// assert_eq!(processed, "((payee =~ /(?i)payee/) or (account =~ /(?i)savings/) and (account =~ /(?i)checking/) and (/aeiou/))") /// ``` pub fn preprocess_query(query: &Vec) -> String { let mut expression = String::new(); From b9df0bdad51dbb61eaa9127976ccd03656a4ec30 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 20:16:44 +0100 Subject: [PATCH 11/21] implement get by regex functionality #31 --- src/list.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/list.rs b/src/list.rs index b9166f4..f9a493a 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,3 +1,4 @@ +use regex::Regex; use std::collections::hash_map::{Iter, RandomState, Values}; use std::collections::HashMap; use std::fmt::Debug; @@ -15,6 +16,7 @@ use crate::LedgerError; /// - Adding new elements to the list /// - Adding new aliases to existing elements /// - Retrieving elements +/// - Retrieving elements with a regular expression #[derive(Debug, Clone)] pub struct List { aliases: HashMap, @@ -86,6 +88,22 @@ impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> Li Some(x) => Ok(x), } } + /// Gets an element from the regex + pub fn get_regex(&self, regex: Regex) -> Option<&Rc> { + // Try the list + for (_alias, value) in self.list.iter() { + if regex.is_match(value.get_name()) { + return Some(value); + } + } + for (alias, value) in self.aliases.iter() { + if regex.is_match(alias) { + return self.list.get(value); + } + } + + None + } pub fn iter(&self) -> Iter<'_, String, Rc> { self.list.iter() @@ -107,3 +125,24 @@ impl List { self.aliases.extend(other.to_owned().aliases.into_iter()); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::Payee; + use regex::Regex; + #[test] + fn list_aliases() { + let name = "ACME Inc."; + let payee = Payee::from(name); + let mut list: List = List::new(); + list.insert(payee); + + // Get ACME from the list, using a regex + let pattern = Regex::new("ACME").unwrap(); + let retrieved = list.get_regex(pattern); + + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().get_name(), "ACME Inc."); + } +} From b02becf3f65999851361c878f738437f6a89b334 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 20:38:37 +0100 Subject: [PATCH 12/21] add tests for lists #3 --- src/list.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/list.rs b/src/list.rs index f9a493a..d65a441 100644 --- a/src/list.rs +++ b/src/list.rs @@ -69,12 +69,6 @@ impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> Li () } - pub fn element_in_list(&self, element: &T) -> bool { - match self.aliases.get(&element.get_name().to_lowercase()) { - None => false, - Some(_) => true, - } - } pub fn get(&self, index: &str) -> Result<&Rc, LedgerError> { match self.list.get(&index.to_lowercase()) { None => match self.aliases.get(&index.to_lowercase()) { @@ -132,11 +126,11 @@ mod tests { use crate::models::Payee; use regex::Regex; #[test] - fn list_aliases() { + fn list() { let name = "ACME Inc."; let payee = Payee::from(name); let mut list: List = List::new(); - list.insert(payee); + list.insert(payee.clone()); // Get ACME from the list, using a regex let pattern = Regex::new("ACME").unwrap(); @@ -144,5 +138,15 @@ mod tests { assert!(retrieved.is_some()); assert_eq!(retrieved.unwrap().get_name(), "ACME Inc."); + assert_eq!(list.len_alias(), 1); + + // Now add and alias + list.add_alias("ACME is awesome".to_string(), &payee); + assert_eq!(list.len_alias(), 2); + + // Retrieve an element that is not in the list + assert!(list.get_regex(Regex::new("Warner").unwrap()).is_none()); + assert!(list.get("Warner").is_err()); + } } From 5e088daeadb8c914c5deb55737083c66e59ff647 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 21:08:00 +0100 Subject: [PATCH 13/21] remove unnecessary function --- src/list.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/list.rs b/src/list.rs index d65a441..bdf8e4b 100644 --- a/src/list.rs +++ b/src/list.rs @@ -23,11 +23,6 @@ pub struct List { list: HashMap>, } -impl Into>> for List { - fn into(self) -> HashMap, RandomState> { - self.list - } -} impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> List { pub fn new() -> Self { @@ -147,6 +142,5 @@ mod tests { // Retrieve an element that is not in the list assert!(list.get_regex(Regex::new("Warner").unwrap()).is_none()); assert!(list.get("Warner").is_err()); - } } From ccc2116576b0bb438bd1ee1fb390b33f40c22d12 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 21:23:01 +0100 Subject: [PATCH 14/21] more tests for lists #3 --- src/list.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index bdf8e4b..0e4ee94 100644 --- a/src/list.rs +++ b/src/list.rs @@ -23,7 +23,6 @@ pub struct List { list: HashMap>, } - impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> List { pub fn new() -> Self { let aliases: HashMap = HashMap::new(); @@ -142,5 +141,16 @@ mod tests { // Retrieve an element that is not in the list assert!(list.get_regex(Regex::new("Warner").unwrap()).is_none()); assert!(list.get("Warner").is_err()); + assert!(list.get_regex(Regex::new("awesome").unwrap()).is_some()); + } + #[test] + #[should_panic] + fn list_repeated_alias() { + let mut list: List = List::new(); + list.insert(Payee::from("ACME")); + for _ in 0..2 { + let retrieved = list.get("ACME").unwrap(); + list.add_alias("ACME, Inc.".to_string(), &retrieved.clone()) + } } } From 1d640bd011674f946ed71f636f65571f9b3d0122 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 21:41:25 +0100 Subject: [PATCH 15/21] some tests for payees #3 --- src/parser/tokenizers/payee.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/parser/tokenizers/payee.rs b/src/parser/tokenizers/payee.rs index 1835f9f..26cf686 100644 --- a/src/parser/tokenizers/payee.rs +++ b/src/parser/tokenizers/payee.rs @@ -77,3 +77,26 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { origin: Origin::FromDirective, }) } + +mod tests { + use super::*; + use crate::models::HasName; + #[test] + fn parse_ko() { + let input = "payee ACME ; From the Looney Tunes\n\tWrong Acme, Inc.\n".to_string(); + let mut tokenizer = Tokenizer::from(input); + let payee_raw = parse(&mut tokenizer); + assert!(payee_raw.is_err()); + } + + + #[test] + fn parse_ok() { + let input = "payee ACME\n\talias Acme, Inc.\n".to_string(); + let mut tokenizer = Tokenizer::from(input); + let payee_raw = parse(& mut tokenizer); + assert!(payee_raw.is_ok()); + let payee = payee_raw.unwrap(); + assert_eq!(payee.get_name(), "ACME"); + } +} From 60b0750cf65c54a8120f8397cdbeccc356ed7679 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Mon, 22 Feb 2021 23:04:51 +0100 Subject: [PATCH 16/21] Implement payees functionality. It looks like this fixes #31, but so far it has only been tested in toy files (added tests for that in the code) --- examples/demo.ledger | 3 +++ src/models/mod.rs | 23 ++++++++++++++++++++++- src/models/payee.rs | 4 +++- src/models/transaction.rs | 1 + src/parser/tokenizers/payee.rs | 7 ++++--- tests/test_commands.rs | 14 +++++++++++++- 6 files changed, 46 insertions(+), 6 deletions(-) diff --git a/examples/demo.ledger b/examples/demo.ledger index a08cb34..85e8011 100644 --- a/examples/demo.ledger +++ b/examples/demo.ledger @@ -34,3 +34,6 @@ commodity € commodity USD alias $ P 2021-01-23 AAPL 139.07 USD +payee ACME, inc. + alias (?i)(.*acme.*) + diff --git a/src/models/mod.rs b/src/models/mod.rs index c143274..369030d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ use num::rational::BigRational; +use regex::Regex; use std::collections::{HashMap, HashSet}; pub use account::Account; @@ -101,10 +102,30 @@ impl ParsedLedger { } // Payees + let payees_copy = self.payees.clone(); for alias in payee_strs { match self.payees.get(&alias) { Ok(_) => {} // do nothing - Err(_) => self.payees.insert(Payee::from(alias.as_str())), + Err(_) => { // Payees are actually matched by regex + let mut matched = false; + let mut alias_to_add = "".to_string(); + let mut payee_to_add = None; + 'outer: for (_, p) in payees_copy.iter() { + for p_alias in p.alias_regex.iter() { + if p_alias.is_match(alias.as_str()) { + // self.payees.add_alias(alias.to_string(), p); + payee_to_add = Some(p); + alias_to_add = alias.to_string(); + matched = true; + break 'outer; + } + } + } + if !matched {self.payees.insert(Payee::from(alias.as_str()))} + else { + self.payees.add_alias(alias_to_add, payee_to_add.unwrap()); + } + }, } } diff --git a/src/models/payee.rs b/src/models/payee.rs index 248a216..0d0eb5d 100644 --- a/src/models/payee.rs +++ b/src/models/payee.rs @@ -1,16 +1,17 @@ use crate::models::{FromDirective, HasAliases, HasName, Origin}; -use regex::Regex; use std::collections::hash_map::RandomState; use std::collections::HashSet; use std::fmt; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; +use regex::Regex; #[derive(Debug, Clone)] pub struct Payee { pub name: String, pub note: Option, pub alias: HashSet, + pub alias_regex: Vec, pub(crate) origin: Origin, } @@ -66,6 +67,7 @@ impl From<&str> for Payee { name: String::from(name), note: None, alias: Default::default(), + alias_regex: Default::default(), origin: Origin::FromTransaction, } } diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 5569962..052a663 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -76,6 +76,7 @@ impl Transaction { name: self.description.clone(), note: None, alias: Default::default(), + alias_regex: vec![], origin: Origin::FromTransaction, }; payees.insert(payee); diff --git a/src/parser/tokenizers/payee.rs b/src/parser/tokenizers/payee.rs index 26cf686..a2d7082 100644 --- a/src/parser/tokenizers/payee.rs +++ b/src/parser/tokenizers/payee.rs @@ -70,10 +70,12 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { } } + let alias_regex : Vec = alias.iter().map(|x| Regex::new(x.clone().as_str()).unwrap()).collect(); Ok(Payee { name, note, alias, + alias_regex, origin: Origin::FromDirective, }) } @@ -89,12 +91,11 @@ mod tests { assert!(payee_raw.is_err()); } - - #[test] + #[test] fn parse_ok() { let input = "payee ACME\n\talias Acme, Inc.\n".to_string(); let mut tokenizer = Tokenizer::from(input); - let payee_raw = parse(& mut tokenizer); + let payee_raw = parse(&mut tokenizer); assert!(payee_raw.is_ok()); let payee = payee_raw.unwrap(); assert_eq!(payee.get_name(), "ACME"); diff --git a/tests/test_commands.rs b/tests/test_commands.rs index 3ad6a57..fd59740 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -157,14 +157,26 @@ fn prices_command() { test_args(args); } +#[test] +/// Check the payees command +fn payees_command() { + let args = &["payees", "-f", "examples/demo.ledger"]; + let assert_1 = Command::cargo_bin("dinero").unwrap().args(args).assert(); + let output = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); + assert_eq!(output.lines().into_iter().count(), 5, "Because of the aliases, there should be only 5 payees, not 6."); + + test_args(args); +} + + #[test] /// Check the commodities command fn commodities_command() { let args = &["commodities", "-f", "examples/demo.ledger"]; let assert_1 = Command::cargo_bin("dinero").unwrap().args(args).assert(); let output = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); - assert_eq!(output.lines().into_iter().count(), 5); + assert_eq!(output.lines().into_iter().count(), 5); test_args(args); } From a8d4f5d5d69bb521cbf09df8ed08cf565de9d720 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Wed, 24 Feb 2021 01:03:38 +0100 Subject: [PATCH 17/21] Fix #36 No notes anymore internally, the search occurs looping through comments. --- src/commands/payees.rs | 2 +- src/commands/register.rs | 5 ++- src/list.rs | 4 +- src/models/mod.rs | 58 +++++++++++++------------- src/models/payee.rs | 2 +- src/models/transaction.rs | 16 ++++--- src/parser/tokenizers/payee.rs | 5 ++- src/parser/tokenizers/transaction.rs | 26 ++++++------ src/parser/value_expr.rs | 62 ++++++++++++++++++---------- tests/test_commands.rs | 7 +++- 10 files changed, 111 insertions(+), 76 deletions(-) diff --git a/src/commands/payees.rs b/src/commands/payees.rs index 033675e..273bdf6 100644 --- a/src/commands/payees.rs +++ b/src/commands/payees.rs @@ -14,7 +14,7 @@ pub fn execute(path: PathBuf, no_balance_check: bool) -> Result<(), Error> { .map(|x| x.1.deref().to_owned()) .collect::>(); payees.sort_by(|a, b| a.get_name().cmp(b.get_name())); - for payee in payees { + for payee in payees.iter() { println!("{}", payee); } Ok(()) diff --git a/src/commands/register.rs b/src/commands/register.rs index 3aa08ac..e9bb350 100644 --- a/src/commands/register.rs +++ b/src/commands/register.rs @@ -44,7 +44,10 @@ pub fn execute(options: &CommonOpts) -> Result<(), Error> { print!( "{:w1$}{:width$}", format!("{}", t.date.unwrap()), - clip(&t.description, w_description), + clip( + &format!("{} ", t.get_payee(&mut ledger.payees)), + w_description + ), width = w_description, w1 = w_date ); diff --git a/src/list.rs b/src/list.rs index 0e4ee94..b970345 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,5 +1,5 @@ use regex::Regex; -use std::collections::hash_map::{Iter, RandomState, Values}; +use std::collections::hash_map::{Iter, Values}; use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; @@ -34,7 +34,7 @@ impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> Li pub fn insert(&mut self, element: T) { let found = self.list.get(&element.get_name().to_lowercase()); match found { - Some(_) => (), // do nothing + Some(_) => eprintln!("Duplicate element: {:?}", element), // do nothing None => { // Change the name which will be used as key to lowercase let name = element.get_name().to_string().to_lowercase(); diff --git a/src/models/mod.rs b/src/models/mod.rs index 369030d..5064a1b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,4 @@ use num::rational::BigRational; -use regex::Regex; use std::collections::{HashMap, HashSet}; pub use account::Account; @@ -65,7 +64,9 @@ impl ParsedLedger { for transaction in self.transactions.iter() { for p in transaction.postings.iter() { account_strs.insert(p.account.clone()); - payee_strs.insert(p.payee.clone()); + if let Some(payee) = p.payee.clone() { + payee_strs.insert(payee); + } // Currencies if let Some(c) = &p.money_currency { commodity_strs.insert(c.clone()); @@ -106,12 +107,14 @@ impl ParsedLedger { for alias in payee_strs { match self.payees.get(&alias) { Ok(_) => {} // do nothing - Err(_) => { // Payees are actually matched by regex + Err(_) => { + // Payees are actually matched by regex let mut matched = false; let mut alias_to_add = "".to_string(); let mut payee_to_add = None; 'outer: for (_, p) in payees_copy.iter() { for p_alias in p.alias_regex.iter() { + // println!("{:?}", p_alias); // todo delete if p_alias.is_match(alias.as_str()) { // self.payees.add_alias(alias.to_string(), p); payee_to_add = Some(p); @@ -121,11 +124,12 @@ impl ParsedLedger { } } } - if !matched {self.payees.insert(Payee::from(alias.as_str()))} - else { + if !matched { + self.payees.insert(Payee::from(alias.as_str())) + } else { self.payees.add_alias(alias_to_add, payee_to_add.unwrap()); } - }, + } } } @@ -219,19 +223,24 @@ impl ParsedLedger { } for auto_posting in automated.postings_iter() { let account_alias = auto_posting.account.clone(); - let payee_alias = auto_posting.payee.clone(); match self.accounts.get(&account_alias) { Ok(_) => {} // do nothing Err(_) => { self.accounts.insert(Account::from(account_alias.as_str())) } } - match self.payees.get(&payee_alias) { - Ok(_) => {} // do nothing - Err(_) => self.payees.insert(Payee::from(payee_alias.as_str())), - } + let payee = if let Some(payee_alias) = &auto_posting.payee { + match self.payees.get(&payee_alias) { + Ok(_) => {} // do nothing + Err(_) => { + self.payees.insert(Payee::from(payee_alias.as_str())) + } + } + Some(self.payees.get(&payee_alias).unwrap().clone()) + } else { + p.payee.clone() + }; let account = self.accounts.get(&account_alias).unwrap(); - let payee = self.payees.get(&payee_alias).unwrap(); let money = match &auto_posting.money_currency { None => Some(value_expr::eval_value_expression( auto_posting.amount_expr.clone().unwrap().as_str(), @@ -268,9 +277,9 @@ impl ParsedLedger { cost: None, kind: auto_posting.kind, tags: vec![], - payee: payee.clone(), + payee, }; - // println!("{:?}", posting); + match auto_posting.kind { PostingType::Real => extra_postings.push(posting), PostingType::Virtual => extra_virtual_postings.push(posting), @@ -350,17 +359,21 @@ impl ParsedLedger { let mut transaction = Transaction::::new(TransactionType::Real); transaction.description = parsed.description.clone(); transaction.code = parsed.code.clone(); - transaction.note = parsed.note.clone(); + transaction.comments = parsed.comments.clone(); transaction.date = parsed.date; transaction.effective_date = parsed.effective_date; + for comment in parsed.comments.iter() { transaction.tags.append(&mut comment.get_tags()); } // Go posting by posting for p in parsed.postings.iter() { let account = self.accounts.get(&p.account)?; - let payee = self.payees.get(&p.payee)?; - let mut posting: Posting = Posting::new(account, p.kind, payee); + let payee = match &p.payee { + None => transaction.get_payee_inmutable(&self.payees), + Some(x) => self.payees.get(x).unwrap().clone(), + }; + let mut posting: Posting = Posting::new(account, p.kind, &payee); posting.tags = transaction.tags.clone(); for comment in p.comments.iter() { posting.tags.append(&mut comment.get_tags()); @@ -460,14 +473,3 @@ pub trait HasAliases { pub trait FromDirective { fn is_from_directive(&self) -> bool; } - -fn _output_balances(bal: &HashMap, Balance>) { - let mut s = String::new(); - for (k, v) in bal.iter() { - if v.is_zero() { - continue; - } - s.push_str(format!("{}: {}\n", k.get_name(), v).as_str()); - } - println!("{}", s); -} diff --git a/src/models/payee.rs b/src/models/payee.rs index 0d0eb5d..cad17a1 100644 --- a/src/models/payee.rs +++ b/src/models/payee.rs @@ -1,10 +1,10 @@ use crate::models::{FromDirective, HasAliases, HasName, Origin}; +use regex::Regex; use std::collections::hash_map::RandomState; use std::collections::HashSet; use std::fmt; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; -use regex::Regex; #[derive(Debug, Clone)] pub struct Payee { diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 052a663..31d0d28 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -26,7 +26,6 @@ pub struct Transaction { pub cleared: Cleared, pub code: Option, pub description: String, - pub note: Option, pub payee: Option, pub postings: Vec, pub virtual_postings: Vec, @@ -84,6 +83,12 @@ impl Transaction { } } } + pub fn get_payee_inmutable(&self, payees: &List) -> Rc { + match payees.get(&self.description) { + Ok(x) => x.clone(), + Err(_) => panic!("Payee not found"), + } + } } #[derive(Debug, Copy, Clone)] @@ -122,7 +127,7 @@ pub struct Posting { pub cost: Option, pub kind: PostingType, pub tags: Vec, - pub payee: Rc, + pub payee: Option>, } impl Posting { @@ -134,7 +139,7 @@ impl Posting { cost: None, kind: kind, tags: vec![], - payee: Rc::new(payee.clone()), + payee: Some(Rc::new(payee.clone())), } } pub fn set_amount(&mut self, money: Money) { @@ -181,7 +186,6 @@ impl Transaction { cleared: Cleared::Unknown, code: None, description: "".to_string(), - note: None, payee: None, postings: vec![], virtual_postings: vec![], @@ -284,7 +288,7 @@ impl Transaction { // 1. Iterate over postings let mut fill_account = &Rc::new(Account::from("this will never be used")); - let mut fill_payee = &Rc::new(Payee::from("this will never be used")); + let mut fill_payee = None; let mut postings: Vec = Vec::new(); for p in self.postings.iter() { // If it has money, update the balance @@ -375,7 +379,7 @@ impl Transaction { } else { // We do nothing, but this is the account for the empty post fill_account = &p.account; - fill_payee = &p.payee; + fill_payee = p.payee.clone(); } } diff --git a/src/parser/tokenizers/payee.rs b/src/parser/tokenizers/payee.rs index a2d7082..863f9f2 100644 --- a/src/parser/tokenizers/payee.rs +++ b/src/parser/tokenizers/payee.rs @@ -70,7 +70,10 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { } } - let alias_regex : Vec = alias.iter().map(|x| Regex::new(x.clone().as_str()).unwrap()).collect(); + let alias_regex: Vec = alias + .iter() + .map(|x| Regex::new(x.clone().as_str()).unwrap()) + .collect(); Ok(Payee { name, note, diff --git a/src/parser/tokenizers/transaction.rs b/src/parser/tokenizers/transaction.rs index ef57001..3442ec7 100644 --- a/src/parser/tokenizers/transaction.rs +++ b/src/parser/tokenizers/transaction.rs @@ -89,9 +89,14 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result - // note + // payee { - transaction.payee = Some(m.as_str().to_string()) + if real { + match m.as_str() { + "" => (), + x => transaction.payee = Some(x.to_string()), + } + } } 7 => // note @@ -104,7 +109,9 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result (), } } - + if real & transaction.payee.is_none() { + transaction.payee = Some(transaction.description.clone()); + } // Have a flag so that it can be known whether a comment belongs to the transaction or to the // posting let mut parsed_posting = false; @@ -119,7 +126,7 @@ fn parse_generic(tokenizer: &mut Tokenizer, real: bool) -> Result Result { - match parse_posting( - tokenizer, - transaction.transaction_type, - transaction.description.clone(), - ) { + match parse_posting(tokenizer, transaction.transaction_type, &transaction.payee) { // Although here we already know the kind of the posting (virtual, real), // we deal with that in the next phase of parsing Ok(posting) => transaction.postings.push(posting), @@ -171,7 +174,7 @@ pub struct RawPosting { pub comments: Vec, pub amount_expr: Option, pub kind: PostingType, - pub payee: String, + pub payee: Option, } /// Parses a posting @@ -179,7 +182,7 @@ pub struct RawPosting { fn parse_posting( tokenizer: &mut Tokenizer, transaction_type: TransactionType, - default_payee: String, + default_payee: &Option, ) -> Result { let mut account = String::new(); let mut posting_type = PostingType::Real; @@ -395,7 +398,6 @@ fn parse_amount(tokenizer: &mut Tokenizer) -> Result { match BigInt::from_str(num.as_str()) { Ok(n) => n, Err(_) => { - // eprintln!("I fail here 372."); //todo delete return Err(ParserError::UnexpectedInput(Some( "Wrong number format".to_string(), ))); diff --git a/src/parser/value_expr.rs b/src/parser/value_expr.rs index 133551e..9d27204 100644 --- a/src/parser/value_expr.rs +++ b/src/parser/value_expr.rs @@ -28,7 +28,7 @@ pub fn eval_expression( // Build the abstract syntax tree let root = build_ast_from_expr(parsed); - + // println!("{:?}", expression); //todo delete eval(&root, posting, transaction, commodities) } @@ -45,7 +45,7 @@ pub fn eval_value_expression( } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Node { Amount, Account, @@ -80,6 +80,7 @@ pub enum EvalResult { Regex(Regex), String(Option), Date(NaiveDate), + Note, } pub fn eval( @@ -88,11 +89,11 @@ pub fn eval( transaction: &Transaction, commodities: &mut List, ) -> EvalResult { - match node { + let res = match node { Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()), Node::Account => EvalResult::Account(posting.account.clone()), - Node::Payee => EvalResult::Payee(posting.payee.clone()), - Node::Note => EvalResult::String(transaction.note.clone()), + Node::Payee => EvalResult::Payee(posting.payee.clone().unwrap()), + Node::Note => EvalResult::Note, Node::Date => EvalResult::Date(transaction.date.clone().unwrap()), Node::Regex(r) => EvalResult::Regex(r.clone()), Node::String(r) => EvalResult::String(Some(r.clone())), @@ -165,25 +166,40 @@ pub fn eval( let left = eval(lhs, posting, transaction, commodities); let right = eval(rhs, posting, transaction, commodities); match op { - Binary::Eq => match right { - EvalResult::Regex(rhs) => match left { - EvalResult::Account(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), - EvalResult::Payee(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), - EvalResult::String(lhs) => match lhs { - Some(lhs) => EvalResult::Boolean(rhs.is_match(lhs.as_str())), - None => EvalResult::Boolean(false), + Binary::Eq => { + // println!("{:?} eq {:?}", left, right); // todo delete + match right { + EvalResult::Regex(rhs) => match left { + EvalResult::Account(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + EvalResult::Payee(lhs) => EvalResult::Boolean(lhs.is_match(rhs)), + EvalResult::String(lhs) => match lhs { + Some(lhs) => EvalResult::Boolean(rhs.is_match(lhs.as_str())), + None => EvalResult::Boolean(false), + }, + EvalResult::Note => { + let mut result = false; + for comment in transaction.comments.iter() { + // println!("{:?} -- {}", rhs, comment.comment); //todo delete + if rhs.is_match(comment.comment.as_str()) { + result = true; + break; + } + } + EvalResult::Boolean(result) + } + x => panic!("Found {:?}", x), }, - x => panic!("Found {:?}", x), - }, - EvalResult::Money(rhs) => match left { - EvalResult::Money(lhs) => EvalResult::Boolean(lhs == rhs), - EvalResult::Number(lhs) => EvalResult::Boolean(lhs == rhs.get_amount()), + EvalResult::Money(rhs) => match left { + EvalResult::Money(lhs) => EvalResult::Boolean(lhs == rhs), + EvalResult::Number(lhs) => EvalResult::Boolean(lhs == rhs.get_amount()), + unknown => panic!("Don't know what to do with {:?}", unknown), + }, unknown => panic!("Don't know what to do with {:?}", unknown), - }, - unknown => panic!("Don't know what to do with {:?}", unknown), - }, + } + } Binary::Lt | Binary::Gt | Binary::Ge | Binary::Le => { + // println!("{:?} {:?} {:?}", left, op, right); // todo delete if let EvalResult::Date(lhs) = left { match right { EvalResult::Date(rhs) => match op { @@ -273,10 +289,12 @@ pub fn eval( unknown => panic!("Not implemented: {:?}", unknown), } } - } + }; + // println!("Result: {:?}", res); //todo delete + res } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Unary { Not, Neg, diff --git a/tests/test_commands.rs b/tests/test_commands.rs index fd59740..c046fe4 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -163,12 +163,15 @@ fn payees_command() { let args = &["payees", "-f", "examples/demo.ledger"]; let assert_1 = Command::cargo_bin("dinero").unwrap().args(args).assert(); let output = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); - assert_eq!(output.lines().into_iter().count(), 5, "Because of the aliases, there should be only 5 payees, not 6."); + assert_eq!( + output.lines().into_iter().count(), + 5, + "Because of the aliases, there should be only 5 payees, not 6." + ); test_args(args); } - #[test] /// Check the commodities command fn commodities_command() { From 2a13b5f6aad05addc94ee6f36b3600fa648a2fd9 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Wed, 24 Feb 2021 01:19:48 +0100 Subject: [PATCH 18/21] Fix #27, fix #31 If an account ends with "unknown", change it based on payees --- src/models/account.rs | 2 +- src/models/mod.rs | 20 ++++++++++++++++++-- src/parser/tokenizers/account.rs | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/models/account.rs b/src/models/account.rs index 1981aa4..67118b9 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -15,7 +15,7 @@ pub struct Account { pub(crate) aliases: HashSet, pub(crate) check: Vec, pub(crate) assert: Vec, - pub(crate) payee: Vec, + pub(crate) payee: Vec, pub(crate) default: bool, } diff --git a/src/models/mod.rs b/src/models/mod.rs index 5064a1b..0d46e96 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -368,12 +368,28 @@ impl ParsedLedger { } // Go posting by posting for p in parsed.postings.iter() { - let account = self.accounts.get(&p.account)?; let payee = match &p.payee { None => transaction.get_payee_inmutable(&self.payees), Some(x) => self.payees.get(x).unwrap().clone(), }; - let mut posting: Posting = Posting::new(account, p.kind, &payee); + let account = if p.account.to_lowercase().ends_with("unknown") { + let mut account = None; + for (_, acc) in self.accounts.iter() { + for alias in acc.payee.iter() { + if alias.is_match(payee.get_name()) { + account = Some(acc.clone()); + break; + } + } + } + match account { + Some(x) => x, + None => self.accounts.get(&p.account)?.clone(), + } + } else { + self.accounts.get(&p.account)?.clone() + }; + let mut posting: Posting = Posting::new(&account, p.kind, &payee); posting.tags = transaction.tags.clone(); for comment in p.comments.iter() { posting.tags.append(&mut comment.get_tags()); diff --git a/src/parser/tokenizers/account.rs b/src/parser/tokenizers/account.rs index 1105fe5..a6f0ec1 100644 --- a/src/parser/tokenizers/account.rs +++ b/src/parser/tokenizers/account.rs @@ -22,7 +22,7 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { let mut aliases = HashSet::new(); let mut check = Vec::new(); let mut assert = Vec::new(); - let mut payee = Vec::new(); + let mut payee: Vec = Vec::new(); let mut note = None; let mut isin = None; @@ -68,7 +68,7 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { assert.push(chars::get_line(tokenizer).trim().to_string()); } "payee" => { - payee.push(chars::get_line(tokenizer).trim().to_string()); + payee.push(Regex::new(chars::get_line(tokenizer).trim()).unwrap()); } "default" => default = true, found => { From c5c558daf93dbb522266ca9ab27f50af35b4ae65 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Wed, 24 Feb 2021 02:01:23 +0100 Subject: [PATCH 19/21] improve test coverage #3 --- examples/demo.ledger | 5 +++-- src/models/mod.rs | 12 ------------ tests/test_commands.rs | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/examples/demo.ledger b/examples/demo.ledger index 85e8011..402e4ee 100644 --- a/examples/demo.ledger +++ b/examples/demo.ledger @@ -14,7 +14,7 @@ 2021-01-15 * Flights ; destination: spain - Expenses:Travel 200 EUR + Expenses:Unknown 200 EUR ; this will get translated to Expenses:Flights Assets:Bank:Checking account $-210.12 2021-01-16 * Alphabet Inc. @@ -36,4 +36,5 @@ commodity USD P 2021-01-23 AAPL 139.07 USD payee ACME, inc. alias (?i)(.*acme.*) - +account Expenses:Travel + payee Flights diff --git a/src/models/mod.rs b/src/models/mod.rs index 0d46e96..8cf910e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -40,18 +40,6 @@ pub struct Ledger { pub(crate) payees: List, } -impl Ledger { - pub fn new() -> Self { - Ledger { - accounts: List::::new(), - prices: vec![], - transactions: vec![], - commodities: List::::new(), - payees: List::::new(), - } - } -} - impl ParsedLedger { /// Creates a proper ledger from a parsed ledger pub fn to_ledger(mut self, no_checks: bool) -> Result { diff --git a/tests/test_commands.rs b/tests/test_commands.rs index c046fe4..6b2de8e 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -111,7 +111,7 @@ fn accounts_command() { let args = &["accounts", "-f", "examples/demo.ledger"]; let assert_1 = Command::cargo_bin("dinero").unwrap().args(args).assert(); let output = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); - assert_eq!(output.lines().into_iter().count(), 6); + assert_eq!(output.lines().into_iter().count(), 7); test_args(args); } From bd68e4699fe5a4b2843d5e6eb0946cd458f121c6 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Wed, 24 Feb 2021 03:22:51 +0100 Subject: [PATCH 20/21] Speed bump by passing around a regexes Hashmap It goes from 44 seconds to 7 seconds in a command over my personal ledger --- src/filter.rs | 13 ++++++++++-- src/models/mod.rs | 3 +++ src/parser/value_expr.rs | 45 +++++++++++++++++++++++++++------------- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index dcf4aa6..c80b5c3 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -2,6 +2,8 @@ use crate::models::{Currency, Posting, PostingType, Transaction}; use crate::parser::value_expr::{eval_expression, EvalResult}; use crate::{CommonOpts, Error, List}; use colored::Colorize; +use regex::Regex; +use std::collections::HashMap; pub fn filter( options: &CommonOpts, @@ -34,7 +36,13 @@ pub fn filter( } } - filter_predicate(predicate.as_str(), posting, transaction, commodities) + filter_predicate( + predicate.as_str(), + posting, + transaction, + commodities, + &mut HashMap::new(), + ) } pub fn filter_predicate( @@ -42,11 +50,12 @@ pub fn filter_predicate( posting: &Posting, transaction: &Transaction, commodities: &mut List, + regexes: &mut HashMap, ) -> Result { if (predicate.len() == 0) | (predicate == "()") { return Ok(true); } - let result = eval_expression(predicate, posting, transaction, commodities); + let result = eval_expression(predicate, posting, transaction, commodities, regexes); match result { EvalResult::Boolean(b) => Ok(b), _ => Err(Error { diff --git a/src/models/mod.rs b/src/models/mod.rs index 8cf910e..19cc9bd 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -194,6 +194,7 @@ impl ParsedLedger { // 5. Go over the transactions again and see if there is something we need to do with them if automated_transactions.len() > 0 { + let mut regexes = HashMap::new(); for automated in automated_transactions.iter_mut() { for t in transactions.iter_mut() { let mut extra_postings = vec![]; @@ -205,6 +206,7 @@ impl ParsedLedger { p, t, &mut self.commodities, + &mut regexes, )? { for comment in t.comments.iter() { p.to_owned().tags.append(&mut comment.get_tags()); @@ -235,6 +237,7 @@ impl ParsedLedger { p, t, &mut self.commodities, + &mut regexes, )), Some(alias) => { if alias == "" { diff --git a/src/parser/value_expr.rs b/src/parser/value_expr.rs index 9d27204..7364ce5 100644 --- a/src/parser/value_expr.rs +++ b/src/parser/value_expr.rs @@ -8,6 +8,8 @@ use regex::Regex; use std::rc::Rc; use std::str::FromStr; +use std::collections::HashMap; + #[derive(Parser)] #[grammar = "grammar/expressions.pest"] pub struct ValueExpressionParser; @@ -17,6 +19,7 @@ pub fn eval_expression( posting: &Posting, transaction: &Transaction, commodities: &mut List, + regexes: &mut HashMap, ) -> EvalResult { let parsed = ValueExpressionParser::parse(Rule::value_expr, expression) .expect("unsuccessful parse") // unwrap the parse result @@ -27,9 +30,9 @@ pub fn eval_expression( .unwrap(); // Build the abstract syntax tree - let root = build_ast_from_expr(parsed); + let root = build_ast_from_expr(parsed, regexes); // println!("{:?}", expression); //todo delete - eval(&root, posting, transaction, commodities) + eval(&root, posting, transaction, commodities, regexes) } pub fn eval_value_expression( @@ -37,8 +40,9 @@ pub fn eval_value_expression( posting: &Posting, transaction: &Transaction, commodities: &mut List, + regexes: &mut HashMap, ) -> Money { - match eval_expression(expression, posting, transaction, commodities) { + match eval_expression(expression, posting, transaction, commodities, regexes) { EvalResult::Number(n) => posting.amount.clone().unwrap() * n, EvalResult::Money(m) => m, _ => panic!("Should be money"), @@ -88,6 +92,7 @@ pub fn eval( posting: &Posting, transaction: &Transaction, commodities: &mut List, + regexes: &mut HashMap, ) -> EvalResult { let res = match node { Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()), @@ -110,7 +115,7 @@ pub fn eval( EvalResult::Money(Money::from((cur.clone(), amount.clone()))) } Node::UnaryExpr { op, child } => { - let res = eval(child, posting, transaction, commodities); + let res = eval(child, posting, transaction, commodities, regexes); match op { Unary::Not => match res { EvalResult::Boolean(b) => EvalResult::Boolean(!b), @@ -119,7 +124,9 @@ pub fn eval( Unary::Any => { let mut res = false; for p in transaction.postings_iter() { - if let EvalResult::Boolean(b) = eval(child, p, transaction, commodities) { + if let EvalResult::Boolean(b) = + eval(child, p, transaction, commodities, regexes) + { if b { res = true; break; @@ -163,8 +170,8 @@ pub fn eval( } } Node::BinaryExpr { op, lhs, rhs } => { - let left = eval(lhs, posting, transaction, commodities); - let right = eval(rhs, posting, transaction, commodities); + let left = eval(lhs, posting, transaction, commodities, regexes); + let right = eval(rhs, posting, transaction, commodities, regexes); match op { Binary::Eq => { // println!("{:?} eq {:?}", left, right); // todo delete @@ -323,10 +330,13 @@ pub enum Binary { #[derive(Clone)] pub enum Ternary {} -fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { +fn build_ast_from_expr( + pair: pest::iterators::Pair, + regexes: &mut HashMap, +) -> Node { let rule = pair.as_rule(); match rule { - Rule::expr => build_ast_from_expr(pair.into_inner().next().unwrap()), + Rule::expr => build_ast_from_expr(pair.into_inner().next().unwrap(), regexes), Rule::comparison_expr | Rule::or_expr | Rule::and_expr @@ -334,7 +344,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { | Rule::multiplicative_expr => { let mut pair = pair.into_inner(); let lhspair = pair.next().unwrap(); - let lhs = build_ast_from_expr(lhspair); + let lhs = build_ast_from_expr(lhspair, regexes); match pair.next() { None => lhs, Some(x) => { @@ -355,7 +365,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { }, }; let rhspair = pair.next().unwrap(); - let rhs = build_ast_from_expr(rhspair); + let rhs = build_ast_from_expr(rhspair, regexes); parse_binary_expr(op, lhs, rhs) } } @@ -375,7 +385,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { "any" => Unary::Any, unknown => panic!("Unknown expr: {:?}", unknown), }; - parse_unary_expr(op, build_ast_from_expr(inner.next().unwrap())) + parse_unary_expr(op, build_ast_from_expr(inner.next().unwrap(), regexes)) } Rule::money => { let mut money = first.into_inner(); @@ -398,7 +408,14 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { let n = full.len() - 1; let slice = &full[1..n]; match first.as_rule() { - Rule::regex => Node::Regex(Regex::new(slice).unwrap()), + Rule::regex => match regexes.get(slice) { + None => { + let regex = Regex::new(slice).unwrap(); + regexes.insert(slice.to_string(), regex.clone()); + Node::Regex(regex) + } + Some(regex) => Node::Regex(regex.clone()), + }, Rule::string => Node::String(slice.to_string()), unknown => unreachable!("This cannot happen {:?}", unknown), } @@ -411,7 +428,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { "date" => Node::Date, unknown => panic!("Unknown variable: {:?}", unknown), }, - Rule::expr => build_ast_from_expr(first), + Rule::expr => build_ast_from_expr(first, regexes), unknown => panic!("Unknown rule: {:?}", unknown), } From e95758ee31e599f85a852922a84986b745e266f9 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Wed, 24 Feb 2021 03:27:01 +0100 Subject: [PATCH 21/21] changelog update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cdb0a2..8d28446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changelog file for dinero-rs project, a command line application for managing fi ## [0.12.0] - 2021-02-xx (planned) ### Added - support for (some of the) automated transaction syntax, what Claudio uses in his personal ledger +### Fixed +- speed bump (from 44 seconds to 7 seconds) in a big personal ledger ## [0.11.1] - 2021-02-22 ### Fixed - Fixed bug in balance report