From 8cb8f91165182b615c32fbeebcaecc503865da2c Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 27 Feb 2021 22:22:28 +0100 Subject: [PATCH 01/10] do not show balance if it is zero --- .gitignore | 2 +- CHANGELOG.md | 3 +- src/app.rs | 12 +++---- src/commands/balance.rs | 5 +++ src/models/price.rs | 2 +- .../example_files}/automated.ledger | 0 .../example_files}/automated_fail.ledger | 0 {examples => tests/example_files}/demo.ledger | 0 .../example_files}/demo_bad.ledger | 0 .../example_files}/example_bad_ledgerrc | 0 .../example_files}/example_bad_ledgerrc2 | 0 .../example_files}/example_ledgerrc | 0 .../example_files}/include.ledger | 0 .../example_files}/quotes/bitcoin.dat | 0 .../example_files}/quotes/sp500.dat | 0 {examples => tests/example_files}/tags.ledger | 0 .../example_files}/virtual_postings.ledger | 0 tests/test_commands.rs | 36 +++++++++---------- tests/test_include.rs | 6 ++-- tests/test_tags.rs | 4 +-- 20 files changed, 38 insertions(+), 32 deletions(-) rename {examples => tests/example_files}/automated.ledger (100%) rename {examples => tests/example_files}/automated_fail.ledger (100%) rename {examples => tests/example_files}/demo.ledger (100%) rename {examples => tests/example_files}/demo_bad.ledger (100%) rename {examples => tests/example_files}/example_bad_ledgerrc (100%) rename {examples => tests/example_files}/example_bad_ledgerrc2 (100%) rename {examples => tests/example_files}/example_ledgerrc (100%) rename {examples => tests/example_files}/include.ledger (100%) rename {examples => tests/example_files}/quotes/bitcoin.dat (100%) rename {examples => tests/example_files}/quotes/sp500.dat (100%) rename {examples => tests/example_files}/tags.ledger (100%) rename {examples => tests/example_files}/virtual_postings.ledger (100%) diff --git a/.gitignore b/.gitignore index 370100e..fd2b400 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ Cargo.lock .idea .vscode -examples/personal.ledger \ No newline at end of file +personal.ledger diff --git a/CHANGELOG.md b/CHANGELOG.md index 177e093..5cd373b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog Changelog file for dinero-rs project, a command line application for managing finances. -## [0.14.0] +## [0.15.0] - +## [0.14.0] - 2021-02-27 ### Fixed - speed bump, from 7 seconds to 4 seconds in my personal ledger (still room to improve) - ability to add tags from automated transactions diff --git a/src/app.rs b/src/app.rs index 3bff06c..44155f0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -311,9 +311,9 @@ mod tests { "testing", "bal", "-f", - "examples/demo.ledger", + "tests/example_files/demo.ledger", "--init-file", - "examples/example_ledgerrc", + "tests/example_files/example_ledgerrc", "--real", ] .iter() @@ -325,14 +325,14 @@ mod tests { #[test] #[should_panic( - expected = "Bad config file \"examples/example_bad_ledgerrc\"\nThis line should be a comment but isn\'t, it is bad on purpose." + expected = "Bad config file \"tests/example_files/example_bad_ledgerrc\"\nThis line should be a comment but isn\'t, it is bad on purpose." )] fn bad_ledgerrc() { let args: Vec = vec![ "testing", "bal", "--init-file", - "examples/example_bad_ledgerrc", + "tests/example_files/example_bad_ledgerrc", ] .iter() .map(|x| x.to_string()) @@ -341,14 +341,14 @@ mod tests { } #[test] #[should_panic( - expected = "Bad config file \"examples/example_bad_ledgerrc2\"\n- This does not parse either. And it shouldn't." + expected = "Bad config file \"tests/example_files/example_bad_ledgerrc2\"\n- This does not parse either. And it shouldn't." )] fn other_bad_ledgerrc() { let args: Vec = vec![ "testing", "bal", "--init-file", - "examples/example_bad_ledgerrc2", + "tests/example_files/example_bad_ledgerrc2", ] .iter() .map(|x| x.to_string()) diff --git a/src/commands/balance.rs b/src/commands/balance.rs index e0ce5dc..e3d67ef 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -123,9 +123,14 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), let (account, bal) = &vec_balances[index]; if let Some(depth) = depth { if account.split(":").count() > depth { + index +=1; continue; } } + if bal.is_zero() { + index+=1; + continue; + } let mut first = true; for (_, money) in bal.balance.iter() { diff --git a/src/models/price.rs b/src/models/price.rs index b4b63f5..22f3e3f 100644 --- a/src/models/price.rs +++ b/src/models/price.rs @@ -263,7 +263,7 @@ mod tests { #[test] fn test_graph() { // Copy from balance command - let path = PathBuf::from("examples/demo.ledger"); + let path = PathBuf::from("tests/example_files/demo.ledger"); let mut tokenizer = Tokenizer::from(&path); let items = tokenizer.tokenize().unwrap(); let ledger = items.to_ledger(false).unwrap(); diff --git a/examples/automated.ledger b/tests/example_files/automated.ledger similarity index 100% rename from examples/automated.ledger rename to tests/example_files/automated.ledger diff --git a/examples/automated_fail.ledger b/tests/example_files/automated_fail.ledger similarity index 100% rename from examples/automated_fail.ledger rename to tests/example_files/automated_fail.ledger diff --git a/examples/demo.ledger b/tests/example_files/demo.ledger similarity index 100% rename from examples/demo.ledger rename to tests/example_files/demo.ledger diff --git a/examples/demo_bad.ledger b/tests/example_files/demo_bad.ledger similarity index 100% rename from examples/demo_bad.ledger rename to tests/example_files/demo_bad.ledger diff --git a/examples/example_bad_ledgerrc b/tests/example_files/example_bad_ledgerrc similarity index 100% rename from examples/example_bad_ledgerrc rename to tests/example_files/example_bad_ledgerrc diff --git a/examples/example_bad_ledgerrc2 b/tests/example_files/example_bad_ledgerrc2 similarity index 100% rename from examples/example_bad_ledgerrc2 rename to tests/example_files/example_bad_ledgerrc2 diff --git a/examples/example_ledgerrc b/tests/example_files/example_ledgerrc similarity index 100% rename from examples/example_ledgerrc rename to tests/example_files/example_ledgerrc diff --git a/examples/include.ledger b/tests/example_files/include.ledger similarity index 100% rename from examples/include.ledger rename to tests/example_files/include.ledger diff --git a/examples/quotes/bitcoin.dat b/tests/example_files/quotes/bitcoin.dat similarity index 100% rename from examples/quotes/bitcoin.dat rename to tests/example_files/quotes/bitcoin.dat diff --git a/examples/quotes/sp500.dat b/tests/example_files/quotes/sp500.dat similarity index 100% rename from examples/quotes/sp500.dat rename to tests/example_files/quotes/sp500.dat diff --git a/examples/tags.ledger b/tests/example_files/tags.ledger similarity index 100% rename from examples/tags.ledger rename to tests/example_files/tags.ledger diff --git a/examples/virtual_postings.ledger b/tests/example_files/virtual_postings.ledger similarity index 100% rename from examples/virtual_postings.ledger rename to tests/example_files/virtual_postings.ledger diff --git a/tests/test_commands.rs b/tests/test_commands.rs index b112b23..7048fc3 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -3,7 +3,7 @@ use common::{test_args, test_err}; mod common; #[test] fn date_filters() { - let args1 = &["bal", "-f", "examples/demo.ledger"]; + let args1 = &["bal", "-f", "tests/example_files/demo.ledger"]; let assert_1 = Command::cargo_bin("dinero").unwrap().args(args1).assert(); let mut output = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); assert_eq!(output.lines().into_iter().count(), 17); @@ -11,7 +11,7 @@ fn date_filters() { let args2 = &[ "bal", "-f", - "examples/demo.ledger", + "tests/example_files/demo.ledger", "-e", "2021-01-17", "-b", @@ -33,7 +33,7 @@ fn exchange() { let args = &[ "bal", "-f", - "examples/demo.ledger", + "tests/example_files/demo.ledger", "-X", "EUR", "--force-color", @@ -54,7 +54,7 @@ fn commodity_alias() { let mut outputs = Vec::new(); let aliases = vec!["EUR", "eur"]; for alias in aliases { - let args = &["bal", "-f", "examples/demo.ledger", "-X", alias]; + let args = &["bal", "-f", "tests/example_files/demo.ledger", "-X", alias]; let assert = Command::cargo_bin("dinero").unwrap().args(args).assert(); outputs.push(String::from_utf8(assert.get_output().to_owned().stdout).unwrap()); test_args(args); @@ -65,7 +65,7 @@ fn commodity_alias() { #[test] /// Check that the register report is showing virtual postings fn virtual_postings() { - let args = &["reg", "-f", "examples/virtual_postings.ledger"]; + let args = &["reg", "-f", "tests/example_files/virtual_postings.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(), 7); @@ -75,7 +75,7 @@ fn virtual_postings() { #[test] /// Check that the virtual postings are being filtered out fn real_filter() { - let args = &["reg", "-f", "examples/virtual_postings.ledger", "--real"]; + let args = &["reg", "-f", "tests/example_files/virtual_postings.ledger", "--real"]; 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); @@ -86,7 +86,7 @@ fn real_filter() { #[test] /// Check that the tag filter works fn tag_filter() { - let args = &["bal", "-f", "examples/demo.ledger", "--flat", "%fruit"]; + let args = &["bal", "-f", "tests/example_files/demo.ledger", "--flat", "%fruit"]; 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); @@ -97,7 +97,7 @@ fn tag_filter() { #[test] /// Check that the tag filter works fn account_filter() { - let args = &["bal", "-f", "examples/demo.ledger", "--flat", "travel"]; + let args = &["bal", "-f", "tests/example_files/demo.ledger", "--flat", "travel"]; 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(), 1); @@ -108,7 +108,7 @@ fn account_filter() { #[test] /// Check the accounts command fn accounts_command() { - let args = &["accounts", "-f", "examples/demo.ledger"]; + let args = &["accounts", "-f", "tests/example_files/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(), 7); @@ -119,7 +119,7 @@ fn accounts_command() { #[test] /// Check the check command fn check_command() { - let args = &["check", "-f", "examples/demo.ledger"]; + let args = &["check", "-f", "tests/example_files/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(), 1); @@ -130,7 +130,7 @@ fn check_command() { #[test] /// Check the check command fn check_command_bad_file() { - let args = &["check", "-f", "examples/demo_bad.ledger"]; + let args = &["check", "-f", "tests/example_files/demo_bad.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(); let output_err = String::from_utf8(assert_1.get_output().to_owned().stderr).unwrap(); @@ -142,14 +142,14 @@ fn check_command_bad_file() { #[should_panic] /// Check the check command fn test_command_bad_file() { - let args = &["check", "-f", "examples/demo_bad.ledger"]; + let args = &["check", "-f", "tests/example_files/demo_bad.ledger"]; test_args(args); } #[test] /// Check the prices command fn prices_command() { - let args = &["prices", "-f", "examples/demo.ledger"]; + let args = &["prices", "-f", "tests/example_files/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(), 7); @@ -160,7 +160,7 @@ fn prices_command() { #[test] /// Check the payees command fn payees_command() { - let args = &["payees", "-f", "examples/demo.ledger"]; + let args = &["payees", "-f", "tests/example_files/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!( @@ -175,7 +175,7 @@ fn payees_command() { #[test] /// Check the commodities command fn commodities_command() { - let args = &["commodities", "-f", "examples/demo.ledger"]; + let args = &["commodities", "-f", "tests/example_files/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(); @@ -186,7 +186,7 @@ fn commodities_command() { #[test] /// If this fails it means that it created an extra posting fn automated_fail() { - let args = &["reg", "-f", "examples/automated_fail.ledger"]; + let args = &["reg", "-f", "tests/example_files/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); @@ -196,7 +196,7 @@ fn automated_fail() { #[test] fn automated_value_expression() { - let args = &["reg", "-f", "examples/automated.ledger"]; + let args = &["reg", "-f", "tests/example_files/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(), 11); @@ -206,7 +206,7 @@ fn automated_value_expression() { #[test] fn automated_add_tag() { - let args = &["reg", "-f", "examples/automated.ledger", "%yummy"]; + let args = &["reg", "-f", "tests/example_files/automated.ledger", "%yummy"]; 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(), 2); diff --git a/tests/test_include.rs b/tests/test_include.rs index a31f247..1950fef 100644 --- a/tests/test_include.rs +++ b/tests/test_include.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; #[test] fn test_include() { - let p1 = PathBuf::from("examples/include.ledger".to_string()); + let p1 = PathBuf::from("tests/example_files/include.ledger".to_string()); let mut tokenizer: Tokenizer = Tokenizer::from(&p1); let res = tokenizer.tokenize(); assert!(res.is_ok()); @@ -13,7 +13,7 @@ fn test_include() { #[test] fn test_build_ledger_from_demo() { - let p1 = PathBuf::from("examples/demo.ledger".to_string()); + let p1 = PathBuf::from("tests/example_files/demo.ledger".to_string()); let mut tokenizer: Tokenizer = Tokenizer::from(&p1); let items = tokenizer.tokenize().unwrap(); let ledger = items.to_ledger(false); @@ -44,7 +44,7 @@ fn test_fail() { fn include_glob() { let assert_1 = Command::cargo_bin("dinero") .unwrap() - .args(&["prices", "-f", "examples/include.ledger"]) + .args(&["prices", "-f", "tests/example_files/include.ledger"]) .assert(); let output = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); assert!(output.lines().into_iter().count() > 100); diff --git a/tests/test_tags.rs b/tests/test_tags.rs index 2aca16f..9986083 100644 --- a/tests/test_tags.rs +++ b/tests/test_tags.rs @@ -4,13 +4,13 @@ mod common; #[test] /// Check the search by tag command fn tags() { - let args1 = &["reg", "-f", "examples/tags.ledger", "%healthy"]; + let args1 = &["reg", "-f", "tests/example_files/tags.ledger", "%healthy"]; let assert_1 = Command::cargo_bin("dinero").unwrap().args(args1).assert(); let output1 = String::from_utf8(assert_1.get_output().to_owned().stdout).unwrap(); assert_eq!(output1.lines().into_iter().count(), 1); test_args(args1); - let args2 = &["reg", "-f", "examples/tags.ledger", "%shopping"]; + let args2 = &["reg", "-f", "tests/example_files/tags.ledger", "%shopping"]; let assert_2 = Command::cargo_bin("dinero").unwrap().args(args2).assert(); let output2 = String::from_utf8(assert_2.get_output().to_owned().stdout).unwrap(); assert_eq!(output2.lines().into_iter().count(), 2); From db6b86d207c71706c58f9dd8fb6cf27b7d5f8385 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 27 Feb 2021 22:54:26 +0100 Subject: [PATCH 02/10] fix #47 --- src/commands/balance.rs | 4 +-- src/grammar/grammar.pest | 7 ++-- src/parser/tokenizers/commodity.rs | 52 ++++++------------------------ tests/test_commands.rs | 30 ++++++++++++++--- 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/src/commands/balance.rs b/src/commands/balance.rs index e3d67ef..8733c84 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -123,12 +123,12 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), let (account, bal) = &vec_balances[index]; if let Some(depth) = depth { if account.split(":").count() > depth { - index +=1; + index += 1; continue; } } if bal.is_zero() { - index+=1; + index += 1; continue; } diff --git a/src/grammar/grammar.pest b/src/grammar/grammar.pest index 34370fc..ec23cad 100644 --- a/src/grammar/grammar.pest +++ b/src/grammar/grammar.pest @@ -4,8 +4,9 @@ // // Directives -directive = { ( price ) ~ end } +directive = { price | commodity } price = {"P" ~ ws* ~ date ~ (ws ~ time)? ~ ws+ ~ currency ~ ws* ~number ~ ws* ~ currency ~ws* ~ comment? ~ end} +commodity = { "commodity" ~ ws+ ~ currency ~ ws* ~ comment? ~ end} transaction = {date ~ (ws ~ time)? ~ ws* ~ status? ~ ws* ~ status? ~ ws* ~ description ~ ws* ~ ("|" ~ ws*~payee)? ~ws* ~ comment? ~ end } code = { "(" ~ string ~ ")" } @@ -56,8 +57,8 @@ string = { ("\"" ~ (("\\\"") | (!"\"" ~ ANY))* ~ "\"") | ("'" ~ (("\\'") | (!"'" ~ ANY))* ~ "'") } -reserved = _{"+" | "*" | "/" | "\\" | "|" | "%" | "&" | "<" | ">" | ":" | "?" | "(" | ")" | ";"} -unquoted = { !reserved ~ !"=" ~ !"-" ~ +reserved = _{"+" | "*" | "/" | "\\" | "|" | "%" | "<" | ">" | ":" | "?" | "(" | ")" | ";"} +unquoted = { !reserved ~ !"=" ~ !"-" ~ !"&" ~ (!reserved ~ !SEPARATOR ~ ANY)+ } variable = { "account" | diff --git a/src/parser/tokenizers/commodity.rs b/src/parser/tokenizers/commodity.rs index 9315d33..a9c2228 100644 --- a/src/parser/tokenizers/commodity.rs +++ b/src/parser/tokenizers/commodity.rs @@ -1,3 +1,4 @@ +use super::super::{GrammarParser, Rule}; use std::collections::HashSet; use lazy_static::lazy_static; @@ -6,59 +7,26 @@ use regex::Regex; use crate::models::{Comment, Currency, Origin}; use crate::parser::chars::LineType; use crate::parser::tokenizers::comment; +use crate::parser::utils::parse_string; use crate::parser::{chars, Tokenizer}; use crate::ParserError; +use pest::Parser; pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result { - lazy_static! { - static ref RE: Regex = Regex::new(format!("{}{}{}", - r"(commodity) +" , // directive commodity - r"(.*)" , // description - r"( ;.*)?" , // note - ).as_str()).unwrap(); - } let mystr = chars::get_line(tokenizer); - let caps = RE.captures(mystr.as_str()).unwrap(); + let mut parsed = GrammarParser::parse(Rule::commodity, mystr.as_str()) + .expect("Could not parse commodity!") // unwrap the parse result + .next() + .unwrap() + .into_inner(); - let mut name = String::new(); - let mut detected: bool = false; + let name = parse_string(parsed.next().unwrap()); let mut note: Option = None; let mut format: Option = None; let mut comments: Vec = vec![]; let mut default = false; let mut aliases = HashSet::new(); - for (i, cap) in caps.iter().enumerate() { - match cap { - Some(m) => { - match i { - 1 => - // commodity - { - detected = true; - } - 2 => - // description - { - name = m.as_str().to_string() - } - 3 => - // note - { - note = Some(m.as_str().to_string()) - } - _ => (), - } - } - None => (), - } - } - - if !detected { - return Err(ParserError::UnexpectedInput(Some( - "Commodity expected. Not found.".to_string(), - ))); - } while let LineType::Indented = chars::consume_whitespaces_and_lines(tokenizer) { match tokenizer.get_char().unwrap() { ';' => comments.push(comment::parse(tokenizer)), @@ -81,7 +49,7 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result } let currency = Currency { - name, + name: name.trim().to_string(), origin: Origin::FromDirective, note, aliases, diff --git a/tests/test_commands.rs b/tests/test_commands.rs index 7048fc3..1422126 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -75,7 +75,12 @@ fn virtual_postings() { #[test] /// Check that the virtual postings are being filtered out fn real_filter() { - let args = &["reg", "-f", "tests/example_files/virtual_postings.ledger", "--real"]; + let args = &[ + "reg", + "-f", + "tests/example_files/virtual_postings.ledger", + "--real", + ]; 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); @@ -86,7 +91,13 @@ fn real_filter() { #[test] /// Check that the tag filter works fn tag_filter() { - let args = &["bal", "-f", "tests/example_files/demo.ledger", "--flat", "%fruit"]; + let args = &[ + "bal", + "-f", + "tests/example_files/demo.ledger", + "--flat", + "%fruit", + ]; 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); @@ -97,7 +108,13 @@ fn tag_filter() { #[test] /// Check that the tag filter works fn account_filter() { - let args = &["bal", "-f", "tests/example_files/demo.ledger", "--flat", "travel"]; + let args = &[ + "bal", + "-f", + "tests/example_files/demo.ledger", + "--flat", + "travel", + ]; 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(), 1); @@ -206,7 +223,12 @@ fn automated_value_expression() { #[test] fn automated_add_tag() { - let args = &["reg", "-f", "tests/example_files/automated.ledger", "%yummy"]; + let args = &[ + "reg", + "-f", + "tests/example_files/automated.ledger", + "%yummy", + ]; 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(), 2); From 2fab10174d500f44da518d14a0e7ccfc7ec8185e Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 27 Feb 2021 23:20:32 +0100 Subject: [PATCH 03/10] correct transaction grammar --- CHANGELOG.md | 2 ++ src/filter.rs | 2 +- src/grammar/grammar.pest | 10 +++++- src/parser/tokenizers/commodity.rs | 3 -- src/parser/tokenizers/transaction.rs | 49 +++++++++++++++------------- 5 files changed, 39 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd373b..02d4d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Changelog file for dinero-rs project, a command line application for managing finances. ## [0.15.0] - +### Added +- complete transaction grammar ## [0.14.0] - 2021-02-27 ### Fixed - speed bump, from 7 seconds to 4 seconds in my personal ledger (still room to improve) diff --git a/src/filter.rs b/src/filter.rs index 366971b..978a645 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,5 +1,5 @@ use crate::models::{Currency, Posting, PostingType, Transaction}; -use crate::parser::value_expr::{eval, eval_expression, EvalResult, Node}; +use crate::parser::value_expr::{eval, EvalResult, Node}; use crate::{CommonOpts, Error, List}; use colored::Colorize; use regex::Regex; diff --git a/src/grammar/grammar.pest b/src/grammar/grammar.pest index ec23cad..22d4594 100644 --- a/src/grammar/grammar.pest +++ b/src/grammar/grammar.pest @@ -7,7 +7,15 @@ directive = { price | commodity } price = {"P" ~ ws* ~ date ~ (ws ~ time)? ~ ws+ ~ currency ~ ws* ~number ~ ws* ~ currency ~ws* ~ comment? ~ end} commodity = { "commodity" ~ ws+ ~ currency ~ ws* ~ comment? ~ end} -transaction = {date ~ (ws ~ time)? ~ ws* ~ status? ~ ws* ~ status? ~ ws* ~ description ~ ws* ~ ("|" ~ ws*~payee)? ~ws* ~ comment? ~ end } +transaction = { + date ~ (ws ~ time)? ~ // date + ("=" ~ date ~ (ws ~ time)?)? ~ // effective_date + ws+ ~ status? ~ // status + ws* ~ code? // code + ~ ws* ~ description // description + ~ ws* ~ ("|" ~ ws*~payee)? // payee + ~ws* ~ comment? // comment + ~ end } code = { "(" ~ string ~ ")" } status = { "*"| "!" } diff --git a/src/parser/tokenizers/commodity.rs b/src/parser/tokenizers/commodity.rs index a9c2228..bdf43ff 100644 --- a/src/parser/tokenizers/commodity.rs +++ b/src/parser/tokenizers/commodity.rs @@ -1,9 +1,6 @@ use super::super::{GrammarParser, Rule}; use std::collections::HashSet; -use lazy_static::lazy_static; -use regex::Regex; - use crate::models::{Comment, Currency, Origin}; use crate::parser::chars::LineType; use crate::parser::tokenizers::comment; diff --git a/src/parser/tokenizers/transaction.rs b/src/parser/tokenizers/transaction.rs index 2d9e8b4..3360f1b 100644 --- a/src/parser/tokenizers/transaction.rs +++ b/src/parser/tokenizers/transaction.rs @@ -20,7 +20,7 @@ pub(crate) fn parse(tokenizer: &mut Tokenizer) -> Result fn parse_with_grammar(tokenizer: &mut Tokenizer) -> Result, Error> { let mystr = chars::get_line(tokenizer); let mut parsed = GrammarParser::parse(Rule::transaction, mystr.as_str()) - .expect("Could not parse price!") // unwrap the parse result + .expect("Could not parse transaction!") // unwrap the parse result .next() .unwrap() .into_inner(); @@ -28,29 +28,34 @@ fn parse_with_grammar(tokenizer: &mut Tokenizer) -> Result::new(TransactionType::Real); transaction.date = Some(parse_date(parsed.next().unwrap())); let mut next_item = parsed.next().unwrap(); - while next_item.as_rule() != Rule::description { + if next_item.as_rule() == Rule::time { + next_item = parsed.next().unwrap() + } + if next_item.as_rule() == Rule::date { + transaction.effective_date = Some(parse_date(next_item)); + next_item = parsed.next().unwrap() + } + if next_item.as_rule() == Rule::time { + next_item = parsed.next().unwrap() + } + if next_item.as_rule() == Rule::status { + next_item = parsed.next().unwrap() + } + if next_item.as_rule() == Rule::code { + next_item = parsed.next().unwrap() + } + if next_item.as_rule() == Rule::description { + transaction.description = parse_string(next_item).trim().to_string(); next_item = parsed.next().unwrap(); } - transaction.description = parse_string(next_item).trim().to_string(); - match parsed.next() { - None => (), // do nothing, we're done - Some(x) => { - match x.as_rule() { - Rule::payee => { - transaction.payee = Some(parse_string(x).trim().to_string()); - let comment = parsed.next(); - if comment.is_some() { - transaction.comments.push(Comment { - comment: parse_string(comment.unwrap()), - }); - } - } - Rule::comment => transaction.comments.push(Comment { - comment: parse_string(x), - }), - _ => (), // Do nothing (it means it reached Rule::end) - } - } + if next_item.as_rule() == Rule::payee { + transaction.payee = Some(parse_string(next_item).trim().to_string()); + next_item = parsed.next().unwrap(); + } + if next_item.as_rule() == Rule::comment { + transaction.comments.push(Comment { + comment: parse_string(next_item), + }); } if transaction.payee.is_none() { From 9291fb3b6cbbc532506271267ecb8ddb29510d8a Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 27 Feb 2021 23:31:44 +0100 Subject: [PATCH 04/10] a comparison script --- scripts/compare.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 scripts/compare.sh diff --git a/scripts/compare.sh b/scripts/compare.sh new file mode 100755 index 0000000..2f4b3ee --- /dev/null +++ b/scripts/compare.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +cd $DIR + +ledger commodities | sort > commodities_ledger.txt & +dinero commodities | sort > commodities_dinero.txt & + +ledger payees | sort > payees_ledger.txt & +dinero payees | sort > payees_dinero.txt & + +ledger bal stockplan -X eur > bal_stockplan_ledger.txt & +dinero bal stockplan -X eur > bal_stockplan_dinero.txt & + From 50c35ce025c7cfaf635f12a8a05d6e1b9e56581c Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sat, 27 Feb 2021 23:45:40 +0100 Subject: [PATCH 05/10] do not show total when there is only one balance --- src/commands/balance.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/balance.rs b/src/commands/balance.rs index 8733c84..ce35f68 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -119,6 +119,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), vec_balances.sort_by(|a, b| a.0.cmp(b.0)); let num_bal = vec_balances.len(); let mut index = 0; + let mut showed_balances = 0; while index < num_bal { let (account, bal) = &vec_balances[index]; if let Some(depth) = depth { @@ -131,6 +132,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), index += 1; continue; } + showed_balances += 1; let mut first = true; for (_, money) in bal.balance.iter() { @@ -191,7 +193,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(), } // Print the total - if show_total & (vec_balances.len() > 1) { + if show_total & (showed_balances > 1) { // Calculate it let mut total_balance = balances .iter() From 91a41860d3b946e06533c1723a8f4ef637746b12 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 28 Feb 2021 00:41:33 +0100 Subject: [PATCH 06/10] write test that reproduces the bug #44 --- src/models/mod.rs | 9 +++++++ tests/test_exchange.rs | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/test_exchange.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index 8cf71f0..b57c571 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -42,6 +42,15 @@ pub struct Ledger { pub(crate) payees: List, } +impl Ledger { + pub fn get_commodities(&self) -> &List { + &self.commodities + } + pub fn get_prices(&self) -> &Vec { + &self.prices + } +} + 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_exchange.rs b/tests/test_exchange.rs new file mode 100644 index 0000000..7fde320 --- /dev/null +++ b/tests/test_exchange.rs @@ -0,0 +1,55 @@ +use chrono::Utc; +use dinero::models::conversion; +use dinero::parser::Tokenizer; +use num::traits::Inv; +use num::{BigInt, BigRational}; + +#[test] +fn exchange() { + let mut tokenizer: Tokenizer = Tokenizer::from( + "2020-01-01 * ACME, Inc. + Assets:Shares 1 ACME @ 1000.00 USD + Assets:Bank:Checking account +2021-01-01 * ACME, Inc. + Assets:Shares 1 ACME @ 1000.00 EUR + Assets:Bank:Checking account + +P 2020-07-01 EUR 1.5 USD + +; I have 2 ACME Shares +; worth 2000 EUR +; worth 3000 USD because the last exchange rate was 1.5 +; in terms of nodes there should be +; 2021-01-01 ACME +; 2021-01-01 EUR +; 2020-07-01 EUR +; 2020-07-01 USD +; NOTHING for 2020-01-01 +; + " + .to_string(), + ); + let items = tokenizer.tokenize().unwrap(); + let ledger = items.to_ledger(false).unwrap(); + let eur = ledger.get_commodities().get("eur").unwrap(); + let usd = ledger.get_commodities().get("usd").unwrap(); + let acme = ledger.get_commodities().get("acme").unwrap(); + let multipliers_acme = conversion( + acme.clone(), + Utc::now().naive_local().date(), + ledger.get_prices(), + ); + + let to_eur = multipliers_acme.get(eur).unwrap(); + let to_usd = multipliers_acme.get(usd).unwrap(); + assert_eq!( + to_eur, + &BigRational::from_integer(BigInt::from(1000)).inv(), + "1 ACME = 1000 EUR" + ); + assert_eq!( + to_usd, + &BigRational::from_integer(BigInt::from(1500)).inv(), + "1 ACME = 1500 USD" + ); +} From 88ed6f83b2b3002dcfcb018b4f6e74c0d7e88640 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 28 Feb 2021 01:32:36 +0100 Subject: [PATCH 07/10] add the missing nodes, but the behavior is inconsistent --- src/models/price.rs | 70 +++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/models/price.rs b/src/models/price.rs index 22f3e3f..6395611 100644 --- a/src/models/price.rs +++ b/src/models/price.rs @@ -3,7 +3,7 @@ use chrono::{Duration, NaiveDate}; use num::rational::BigRational; use num::BigInt; use std::cmp::Ordering; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt; use std::fmt::{Display, Formatter}; use std::rc::Rc; @@ -62,26 +62,31 @@ pub fn conversion( // Initialize distances for node in graph.nodes.iter() { + // println!("{} {} ", node.currency.get_name(), node.date); if node.currency == currency { distances.insert(node.clone(), Some(date - node.date)); + // distances.insert(node.clone(), Some(date - date)); + // println!("{}", date - node.date); } else { distances.insert(node.clone(), None); + // println!("None"); } queue.push(node.clone()); } while !queue.is_empty() { queue.sort_by(|a, b| cmp(distances.get(b).unwrap(), distances.get(a).unwrap())); let v = queue.pop().unwrap(); - if distances.get(v.as_ref()).unwrap().is_none() { break; } + let current_path = if let Some(path) = paths.get(v.as_ref()) { path.clone() } else { Vec::new() }; for (u, e) in graph.get_neighbours(v.as_ref()).iter() { + // println!("Neighbour: {} {}", u.currency.get_name(), u.date); let alt = distances.get(v.as_ref()).unwrap().unwrap() + e.length(); let distance = distances.get(u.as_ref()).unwrap(); let mut update = distance.is_none(); @@ -103,12 +108,17 @@ pub fn conversion( let mut mult = BigRational::new(BigInt::from(1), BigInt::from(1)); let mut currency = k.currency.clone(); for edge in v.iter().rev() { - if currency == edge.from.currency { - mult *= edge.price.get_price().get_amount(); - currency = edge.to.currency.clone(); - } else { - mult /= edge.price.get_price().get_amount(); - currency = edge.from.currency.clone(); + match edge.as_ref().price.as_ref() { + None => (), // do nothing, multiply by one and keep the same currency + Some(price) => { + if currency == edge.from.currency { + mult *= price.get_price().get_amount(); + currency = edge.to.currency.clone(); + } else { + mult /= price.get_price().get_amount(); + currency = edge.from.currency.clone(); + } + } } } multipliers.insert(k.currency.clone(), mult); @@ -124,7 +134,7 @@ pub struct Node { #[derive(Debug, Clone)] pub struct Edge { - price: Price, + price: Option, from: Rc, to: Rc, } @@ -150,8 +160,8 @@ impl Graph { fn from_prices(prices: &Vec, source: Node) -> Self { let mut nodes = HashMap::new(); let mut edges = Vec::new(); - let mut currency_dates = HashMap::new(); - currency_dates.insert(source.currency.clone(), source.date); + let mut currency_dates = HashSet::new(); + currency_dates.insert((source.currency.clone(), source.date)); // Remove redundant prices and create the nodes let mut prices_nodup = HashMap::new(); for p in prices.iter() { @@ -176,42 +186,52 @@ impl Graph { } let c_vec = vec![commodities.0.clone(), commodities.1.clone()]; for c in c_vec { - match currency_dates.get(c.as_ref()) { - Some(v) => { - if v < &p.date { - currency_dates.insert(c.clone(), p.date); - } - } - None => { - currency_dates.insert(c.clone(), p.date); - } - } + currency_dates.insert((c.clone(), p.date)); } } // Create the nodes for (c, d) in currency_dates.iter() { nodes.insert( - c.clone(), + (c.clone(), d.clone()), Rc::new(Node { currency: c.clone(), date: d.clone(), }), ); } + + // Edges from the prices for (_, p) in prices_nodup.iter() { - let from = nodes.get(p.commodity.as_ref()).unwrap().clone(); + let from = nodes + .get(&(p.commodity.clone(), p.date.clone())) + .unwrap() + .clone(); let to = nodes - .get(p.price.get_commodity().unwrap().as_ref()) + .get(&(p.price.get_commodity().unwrap(), p.date.clone())) .unwrap() .clone(); edges.push(Rc::new(Edge { - price: p.clone(), + price: Some(p.clone()), from: from.clone(), to: to.clone(), })); } + let vec_node: Vec> = nodes.iter().map(|x| x.1.clone()).collect(); + let n = vec_node.len(); + for i in 0..n { + for j in i..n { + if vec_node[i].currency == vec_node[j].currency { + edges.push(Rc::new(Edge { + price: None, + from: vec_node[i].clone(), + to: vec_node[j].clone(), + })) + } + } + } + Graph { nodes: nodes.iter().map(|x| x.1.clone()).collect(), edges, From 950d797f1b8d5d60e6f39e0afa779d2dd37efa86 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 28 Feb 2021 01:40:07 +0100 Subject: [PATCH 08/10] Rewrite test #44 Because the current solution is wrong but maybe right due to randomness (the solution is not deterministic), run the test several times to reduce the possibility of passing it by chance. --- tests/test_exchange.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/test_exchange.rs b/tests/test_exchange.rs index 7fde320..496bbfa 100644 --- a/tests/test_exchange.rs +++ b/tests/test_exchange.rs @@ -34,22 +34,24 @@ P 2020-07-01 EUR 1.5 USD let eur = ledger.get_commodities().get("eur").unwrap(); let usd = ledger.get_commodities().get("usd").unwrap(); let acme = ledger.get_commodities().get("acme").unwrap(); - let multipliers_acme = conversion( - acme.clone(), - Utc::now().naive_local().date(), - ledger.get_prices(), - ); + for _ in 0..30 { + let multipliers_acme = conversion( + acme.clone(), + Utc::now().naive_local().date(), + ledger.get_prices(), + ); - let to_eur = multipliers_acme.get(eur).unwrap(); - let to_usd = multipliers_acme.get(usd).unwrap(); - assert_eq!( - to_eur, - &BigRational::from_integer(BigInt::from(1000)).inv(), - "1 ACME = 1000 EUR" - ); - assert_eq!( - to_usd, - &BigRational::from_integer(BigInt::from(1500)).inv(), - "1 ACME = 1500 USD" - ); + let to_eur = multipliers_acme.get(eur).unwrap(); + let to_usd = multipliers_acme.get(usd).unwrap(); + assert_eq!( + to_eur, + &BigRational::from_integer(BigInt::from(1000)).inv(), + "1 ACME = 1000 EUR" + ); + assert_eq!( + to_usd, + &BigRational::from_integer(BigInt::from(1500)).inv(), + "1 ACME = 1500 USD" + ); + } } From b89b9d16f2abfecb29631bbe41ce1e2b70c09087 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 28 Feb 2021 03:35:13 +0100 Subject: [PATCH 09/10] correct but slow #42 --- src/models/price.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/models/price.rs b/src/models/price.rs index 6395611..46c1ef7 100644 --- a/src/models/price.rs +++ b/src/models/price.rs @@ -74,17 +74,24 @@ pub fn conversion( queue.push(node.clone()); } while !queue.is_empty() { + // Sort largest to smallest queue.sort_by(|a, b| cmp(distances.get(b).unwrap(), distances.get(a).unwrap())); + + // Take the closest node let v = queue.pop().unwrap(); + // This means there is no path to the node if distances.get(v.as_ref()).unwrap().is_none() { break; } + // The path from the starting currency to the node let current_path = if let Some(path) = paths.get(v.as_ref()) { path.clone() } else { Vec::new() }; + + // Update the distances for (u, e) in graph.get_neighbours(v.as_ref()).iter() { // println!("Neighbour: {} {}", u.currency.get_name(), u.date); let alt = distances.get(v.as_ref()).unwrap().unwrap() + e.length(); @@ -102,11 +109,24 @@ pub fn conversion( paths.insert(u.clone(), u_path); } } + // Return not the paths but the multipliers let mut multipliers = HashMap::new(); + let mut inserted = HashMap::new(); for (k, v) in paths.iter() { + // println!("{} {} ~{:?}", k.currency.get_name(), k.date, v.len()); let mut mult = BigRational::new(BigInt::from(1), BigInt::from(1)); let mut currency = k.currency.clone(); + match inserted.get(&k.currency) { + Some(x) => { + if *x > k.date { + continue; + } + } + None => { + inserted.insert(currency.clone(), k.date); + } + } for edge in v.iter().rev() { match edge.as_ref().price.as_ref() { None => (), // do nothing, multiply by one and keep the same currency From 2471fba278bda922fb62b47d717a9faccba9b592 Mon Sep 17 00:00:00 2001 From: Claudio Noguera Date: Sun, 28 Feb 2021 04:33:39 +0100 Subject: [PATCH 10/10] correct and fast enough. Fix #44 --- src/models/price.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/models/price.rs b/src/models/price.rs index 46c1ef7..be0e7b9 100644 --- a/src/models/price.rs +++ b/src/models/price.rs @@ -204,12 +204,19 @@ impl Graph { } } } + } + for (_, p) in prices_nodup.iter() { + let commodities = + if p.price.get_commodity().unwrap().get_name() < p.commodity.as_ref().get_name() { + (p.price.get_commodity().unwrap(), p.commodity.clone()) + } else { + (p.commodity.clone(), p.price.get_commodity().unwrap()) + }; let c_vec = vec![commodities.0.clone(), commodities.1.clone()]; for c in c_vec { currency_dates.insert((c.clone(), p.date)); } } - // Create the nodes for (c, d) in currency_dates.iter() { nodes.insert( @@ -220,7 +227,6 @@ impl Graph { }), ); } - // Edges from the prices for (_, p) in prices_nodup.iter() { let from = nodes @@ -238,6 +244,8 @@ impl Graph { })); } + // println!("Nodes: {}", nodes.len()); + // println!("Edges: {}", edges.len()); let vec_node: Vec> = nodes.iter().map(|x| x.1.clone()).collect(); let n = vec_node.len(); for i in 0..n { @@ -251,6 +259,7 @@ impl Graph { } } } + // println!("Edges: {}", edges.len()); Graph { nodes: nodes.iter().map(|x| x.1.clone()).collect(),