From d0c93bad3d4e8a6d5d929e946a4df585a252f2c9 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Fri, 12 Feb 2021 23:50:44 +0100 Subject: [PATCH 01/12] filter refactor --- src/lib/filter.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/filter.rs b/src/lib/filter.rs index 23604a0..a67223f 100644 --- a/src/lib/filter.rs +++ b/src/lib/filter.rs @@ -5,7 +5,6 @@ pub fn filter(options: &CommonOpts, transaction: &Transaction, posting: // Get what's needed let predicate = &options.query; let real = options.real; - let name = posting.account.get_name().to_lowercase(); // Check for real postings if real { @@ -27,7 +26,11 @@ pub fn filter(options: &CommonOpts, transaction: &Transaction, posting: return false; } } - + return filter_predicate(predicate, posting); +} +pub fn filter_predicate(predicate: &Vec,posting: &Posting) -> bool { + + let name = posting.account.get_name().to_lowercase(); if predicate.len() == 0 { return true; } From f894e002a0e196a66c1dfa197a1d572ef00fcefb Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 14 Feb 2021 11:58:25 +0100 Subject: [PATCH 02/12] structure done but not quite --- Cargo.toml | 2 +- src/lib/filter.rs | 6 +- src/lib/models/mod.rs | 272 +++++++++++++++-------- src/lib/parser/mod.rs | 2 +- src/lib/parser/tokenizers/transaction.rs | 19 +- 5 files changed, 198 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6e29756..9c4f91b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Claudio Noguera "] edition = "2018" readme = "README.md" description = "A command line ledger tool" -keywords = ["ledger"] +keywords = ["ledger", "plaintext-accounting"] license = "MIT" homepage = "https://github.com/frosklis/dinero-rs" repository = "https://github.com/frosklis/dinero-rs" diff --git a/src/lib/filter.rs b/src/lib/filter.rs index a67223f..593b1f2 100644 --- a/src/lib/filter.rs +++ b/src/lib/filter.rs @@ -28,13 +28,13 @@ pub fn filter(options: &CommonOpts, transaction: &Transaction, posting: } return filter_predicate(predicate, posting); } -pub fn filter_predicate(predicate: &Vec,posting: &Posting) -> bool { - +pub fn filter_predicate(predicate: &Vec, posting: &Posting) -> bool { let name = posting.account.get_name().to_lowercase(); if predicate.len() == 0 { return true; } - for p in predicate { + for pred in predicate { + let p = pred.trim(); if p.starts_with("%") { // look in the posting tags for tag in posting.tags.iter() { diff --git a/src/lib/models/mod.rs b/src/lib/models/mod.rs index e17150b..3d88665 100644 --- a/src/lib/models/mod.rs +++ b/src/lib/models/mod.rs @@ -14,8 +14,9 @@ pub use transaction::{ Cleared, Posting, PostingType, Transaction, TransactionStatus, TransactionType, }; -use crate::models::transaction::Cost; +use crate::parser::tokenizers; use crate::parser::ParsedLedger; +use crate::{filter::filter_predicate, models::transaction::Cost}; use crate::{Error, List}; use num::BigInt; use std::rc::Rc; @@ -123,98 +124,86 @@ impl ParsedLedger { // 4. Get the right postings // let mut transactions = Vec::new(); + let mut automated_transactions = Vec::new(); for parsed in self.transactions.iter() { - match parsed.transaction_type { - TransactionType::Real => {} - TransactionType::Automated => { - eprintln!("Found automated transaction. Skipping."); - continue; - } - TransactionType::Periodic => { - eprintln!("Found periodic transaction. Skipping."); - continue; - } - } - let mut transaction = Transaction::::new(TransactionType::Real); - transaction.description = parsed.description.clone(); - transaction.code = parsed.code.clone(); - transaction.note = parsed.note.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 (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); + } - let mut posting: Posting = Posting::new(account, p.kind); - 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(( - self.commodities.get(&c.as_str()).unwrap().clone(), - p.money_amount.clone().unwrap(), - ))); - } - if let Some(c) = &p.cost_currency { - let posting_currency = self - .commodities - .get(&p.money_currency.as_ref().unwrap().as_str()) - .unwrap(); - let amount = Money::from(( - self.commodities.get(c.as_str()).unwrap().clone(), - p.cost_amount.clone().unwrap(), - )); - posting.cost = match p.cost_type.as_ref().unwrap() { - PriceType::Total => Some(Cost::Total { - amount: amount.clone(), - }), - PriceType::PerUnit => Some(Cost::PerUnit { - amount: amount.clone(), - }), - }; - prices.push(Price { - date: transaction.date.unwrap(), - commodity: posting_currency.clone(), - price: Money::Money { - amount: p.cost_amount.clone().unwrap() - / match p.cost_type.as_ref().unwrap() { - PriceType::Total => { - posting.amount.as_ref().unwrap().get_amount() + // 5. Go over the transactions again and see if there is something we need to do with them + for automated in automated_transactions.iter() { + 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 p.amount.is_none() { + continue; + } + if filter_predicate(&vec![automated.description.clone()], p) { + for comment in t.comments.iter() { + p.to_owned().tags.append(&mut comment.get_tags()); + } + for auto_posting in automated.postings_iter() { + let account_alias = auto_posting.account.clone(); + match self.accounts.get(&account_alias) { + Ok(_) => {} // do nothing + Err(_) => { + self.accounts.insert(Account::from(account_alias.as_str())) + } + } + let account = self.accounts.get(&account_alias).unwrap(); + let money = match &auto_posting.money_currency { + None => None, + Some(alias) => { + if alias == "" { + Some(Money::from(( + p.amount.clone().unwrap().get_commodity().unwrap(), + p.amount.clone().unwrap().get_amount() + * auto_posting.money_amount.clone().unwrap(), + ))) + } else { + match self.commodities.get(&alias) { + Ok(_) => {} // do nothing + Err(_) => self + .commodities + .insert(Currency::from(alias.as_str())), + } + Some(Money::from(( + self.commodities.get(alias).unwrap().clone(), + auto_posting.money_amount.clone().unwrap(), + ))) } - PriceType::PerUnit => BigRational::from(BigInt::from(1)), - }, - currency: amount.get_commodity().unwrap().clone(), - }, - }) - } - if let Some(c) = &p.balance_currency { - posting.balance = Some(Money::from(( - self.commodities.get(c.as_str()).unwrap().clone(), - p.balance_amount.clone().unwrap(), - ))); - } - match posting.kind { - PostingType::Real => transaction.postings.push(posting.to_owned()), - PostingType::Virtual => transaction.virtual_postings.push(posting.to_owned()), - PostingType::VirtualMustBalance => transaction - .virtual_postings_balance - .push(posting.to_owned()), + } + }; + let posting = Posting { + account: account.clone(), + amount: money, + balance: None, + cost: None, + kind: auto_posting.kind, + tags: vec![], + }; + println!("{:?}", posting); + match auto_posting.kind { + PostingType::Real => extra_postings.push(posting), + PostingType::Virtual => extra_virtual_postings.push(posting), + PostingType::VirtualMustBalance => { + extra_virtual_postings_balance.push(posting) + } + } + } + // todo!("Need to work on transaction automation"); + } } + t.postings.append(&mut extra_postings); + t.virtual_postings.append(&mut extra_virtual_postings); + t.virtual_postings_balance + .append(&mut extra_virtual_postings_balance); } - match transaction.clone().is_balanced() { - true => { - transaction.status = TransactionStatus::InternallyBalanced; - } - false => {} - } - transactions.push(transaction); } - // Now sort the transactions vector by date transactions.sort_by(|a, b| a.date.unwrap().cmp(&b.date.unwrap())); @@ -259,6 +248,115 @@ impl ParsedLedger { prices, }) } + + fn _transaction_to_ledger( + &self, + parsed: &Transaction, + ) -> Result< + ( + Vec>, + Vec>, + Vec, + ), + Error, + > { + let mut automated_transactions = vec![]; + let mut prices = vec![]; + let mut transactions = vec![]; + match parsed.transaction_type { + TransactionType::Real => { + let mut transaction = Transaction::::new(TransactionType::Real); + transaction.description = parsed.description.clone(); + transaction.code = parsed.code.clone(); + transaction.note = parsed.note.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 mut posting: Posting = Posting::new(account, p.kind); + 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(( + self.commodities.get(&c.as_str()).unwrap().clone(), + p.money_amount.clone().unwrap(), + ))); + } + if let Some(c) = &p.cost_currency { + let posting_currency = self + .commodities + .get(&p.money_currency.as_ref().unwrap().as_str()) + .unwrap(); + let amount = Money::from(( + self.commodities.get(c.as_str()).unwrap().clone(), + p.cost_amount.clone().unwrap(), + )); + posting.cost = match p.cost_type.as_ref().unwrap() { + PriceType::Total => Some(Cost::Total { + amount: amount.clone(), + }), + PriceType::PerUnit => Some(Cost::PerUnit { + amount: amount.clone(), + }), + }; + prices.push(Price { + date: transaction.date.unwrap(), + commodity: posting_currency.clone(), + price: Money::Money { + amount: p.cost_amount.clone().unwrap() + / match p.cost_type.as_ref().unwrap() { + PriceType::Total => { + posting.amount.as_ref().unwrap().get_amount() + } + PriceType::PerUnit => BigRational::from(BigInt::from(1)), + }, + currency: amount.get_commodity().unwrap().clone(), + }, + }) + } + if let Some(c) = &p.balance_currency { + posting.balance = Some(Money::from(( + self.commodities.get(c.as_str()).unwrap().clone(), + p.balance_amount.clone().unwrap(), + ))); + } + match posting.kind { + PostingType::Real => transaction.postings.push(posting.to_owned()), + PostingType::Virtual => { + transaction.virtual_postings.push(posting.to_owned()) + } + PostingType::VirtualMustBalance => transaction + .virtual_postings_balance + .push(posting.to_owned()), + } + } + match transaction.clone().is_balanced() { + true => { + transaction.status = TransactionStatus::InternallyBalanced; + } + false => {} + } + transactions.push(transaction); + } + TransactionType::Automated => { + // Add transaction to the automated transactions queue, we'll process them + // later. + automated_transactions.push(parsed.clone()); + } + TransactionType::Periodic => { + eprintln!("Found periodic transaction. Skipping."); + } + } + Ok((transactions, automated_transactions, prices)) + } } #[derive(Copy, Clone, Debug)] diff --git a/src/lib/parser/mod.rs b/src/lib/parser/mod.rs index 5805daf..23794e6 100644 --- a/src/lib/parser/mod.rs +++ b/src/lib/parser/mod.rs @@ -19,7 +19,7 @@ use crate::{models, Error, List, ParserError}; mod chars; mod include; -mod tokenizers; +pub mod tokenizers; use tokenizers::{account, comment, commodity, payee, price, tag, transaction}; diff --git a/src/lib/parser/tokenizers/transaction.rs b/src/lib/parser/tokenizers/transaction.rs index 1161898..444acc8 100644 --- a/src/lib/parser/tokenizers/transaction.rs +++ b/src/lib/parser/tokenizers/transaction.rs @@ -34,7 +34,7 @@ pub(crate) fn parse_generic<'a>( r"(.*)" , // description r"( ;.*)?" , // note ).as_str()).unwrap(); - static ref RE_AUTOMATED: Regex = Regex::new(format!("{}",r"(.*)" ).as_str()).unwrap(); + static ref RE_AUTOMATED: Regex = Regex::new(format!("{}",r"=(.*)" ).as_str()).unwrap(); } let mystr = chars::get_line(tokenizer); let caps = match real { @@ -284,18 +284,15 @@ fn parse_posting( posting.money_amount = Some(money.0); posting.money_currency = Some(money.1); } - Err(e) => { - // eprintln!("I fail here 260"); - match transaction_type { - TransactionType::Real | TransactionType::Periodic => return Err(e), - TransactionType::Automated => { - posting.amount_expr = Some(chars::get_line(tokenizer)); + Err(e) => match transaction_type { + TransactionType::Real | TransactionType::Periodic => return Err(e), + TransactionType::Automated => { + posting.amount_expr = Some(chars::get_line(tokenizer)); - tokenizer.line_index -= 1; - tokenizer.position -= 1; - } + tokenizer.line_index -= 1; + tokenizer.position -= 1; } - } + }, }, } chars::consume_whitespaces(tokenizer); From 635c6f1356047484d149af2e34c52dfb00043639 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 14 Feb 2021 22:30:04 +0100 Subject: [PATCH 03/12] New sequence for evaluating automated transactions #27 First balance the transactions. Then applied automated transactions. Then balance again. Not the most efficient but it works. First get it to work, then make it fast. --- src/lib/models/mod.rs | 179 +++++++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 72 deletions(-) diff --git a/src/lib/models/mod.rs b/src/lib/models/mod.rs index 3d88665..7e23f04 100644 --- a/src/lib/models/mod.rs +++ b/src/lib/models/mod.rs @@ -132,78 +132,6 @@ impl ParsedLedger { prices.append(&mut new_prices); } - // 5. Go over the transactions again and see if there is something we need to do with them - for automated in automated_transactions.iter() { - 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 p.amount.is_none() { - continue; - } - if filter_predicate(&vec![automated.description.clone()], p) { - for comment in t.comments.iter() { - p.to_owned().tags.append(&mut comment.get_tags()); - } - for auto_posting in automated.postings_iter() { - let account_alias = auto_posting.account.clone(); - match self.accounts.get(&account_alias) { - Ok(_) => {} // do nothing - Err(_) => { - self.accounts.insert(Account::from(account_alias.as_str())) - } - } - let account = self.accounts.get(&account_alias).unwrap(); - let money = match &auto_posting.money_currency { - None => None, - Some(alias) => { - if alias == "" { - Some(Money::from(( - p.amount.clone().unwrap().get_commodity().unwrap(), - p.amount.clone().unwrap().get_amount() - * auto_posting.money_amount.clone().unwrap(), - ))) - } else { - match self.commodities.get(&alias) { - Ok(_) => {} // do nothing - Err(_) => self - .commodities - .insert(Currency::from(alias.as_str())), - } - Some(Money::from(( - self.commodities.get(alias).unwrap().clone(), - auto_posting.money_amount.clone().unwrap(), - ))) - } - } - }; - let posting = Posting { - account: account.clone(), - amount: money, - balance: None, - cost: None, - kind: auto_posting.kind, - tags: vec![], - }; - println!("{:?}", posting); - match auto_posting.kind { - PostingType::Real => extra_postings.push(posting), - PostingType::Virtual => extra_virtual_postings.push(posting), - PostingType::VirtualMustBalance => { - extra_virtual_postings_balance.push(posting) - } - } - } - // todo!("Need to work on transaction automation"); - } - } - t.postings.append(&mut extra_postings); - t.virtual_postings.append(&mut extra_virtual_postings); - t.virtual_postings_balance - .append(&mut extra_virtual_postings_balance); - } - } // Now sort the transactions vector by date transactions.sort_by(|a, b| a.date.unwrap().cmp(&b.date.unwrap())); @@ -241,6 +169,113 @@ 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 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 p.amount.is_none() { + continue; + } + if filter_predicate(&vec![automated.description.clone()], p) { + for comment in t.comments.iter() { + p.to_owned().tags.append(&mut comment.get_tags()); + } + for auto_posting in automated.postings_iter() { + let account_alias = auto_posting.account.clone(); + match self.accounts.get(&account_alias) { + Ok(_) => {} // do nothing + Err(_) => { + self.accounts.insert(Account::from(account_alias.as_str())) + } + } + let account = self.accounts.get(&account_alias).unwrap(); + let money = match &auto_posting.money_currency { + None => None, + Some(alias) => { + if alias == "" { + Some(Money::from(( + p.amount.clone().unwrap().get_commodity().unwrap(), + p.amount.clone().unwrap().get_amount() + * auto_posting.money_amount.clone().unwrap(), + ))) + } else { + match self.commodities.get(&alias) { + Ok(_) => {} // do nothing + Err(_) => self + .commodities + .insert(Currency::from(alias.as_str())), + } + Some(Money::from(( + self.commodities.get(alias).unwrap().clone(), + auto_posting.money_amount.clone().unwrap(), + ))) + } + } + }; + let posting = Posting { + account: account.clone(), + amount: money, + balance: None, + cost: None, + kind: auto_posting.kind, + tags: vec![], + }; + // println!("{:?}", posting); + match auto_posting.kind { + PostingType::Real => extra_postings.push(posting), + PostingType::Virtual => extra_virtual_postings.push(posting), + PostingType::VirtualMustBalance => { + extra_virtual_postings_balance.push(posting) + } + } + } + // todo!("Need to work on transaction automation"); + } + } + t.postings.append(&mut extra_postings); + t.virtual_postings.append(&mut extra_virtual_postings); + t.virtual_postings_balance + .append(&mut extra_virtual_postings_balance); + } + } + // Populate balances + let mut balances: HashMap, Balance> = HashMap::new(); + for account in self.accounts.values() { + balances.insert(account.clone(), Balance::new()); + } + + // Balance the transactions + for t in transactions.iter_mut() { + let date = t.date.unwrap().clone(); + // output_balances(&balances); + let balance = match t.balance(&mut balances, no_checks) { + Ok(balance) => balance, + Err(e) => { + eprintln!("{}", t); + return Err(e.into()); + } + }; + if balance.len() == 2 { + let vec = balance.iter().map(|(_, x)| x.abs()).collect::>(); + + let commodity = vec[0].get_commodity().unwrap().clone(); + let price = Money::Money { + amount: vec[1].get_amount() / vec[0].get_amount(), + currency: vec[1].get_commodity().unwrap().clone(), + }; + + prices.push(Price { + date, + commodity, + price, + }); + } + } + } Ok(Ledger { accounts: self.accounts, commodities: self.commodities, From bd55d0b1b665c777bd92b79e7eb15a7495d4a2c6 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 14 Feb 2021 22:41:48 +0100 Subject: [PATCH 04/12] add a test for the newly created functionality #27 --- examples/automated_fail.ledger | 11 +++++++++++ tests/common.rs | 8 ++++++++ tests/test_commands.rs | 13 ++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 examples/automated_fail.ledger diff --git a/examples/automated_fail.ledger b/examples/automated_fail.ledger new file mode 100644 index 0000000..ad98088 --- /dev/null +++ b/examples/automated_fail.ledger @@ -0,0 +1,11 @@ +; This should fail because the added posting makes the transaction unbalanced + += flights + [Bugdet:Holiday] -1 + +2021-01-01 * Flights + ; :holiday: + Expenses:Flights 200 USD + Assets:Checking account + + diff --git a/tests/common.rs b/tests/common.rs index 01cfba3..0529d57 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -8,3 +8,11 @@ pub fn test_args(args: &[&str]) { let res = run_app(function_args.iter().map(|x| x.to_string()).collect()); assert!(res.is_ok()); } +pub fn test_err(args: &[&str]) { + let mut function_args: Vec<&str> = vec!["testing"]; + for arg in args { + function_args.push(arg); + } + let res = run_app(function_args.iter().map(|x| x.to_string()).collect()); + assert!(res.is_err()); +} diff --git a/tests/test_commands.rs b/tests/test_commands.rs index d425c51..b4e4a9b 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -1,5 +1,5 @@ use assert_cmd::Command; -use common::test_args; +use common::{test_args, test_err}; mod common; #[test] fn date_filters() { @@ -167,3 +167,14 @@ fn commodities_command() { test_args(args); } + +#[test] +/// If this fails it means that it created an extra posting +fn automated_fail() { + let args = &["reg", "-f", "examples/automated_fail.ledger"]; + let assert_1 = Command::cargo_bin("dinero").unwrap().args(args).assert(); + let output_err = String::from_utf8(assert_1.get_output().to_owned().stderr).unwrap(); + assert_eq!(output_err.lines().into_iter().count(), 5); + + test_err(args); +} From 0eb7ac922ac626e1e8d50505f7eda801b63fe3b0 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Tue, 16 Feb 2021 21:57:20 +0100 Subject: [PATCH 05/12] start adding payees in places --- README.md | 2 +- src/lib/models/mod.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52b08f3..4b4b242 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,4 @@ Currently supported are: Report filtering by account name and by date. -Run ```dinero --help``` for a list of available commands and options. +Run ```dinero --help``` for a list of available commands and options. \ No newline at end of file diff --git a/src/lib/models/mod.rs b/src/lib/models/mod.rs index 7e23f04..e4b2991 100644 --- a/src/lib/models/mod.rs +++ b/src/lib/models/mod.rs @@ -37,6 +37,7 @@ pub struct Ledger { pub(crate) commodities: List, pub(crate) transactions: Vec>, pub(crate) prices: Vec, + payees: List, } impl Ledger { @@ -46,6 +47,7 @@ impl Ledger { prices: vec![], transactions: vec![], commodities: List::::new(), + payees: List::::new(), } } } @@ -55,6 +57,7 @@ impl ParsedLedger { pub fn to_ledger(mut self, no_checks: bool) -> Result { let mut commodity_strs = HashSet::::new(); let mut account_strs = HashSet::::new(); + let mut payee_strs = HashSet::::new(); // // 1. Populate the directive lists @@ -281,6 +284,7 @@ impl ParsedLedger { commodities: self.commodities, transactions, prices, + payees: self.payees, }) } From 9e48a03fae2df237dc7c847a8740a5552f755e48 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Fri, 19 Feb 2021 06:17:57 +0100 Subject: [PATCH 06/12] Towards value expressions!!! #27 Implemented a grammar that is able to parse value expressions, although they are not yet fully evaluated. It is kind of working :) --- Cargo.toml | 2 + examples/automated.ledger | 11 + src/grammar/value_expression.pest | 65 ++++++ src/lib/mod.rs | 4 + src/lib/models/mod.rs | 12 +- src/lib/models/money.rs | 11 +- src/lib/parser/chars.rs | 25 ++ src/lib/parser/mod.rs | 1 + src/lib/parser/tokenizers/transaction.rs | 13 +- src/lib/parser/value_expr.rs | 279 +++++++++++++++++++++++ 10 files changed, 412 insertions(+), 11 deletions(-) create mode 100644 examples/automated.ledger create mode 100644 src/grammar/value_expression.pest create mode 100644 src/lib/parser/value_expr.rs diff --git a/Cargo.toml b/Cargo.toml index 9b3c812..991c76e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,5 @@ shellexpand = "2.1.0" two_timer = "2.2.0" assert_cmd = "1.0.3" terminal_size = "0.1.16" +pest = "2.0" +pest_derive = "2.0" diff --git a/examples/automated.ledger b/examples/automated.ledger new file mode 100644 index 0000000..867ee47 --- /dev/null +++ b/examples/automated.ledger @@ -0,0 +1,11 @@ +; This should fail because the added posting makes the transaction unbalanced + += Income:Salary + (Savings) 600 EUR + (Free spending) (abs(amount) - 600 EUR) + +2021-01-01 * Flights + Income:Salary -1000 EUR + Assets:Checking account + + diff --git a/src/grammar/value_expression.pest b/src/grammar/value_expression.pest new file mode 100644 index 0000000..e380f4f --- /dev/null +++ b/src/grammar/value_expression.pest @@ -0,0 +1,65 @@ +// Grammar specification for value expressions + +value_expr = {"(" ~ ws* ~ expr ~ ws* ~ ")"} +term = _{ variable | money | number } +money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) } +currency = { LETTER+ | ("\"" ~ (!"\"" ~ 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 } +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 ) * } +multiplicative_expr = { primary ~ ws* ~ ( mult ~ ws* ~ primary )* } +primary = { + ("(" ~ ws* ~ expr ~ ws* ~ ")") | + term | + (unary ~ ws* ~ primary) | + (unary_function ~ ws* ~ "(" ~ expr ~ ws* ~ ")") + } + +add = { "+" | "-" } +mult = { "*" | "/" } +and = {"&"} +or = {"|"} +unary = { "-" | "!"} +unary_function = { "abs" } + +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"} \ No newline at end of file diff --git a/src/lib/mod.rs b/src/lib/mod.rs index 43ea2a5..ea5a94f 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,3 +1,7 @@ +extern crate pest; +#[macro_use] +extern crate pest_derive; + pub mod commands; mod error; mod filter; diff --git a/src/lib/models/mod.rs b/src/lib/models/mod.rs index e4b2991..f010ee5 100644 --- a/src/lib/models/mod.rs +++ b/src/lib/models/mod.rs @@ -14,8 +14,8 @@ pub use transaction::{ Cleared, Posting, PostingType, Transaction, TransactionStatus, TransactionType, }; -use crate::parser::tokenizers; use crate::parser::ParsedLedger; +use crate::parser::{tokenizers, value_expr}; use crate::{filter::filter_predicate, models::transaction::Cost}; use crate::{Error, List}; use num::BigInt; @@ -180,9 +180,6 @@ impl ParsedLedger { let mut extra_virtual_postings = vec![]; let mut extra_virtual_postings_balance = vec![]; for p in t.postings_iter() { - if p.amount.is_none() { - continue; - } if filter_predicate(&vec![automated.description.clone()], p) { for comment in t.comments.iter() { p.to_owned().tags.append(&mut comment.get_tags()); @@ -197,7 +194,12 @@ impl ParsedLedger { } let account = self.accounts.get(&account_alias).unwrap(); let money = match &auto_posting.money_currency { - None => None, + None => Some(value_expr::eval_value_expression( + auto_posting.amount_expr.clone().unwrap().as_str(), + p, + t, + &mut self.commodities, + )), Some(alias) => { if alias == "" { Some(Money::from(( diff --git a/src/lib/models/money.rs b/src/lib/models/money.rs index 8d97360..1865389 100644 --- a/src/lib/models/money.rs +++ b/src/lib/models/money.rs @@ -1,6 +1,6 @@ use std::fmt; use std::fmt::{Display, Formatter}; -use std::ops::{Add, Mul, Neg}; +use std::ops::{Add, Mul, Neg, Sub}; use std::rc::Rc; use num; @@ -195,6 +195,15 @@ impl Add for Money { b1 + b2 } } +impl Sub for Money { + type Output = Balance; + + fn sub(self, rhs: Self) -> Self::Output { + let b1 = Balance::from(self); + let b2 = Balance::from(rhs); + b1 - b2 + } +} impl<'a> Neg for Money { type Output = Money; diff --git a/src/lib/parser/chars.rs b/src/lib/parser/chars.rs index 4f3d34e..93d6245 100644 --- a/src/lib/parser/chars.rs +++ b/src/lib/parser/chars.rs @@ -62,6 +62,31 @@ pub(super) fn consume_str<'a>( Ok(()) } +pub(super) fn get_value_expression(tokenizer: &mut Tokenizer) -> String { + let mut retval: Vec = Vec::new(); + let mut open = 0; + let mut close = 0; + + while let Some(c) = tokenizer.content.get(tokenizer.position) { + tokenizer.position += 1; + if *c == '(' { + open += 1; + } else if *c == ')' { + close += 1; + } else if *c == '\n' { + tokenizer.line_index += 1; + tokenizer.line_position = 0; + if open == close { + break; + } + } else if *c == ';' { + break; + } + retval.push(*c); + } + retval.iter().collect() +} + pub(super) fn get_line(tokenizer: &mut Tokenizer) -> String { let mut retval: Vec = Vec::new(); while let Some(c) = tokenizer.content.get(tokenizer.position) { diff --git a/src/lib/parser/mod.rs b/src/lib/parser/mod.rs index 23794e6..cce4c0f 100644 --- a/src/lib/parser/mod.rs +++ b/src/lib/parser/mod.rs @@ -20,6 +20,7 @@ use crate::{models, Error, List, ParserError}; mod chars; mod include; pub mod tokenizers; +pub mod value_expr; use tokenizers::{account, comment, commodity, payee, price, tag, transaction}; diff --git a/src/lib/parser/tokenizers/transaction.rs b/src/lib/parser/tokenizers/transaction.rs index 444acc8..7b0ea88 100644 --- a/src/lib/parser/tokenizers/transaction.rs +++ b/src/lib/parser/tokenizers/transaction.rs @@ -10,19 +10,19 @@ use num::BigInt; use regex::Regex; use std::str::FromStr; -pub(crate) fn parse<'a>(tokenizer: &'a mut Tokenizer) -> Result, Error> { +pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result, Error> { parse_generic(tokenizer, true) } -pub(crate) fn parse_automated_transaction<'a>( - tokenizer: &'a mut Tokenizer, +pub(crate) fn parse_automated_transaction( + tokenizer: &mut Tokenizer, ) -> Result, Error> { parse_generic(tokenizer, false) } /// Parses a transaction -pub(crate) fn parse_generic<'a>( - tokenizer: &'a mut Tokenizer, +pub(crate) fn parse_generic( + tokenizer: &mut Tokenizer, real: bool, ) -> Result, Error> { lazy_static! { @@ -232,6 +232,9 @@ fn parse_posting( // Amounts loop { match tokenizer.get_char() { + Some('(') => { // This is a value expression + posting.amount_expr = Some(chars::get_value_expression(tokenizer)); + }, Some('\n') => break, None => break, Some(';') => { diff --git a/src/lib/parser/value_expr.rs b/src/lib/parser/value_expr.rs new file mode 100644 index 0000000..71fcd27 --- /dev/null +++ b/src/lib/parser/value_expr.rs @@ -0,0 +1,279 @@ +use crate::models::{Currency, Money, Posting, Transaction}; +use crate::pest::Parser; +use crate::List; +use num::{abs, BigInt, BigRational}; +use std::rc::Rc; +use std::str::FromStr; + +#[derive(Parser)] +#[grammar = "grammar/value_expression.pest"] +pub struct ValueExpressionParser; + +pub fn eval_value_expression( + expression: &str, + posting: &Posting, + transaction: &Transaction, + commodities: &mut List, +) -> Money { + let parsed = ValueExpressionParser::parse(Rule::value_expr, expression) + .expect("unsuccessful parse") // unwrap the parse result + .next() + .unwrap() + .into_inner() + .next() + .unwrap(); + + // Build the abstract syntax tree + let root = build_ast_from_expr(parsed); + + match eval(&root, posting, transaction, commodities) { + EvalResult::Number(n) => posting.amount.clone().unwrap() * n, + EvalResult::Money(m) => m, + _ => panic!("Should be money"), + } +} + +#[derive(Clone)] +enum Node { + Amount, + Number(BigRational), + Money { + currency: String, + amount: BigRational, + }, + UnaryExpr { + op: Unary, + child: Box, + }, + BinaryExpr { + op: Binary, + lhs: Box, + rhs: Box, + }, +} + +enum EvalResult { + Number(BigRational), + Money(Money), + Boolean(bool), +} + +fn eval( + node: &Node, + posting: &Posting, + transaction: &Transaction, + commodities: &mut List, +) -> EvalResult { + match node { + Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()), + Node::Number(n) => EvalResult::Number(n.clone()), + Node::Money { currency, amount } => { + let cur = match commodities.get(¤cy) { + Ok(c) => c.clone(), + Err(_) => { + let c = Currency::from(currency.as_str()); + commodities.insert(c.clone()); + Rc::new(c) + } + }; + EvalResult::Money(Money::from((cur.clone(), amount.clone()))) + } + Node::UnaryExpr { op, child } => { + let res = eval(child, posting, transaction, commodities); + match op { + Unary::Not => EvalResult::Boolean(false), + Unary::Neg => match res { + EvalResult::Number(n) => EvalResult::Number(-n), + EvalResult::Money(money) => EvalResult::Money(-money), + EvalResult::Boolean(b) => EvalResult::Boolean(!b), + }, + Unary::Abs => match res { + EvalResult::Number(n) => EvalResult::Number(abs(n)), + EvalResult::Money(money) => EvalResult::Money(match money { + Money::Zero => Money::Zero, + Money::Money { amount, currency } => Money::from((currency, abs(amount))), + }), + EvalResult::Boolean(_b) => panic!("Can't do abs of boolean"), + }, + } + } + Node::BinaryExpr { op, lhs, rhs } => { + let left = eval(lhs, posting, transaction, commodities); + let right = eval(rhs, posting, transaction, commodities); + match op { + Binary::Add | Binary::Subtract => { + if let EvalResult::Number(lhs) = left { + if let EvalResult::Number(rhs) = right { + EvalResult::Number(match op { + Binary::Add => lhs + rhs, + Binary::Subtract => lhs - rhs, + _ => unreachable!(), + }) + } else { + panic!("Should be numbers") + } + } else if let EvalResult::Money(lhs) = left { + if let EvalResult::Money(rhs) = right { + EvalResult::Money(match op { + Binary::Add => (lhs + rhs).to_money().unwrap(), + Binary::Subtract => (lhs - rhs).to_money().unwrap(), + _ => unreachable!(), + }) + } else { + panic!("Should be money") + } + } else { + panic!("Should be money") + } + } + Binary::Mult | Binary::Div => { + if let EvalResult::Number(lhs) = left { + if let EvalResult::Number(rhs) = right { + EvalResult::Number(match op { + Binary::Mult => lhs * rhs, + Binary::Div => lhs / rhs, + _ => unreachable!(), + }) + } else { + panic!("Should be numbers") + } + } else { + panic!("Should be numbers") + } + } + Binary::Or | Binary::And => { + if let EvalResult::Boolean(lhs) = left { + if let EvalResult::Boolean(rhs) = right { + EvalResult::Boolean(match op { + Binary::Or => lhs | rhs, + Binary::And => lhs & rhs, + _ => unreachable!(), + }) + } else { + panic!("Should be booleans") + } + } else { + panic!("Should be booleans") + } + } + } + } + } +} + +#[derive(Clone)] +enum Unary { + Not, + Neg, + Abs, +} + +#[derive(Clone)] +enum Binary { + Add, + Subtract, + Mult, + Div, + Or, + And, +} + +#[derive(Clone)] +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::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); + match pair.next() { + None => lhs, + Some(x) => { + let op = match rule { + Rule::or_expr => Binary::Or, + Rule::and_expr => Binary::And, + _ => match x.as_str() { + "+" => Binary::Add, + "-" => Binary::Subtract, + "*" => Binary::Mult, + "/" => Binary::Div, + x => unreachable!("{}", x), + }, + }; + let rhspair = pair.next().unwrap(); + let rhs = build_ast_from_expr(rhspair); + parse_binary_expr(op, lhs, rhs) + } + } + } + Rule::primary => { + let mut inner = pair.into_inner(); + let first = inner.next().unwrap(); + match first.as_rule() { + Rule::unary_function => { + let op = match first.as_str() { + "abs" => Unary::Abs, + 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(); + match child.as_rule() { + Rule::number => Node::Money { + currency: money.next().unwrap().as_str().to_string(), + amount: parse_big_rational(child.as_str()), + }, + Rule::currency => Node::Money { + currency: child.as_str().to_string(), + amount: parse_big_rational(money.next().unwrap().as_str()), + }, + unknown => panic!("Unknown rule: {:?}", unknown), + } + } + unknown => panic!("Unknown rule: {:?}", unknown), + } + } + unknown => panic!("Unknown expr: {:?}", unknown), + } +} + +fn parse_binary_expr(operation: Binary, lhs: Node, rhs: Node) -> Node { + Node::BinaryExpr { + op: operation, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + } +} + +fn parse_unary_expr(operation: Unary, child: Node) -> Node { + Node::UnaryExpr { + op: operation, + child: Box::new(child), + } +} + +fn parse_big_rational(input: &str) -> BigRational { + let mut num = String::new(); + let mut den = "1".to_string(); + let mut decimal = false; + for c in input.chars() { + if c == '.' { + decimal = true + } else { + num.push(c); + if decimal { + den.push('0') + }; + } + } + BigRational::new( + BigInt::from_str(num.as_str()).unwrap(), + BigInt::from_str(den.as_str()).unwrap(), + ) +} From dcb5e16daeee9bbb51207d7bbf46fc3e859d0e5a Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Fri, 19 Feb 2021 11:26:33 +0100 Subject: [PATCH 07/12] add tests for the value expressions --- src/lib/parser/tokenizers/transaction.rs | 5 +++-- tests/test_commands.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/lib/parser/tokenizers/transaction.rs b/src/lib/parser/tokenizers/transaction.rs index 7b0ea88..66201be 100644 --- a/src/lib/parser/tokenizers/transaction.rs +++ b/src/lib/parser/tokenizers/transaction.rs @@ -232,9 +232,10 @@ fn parse_posting( // Amounts loop { match tokenizer.get_char() { - Some('(') => { // This is a value expression + Some('(') => { + // This is a value expression posting.amount_expr = Some(chars::get_value_expression(tokenizer)); - }, + } Some('\n') => break, None => break, Some(';') => { diff --git a/tests/test_commands.rs b/tests/test_commands.rs index fdf405b..a0d54d1 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -178,3 +178,14 @@ fn automated_fail() { test_err(args); } + +#[test] +/// If this fails it means that it created an extra posting +fn automated_value_expression() { + let args = &["reg", "-f", "examples/automated.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(), 4); + + test_args(args); +} From 04ea41ed33978f34edc90e1e46ac7e44a5ba2f91 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 20 Feb 2021 11:28:45 +0100 Subject: [PATCH 08/12] Improvements on value expressions #27 Now they work for usage in automated transactions. Had to modify the tests as well. --- examples/automated.ledger | 16 ++++++++++++---- src/lib/models/money.rs | 11 ++++++++++- src/lib/parser/chars.rs | 6 +++--- src/lib/parser/tokenizers/transaction.rs | 3 ++- src/lib/parser/value_expr.rs | 17 +++++++++++++++++ 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/examples/automated.ledger b/examples/automated.ledger index 867ee47..b9a6e9e 100644 --- a/examples/automated.ledger +++ b/examples/automated.ledger @@ -1,11 +1,19 @@ ; This should fail because the added posting makes the transaction unbalanced - +; Different formats for currencies used, different operations = Income:Salary (Savings) 600 EUR (Free spending) (abs(amount) - 600 EUR) - += Savings + (Savings:Risky investments) (amount * 0.10) + (Savings:Deposits) 0.40 + (Savings:Shares) (amount / 2) += Expenses:Rent + Expenses:Rent EUR 553.12 + Expenses:Utilities (amount - 553.12 EUR) + Expenses:Rent -1 2021-01-01 * Flights Income:Salary -1000 EUR Assets:Checking account - - +2021-01-05 * Rent + Expenses:Rent 705.43 EUR + Assets:Checking account diff --git a/src/lib/models/money.rs b/src/lib/models/money.rs index 1865389..27e8082 100644 --- a/src/lib/models/money.rs +++ b/src/lib/models/money.rs @@ -1,6 +1,6 @@ use std::fmt; use std::fmt::{Display, Formatter}; -use std::ops::{Add, Mul, Neg, Sub}; +use std::ops::{Add, Div, Mul, Neg, Sub}; use std::rc::Rc; use num; @@ -9,6 +9,7 @@ use num::{BigInt, Signed, Zero}; use crate::models::balance::Balance; use crate::models::{Currency, HasName}; +use num::traits::Inv; use std::str::FromStr; /// Money representation: an amount and a currency @@ -169,6 +170,14 @@ impl Mul for Money { } } +impl Div for Money { + type Output = Money; + + fn div(self, rhs: BigRational) -> Self::Output { + self * rhs.inv() + } +} + impl From<(Rc, BigRational)> for Money { fn from(cur_amount: (Rc, BigRational)) -> Self { let (currency, amount) = cur_amount; diff --git a/src/lib/parser/chars.rs b/src/lib/parser/chars.rs index 93d6245..2c44973 100644 --- a/src/lib/parser/chars.rs +++ b/src/lib/parser/chars.rs @@ -68,21 +68,21 @@ pub(super) fn get_value_expression(tokenizer: &mut Tokenizer) -> String { let mut close = 0; while let Some(c) = tokenizer.content.get(tokenizer.position) { - tokenizer.position += 1; if *c == '(' { open += 1; } else if *c == ')' { close += 1; } else if *c == '\n' { - tokenizer.line_index += 1; - tokenizer.line_position = 0; if open == close { break; } + tokenizer.line_index += 1; + tokenizer.line_position = 0; } else if *c == ';' { break; } retval.push(*c); + tokenizer.position += 1; } retval.iter().collect() } diff --git a/src/lib/parser/tokenizers/transaction.rs b/src/lib/parser/tokenizers/transaction.rs index 66201be..18d89a0 100644 --- a/src/lib/parser/tokenizers/transaction.rs +++ b/src/lib/parser/tokenizers/transaction.rs @@ -212,6 +212,7 @@ fn parse_posting( // println!("{} is a virtual account {:?}", account, posting_type) } } + let mut posting = Posting { account, money_amount: None, @@ -369,7 +370,7 @@ fn parse_amount(tokenizer: &mut Tokenizer) -> Result { match BigInt::from_str(num.as_str()) { Ok(n) => n, Err(_) => { - // eprintln!("I fail here 341."); + // eprintln!("I fail here 372."); //todo delete return Err(ParserError::UnexpectedInput(Some( "Wrong number format".to_string(), ))); diff --git a/src/lib/parser/value_expr.rs b/src/lib/parser/value_expr.rs index 71fcd27..0d33698 100644 --- a/src/lib/parser/value_expr.rs +++ b/src/lib/parser/value_expr.rs @@ -134,9 +134,25 @@ fn eval( Binary::Div => lhs / rhs, _ => unreachable!(), }) + } else if let EvalResult::Money(rhs) = right { + EvalResult::Money(match op { + Binary::Mult => rhs * lhs, // the other way around is not implemented + Binary::Div => panic!("Can't divide number by money"), + _ => unreachable!(), + }) } else { panic!("Should be numbers") } + } else if let EvalResult::Money(lhs) = left { + if let EvalResult::Number(rhs) = right { + EvalResult::Money(match op { + Binary::Mult => lhs * rhs, + Binary::Div => lhs / rhs, + _ => unreachable!(), + }) + } else { + panic!("rhs should be a number") + } } else { panic!("Should be numbers") } @@ -236,6 +252,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { unknown => panic!("Unknown rule: {:?}", unknown), } } + Rule::number => Node::Number(parse_big_rational(first.as_str())), unknown => panic!("Unknown rule: {:?}", unknown), } } From a3b8d2be262eda5a4638f27493115438d1835509 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 20 Feb 2021 11:41:06 +0100 Subject: [PATCH 09/12] Modify test\n\nthe test was badly written because the automated.ledger file changed, adding more registers #27 --- tests/test_commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_commands.rs b/tests/test_commands.rs index a0d54d1..3ad6a57 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -185,7 +185,7 @@ fn automated_value_expression() { let args = &["reg", "-f", "examples/automated.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(), 4); + assert_eq!(output.lines().into_iter().count(), 12); test_args(args); } From a5328dcfccc2b37e91f2c9a7a78350f8d681e9c4 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 20 Feb 2021 12:06:12 +0100 Subject: [PATCH 10/12] make function private --- src/lib/parser/tokenizers/transaction.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/parser/tokenizers/transaction.rs b/src/lib/parser/tokenizers/transaction.rs index 18d89a0..04a3e9e 100644 --- a/src/lib/parser/tokenizers/transaction.rs +++ b/src/lib/parser/tokenizers/transaction.rs @@ -21,10 +21,7 @@ pub(crate) fn parse_automated_transaction( } /// Parses a transaction -pub(crate) 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 From 0c5dd733ef7cdf8f42fb4ad49daab9efddc19591 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 20 Feb 2021 12:13:32 +0100 Subject: [PATCH 11/12] test commodities written commodity first then number --- examples/automated.ledger | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/automated.ledger b/examples/automated.ledger index b9a6e9e..8c971a2 100644 --- a/examples/automated.ledger +++ b/examples/automated.ledger @@ -2,7 +2,7 @@ ; Different formats for currencies used, different operations = Income:Salary (Savings) 600 EUR - (Free spending) (abs(amount) - 600 EUR) + (Free spending) (abs(amount) - EUR 600) = Savings (Savings:Risky investments) (amount * 0.10) (Savings:Deposits) 0.40 From d9a6c69e5fe0723e7f3492bc0d69b57ba84e084c Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 20 Feb 2021 12:28:53 +0100 Subject: [PATCH 12/12] add support for negative operator #27 --- examples/automated.ledger | 2 +- src/lib/parser/value_expr.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/automated.ledger b/examples/automated.ledger index 8c971a2..ba35512 100644 --- a/examples/automated.ledger +++ b/examples/automated.ledger @@ -10,7 +10,7 @@ = Expenses:Rent Expenses:Rent EUR 553.12 Expenses:Utilities (amount - 553.12 EUR) - Expenses:Rent -1 + Expenses:Rent (-amount) 2021-01-01 * Flights Income:Salary -1000 EUR Assets:Checking account diff --git a/src/lib/parser/value_expr.rs b/src/lib/parser/value_expr.rs index 0d33698..a6ac4d1 100644 --- a/src/lib/parser/value_expr.rs +++ b/src/lib/parser/value_expr.rs @@ -229,9 +229,10 @@ 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_function | Rule::unary => { let op = match first.as_str() { "abs" => Unary::Abs, + "-" => Unary::Neg, unknown => panic!("Unknown expr: {:?}", unknown), }; parse_unary_expr(op, build_ast_from_expr(inner.next().unwrap())) @@ -253,6 +254,7 @@ fn build_ast_from_expr(pair: pest::iterators::Pair) -> Node { } } Rule::number => Node::Number(parse_big_rational(first.as_str())), + unknown => panic!("Unknown rule: {:?}", unknown), } }