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..02d4d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Changelog Changelog file for dinero-rs project, a command line application for managing finances. -## [0.14.0] +## [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) - ability to add tags from automated transactions 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 & + diff --git a/src/app.rs b/src/app.rs index 9fbef0d..c7e89e0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -312,9 +312,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() @@ -326,14 +326,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()) @@ -342,14 +342,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..ce35f68 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -119,13 +119,20 @@ 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 { if account.split(":").count() > depth { + index += 1; continue; } } + if bal.is_zero() { + index += 1; + continue; + } + showed_balances += 1; let mut first = true; for (_, money) in bal.balance.iter() { @@ -186,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() 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 34370fc..22d4594 100644 --- a/src/grammar/grammar.pest +++ b/src/grammar/grammar.pest @@ -4,9 +4,18 @@ // // Directives -directive = { ( price ) ~ end } +directive = { price | commodity } price = {"P" ~ ws* ~ date ~ (ws ~ time)? ~ ws+ ~ currency ~ ws* ~number ~ ws* ~ currency ~ws* ~ comment? ~ end} -transaction = {date ~ (ws ~ time)? ~ ws* ~ status? ~ ws* ~ status? ~ ws* ~ description ~ ws* ~ ("|" ~ ws*~payee)? ~ws* ~ comment? ~ end } +commodity = { "commodity" ~ ws+ ~ currency ~ 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 = { "*"| "!" } @@ -56,8 +65,8 @@ string = { ("\"" ~ (("\\\"") | (!"\"" ~ ANY))* ~ "\"") | ("'" ~ (("\\'") | (!"'" ~ ANY))* ~ "'") } -reserved = _{"+" | "*" | "/" | "\\" | "|" | "%" | "&" | "<" | ">" | ":" | "?" | "(" | ")" | ";"} -unquoted = { !reserved ~ !"=" ~ !"-" ~ +reserved = _{"+" | "*" | "/" | "\\" | "|" | "%" | "<" | ">" | ":" | "?" | "(" | ")" | ";"} +unquoted = { !reserved ~ !"=" ~ !"-" ~ !"&" ~ (!reserved ~ !SEPARATOR ~ ANY)+ } variable = { "account" | 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/src/models/price.rs b/src/models/price.rs index b4b63f5..be0e7b9 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,38 @@ 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() { + // Sort largest to smallest queue.sort_by(|a, b| cmp(distances.get(b).unwrap(), distances.get(a).unwrap())); - let v = queue.pop().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(); let distance = distances.get(u.as_ref()).unwrap(); let mut update = distance.is_none(); @@ -97,18 +109,36 @@ 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() { - 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 +154,7 @@ pub struct Node { #[derive(Debug, Clone)] pub struct Edge { - price: Price, + price: Option, from: Rc, to: Rc, } @@ -150,8 +180,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() { @@ -174,44 +204,63 @@ 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 { - 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(), })); } + // 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 { + 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(), + })) + } + } + } + // println!("Edges: {}", edges.len()); + Graph { nodes: nodes.iter().map(|x| x.1.clone()).collect(), edges, @@ -263,7 +312,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/src/parser/tokenizers/commodity.rs b/src/parser/tokenizers/commodity.rs index 9315d33..bdf43ff 100644 --- a/src/parser/tokenizers/commodity.rs +++ b/src/parser/tokenizers/commodity.rs @@ -1,64 +1,29 @@ +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; +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 +46,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/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() { 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..1422126 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,12 @@ 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 +91,13 @@ 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 +108,13 @@ 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 +125,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 +136,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 +147,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 +159,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 +177,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 +192,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 +203,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 +213,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 +223,12 @@ 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_exchange.rs b/tests/test_exchange.rs new file mode 100644 index 0000000..496bbfa --- /dev/null +++ b/tests/test_exchange.rs @@ -0,0 +1,57 @@ +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(); + 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" + ); + } +} 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);