Skip to content

Commit 6c8cb75

Browse files
authored
Merge pull request #48 from frosklis/fix_44
Fix 44. Correct currency conversion.
2 parents 5fa1761 + 2471fba commit 6c8cb75

27 files changed

+272
-131
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Cargo.lock
33
.idea
44
.vscode
5-
examples/personal.ledger
5+
personal.ledger

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Changelog
22
Changelog file for dinero-rs project, a command line application for managing finances.
33

4-
## [0.14.0]
4+
## [0.15.0] -
5+
### Added
6+
- complete transaction grammar
7+
## [0.14.0] - 2021-02-27
58
### Fixed
69
- speed bump, from 7 seconds to 4 seconds in my personal ledger (still room to improve)
710
- ability to add tags from automated transactions

scripts/compare.sh

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/sh
2+
3+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
4+
5+
cd $DIR
6+
7+
ledger commodities | sort > commodities_ledger.txt &
8+
dinero commodities | sort > commodities_dinero.txt &
9+
10+
ledger payees | sort > payees_ledger.txt &
11+
dinero payees | sort > payees_dinero.txt &
12+
13+
ledger bal stockplan -X eur > bal_stockplan_ledger.txt &
14+
dinero bal stockplan -X eur > bal_stockplan_dinero.txt &
15+

src/app.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,9 @@ mod tests {
312312
"testing",
313313
"bal",
314314
"-f",
315-
"examples/demo.ledger",
315+
"tests/example_files/demo.ledger",
316316
"--init-file",
317-
"examples/example_ledgerrc",
317+
"tests/example_files/example_ledgerrc",
318318
"--real",
319319
]
320320
.iter()
@@ -326,14 +326,14 @@ mod tests {
326326

327327
#[test]
328328
#[should_panic(
329-
expected = "Bad config file \"examples/example_bad_ledgerrc\"\nThis line should be a comment but isn\'t, it is bad on purpose."
329+
expected = "Bad config file \"tests/example_files/example_bad_ledgerrc\"\nThis line should be a comment but isn\'t, it is bad on purpose."
330330
)]
331331
fn bad_ledgerrc() {
332332
let args: Vec<String> = vec![
333333
"testing",
334334
"bal",
335335
"--init-file",
336-
"examples/example_bad_ledgerrc",
336+
"tests/example_files/example_bad_ledgerrc",
337337
]
338338
.iter()
339339
.map(|x| x.to_string())
@@ -342,14 +342,14 @@ mod tests {
342342
}
343343
#[test]
344344
#[should_panic(
345-
expected = "Bad config file \"examples/example_bad_ledgerrc2\"\n- This does not parse either. And it shouldn't."
345+
expected = "Bad config file \"tests/example_files/example_bad_ledgerrc2\"\n- This does not parse either. And it shouldn't."
346346
)]
347347
fn other_bad_ledgerrc() {
348348
let args: Vec<String> = vec![
349349
"testing",
350350
"bal",
351351
"--init-file",
352-
"examples/example_bad_ledgerrc2",
352+
"tests/example_files/example_bad_ledgerrc2",
353353
]
354354
.iter()
355355
.map(|x| x.to_string())

src/commands/balance.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,20 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(),
119119
vec_balances.sort_by(|a, b| a.0.cmp(b.0));
120120
let num_bal = vec_balances.len();
121121
let mut index = 0;
122+
let mut showed_balances = 0;
122123
while index < num_bal {
123124
let (account, bal) = &vec_balances[index];
124125
if let Some(depth) = depth {
125126
if account.split(":").count() > depth {
127+
index += 1;
126128
continue;
127129
}
128130
}
131+
if bal.is_zero() {
132+
index += 1;
133+
continue;
134+
}
135+
showed_balances += 1;
129136

130137
let mut first = true;
131138
for (_, money) in bal.balance.iter() {
@@ -186,7 +193,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(),
186193
}
187194

188195
// Print the total
189-
if show_total & (vec_balances.len() > 1) {
196+
if show_total & (showed_balances > 1) {
190197
// Calculate it
191198
let mut total_balance = balances
192199
.iter()

src/filter.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::models::{Currency, Posting, PostingType, Transaction};
2-
use crate::parser::value_expr::{eval, eval_expression, EvalResult, Node};
2+
use crate::parser::value_expr::{eval, EvalResult, Node};
33
use crate::{CommonOpts, Error, List};
44
use colored::Colorize;
55
use regex::Regex;

src/grammar/grammar.pest

+13-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
//
55

66
// Directives
7-
directive = { ( price ) ~ end }
7+
directive = { price | commodity }
88
price = {"P" ~ ws* ~ date ~ (ws ~ time)? ~ ws+ ~ currency ~ ws* ~number ~ ws* ~ currency ~ws* ~ comment? ~ end}
9-
transaction = {date ~ (ws ~ time)? ~ ws* ~ status? ~ ws* ~ status? ~ ws* ~ description ~ ws* ~ ("|" ~ ws*~payee)? ~ws* ~ comment? ~ end }
9+
commodity = { "commodity" ~ ws+ ~ currency ~ ws* ~ comment? ~ end}
10+
transaction = {
11+
date ~ (ws ~ time)? ~ // date
12+
("=" ~ date ~ (ws ~ time)?)? ~ // effective_date
13+
ws+ ~ status? ~ // status
14+
ws* ~ code? // code
15+
~ ws* ~ description // description
16+
~ ws* ~ ("|" ~ ws*~payee)? // payee
17+
~ws* ~ comment? // comment
18+
~ end }
1019

1120
code = { "(" ~ string ~ ")" }
1221
status = { "*"| "!" }
@@ -56,8 +65,8 @@ string = {
5665
("\"" ~ (("\\\"") | (!"\"" ~ ANY))* ~ "\"") |
5766
("'" ~ (("\\'") | (!"'" ~ ANY))* ~ "'")
5867
}
59-
reserved = _{"+" | "*" | "/" | "\\" | "|" | "%" | "&" | "<" | ">" | ":" | "?" | "(" | ")" | ";"}
60-
unquoted = { !reserved ~ !"=" ~ !"-" ~
68+
reserved = _{"+" | "*" | "/" | "\\" | "|" | "%" | "<" | ">" | ":" | "?" | "(" | ")" | ";"}
69+
unquoted = { !reserved ~ !"=" ~ !"-" ~ !"&" ~
6170
(!reserved ~ !SEPARATOR ~ ANY)+ }
6271
variable = {
6372
"account" |

src/models/mod.rs

+9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ pub struct Ledger {
4242
pub(crate) payees: List<Payee>,
4343
}
4444

45+
impl Ledger {
46+
pub fn get_commodities(&self) -> &List<Currency> {
47+
&self.commodities
48+
}
49+
pub fn get_prices(&self) -> &Vec<Price> {
50+
&self.prices
51+
}
52+
}
53+
4554
impl ParsedLedger {
4655
/// Creates a proper ledger from a parsed ledger
4756
pub fn to_ledger(mut self, no_checks: bool) -> Result<Ledger, Error> {

src/models/price.rs

+76-27
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use chrono::{Duration, NaiveDate};
33
use num::rational::BigRational;
44
use num::BigInt;
55
use std::cmp::Ordering;
6-
use std::collections::HashMap;
6+
use std::collections::{HashMap, HashSet};
77
use std::fmt;
88
use std::fmt::{Display, Formatter};
99
use std::rc::Rc;
@@ -62,26 +62,38 @@ pub fn conversion(
6262

6363
// Initialize distances
6464
for node in graph.nodes.iter() {
65+
// println!("{} {} ", node.currency.get_name(), node.date);
6566
if node.currency == currency {
6667
distances.insert(node.clone(), Some(date - node.date));
68+
// distances.insert(node.clone(), Some(date - date));
69+
// println!("{}", date - node.date);
6770
} else {
6871
distances.insert(node.clone(), None);
72+
// println!("None");
6973
}
7074
queue.push(node.clone());
7175
}
7276
while !queue.is_empty() {
77+
// Sort largest to smallest
7378
queue.sort_by(|a, b| cmp(distances.get(b).unwrap(), distances.get(a).unwrap()));
74-
let v = queue.pop().unwrap();
7579

80+
// Take the closest node
81+
let v = queue.pop().unwrap();
82+
// This means there is no path to the node
7683
if distances.get(v.as_ref()).unwrap().is_none() {
7784
break;
7885
}
86+
87+
// The path from the starting currency to the node
7988
let current_path = if let Some(path) = paths.get(v.as_ref()) {
8089
path.clone()
8190
} else {
8291
Vec::new()
8392
};
93+
94+
// Update the distances
8495
for (u, e) in graph.get_neighbours(v.as_ref()).iter() {
96+
// println!("Neighbour: {} {}", u.currency.get_name(), u.date);
8597
let alt = distances.get(v.as_ref()).unwrap().unwrap() + e.length();
8698
let distance = distances.get(u.as_ref()).unwrap();
8799
let mut update = distance.is_none();
@@ -97,18 +109,36 @@ pub fn conversion(
97109
paths.insert(u.clone(), u_path);
98110
}
99111
}
112+
100113
// Return not the paths but the multipliers
101114
let mut multipliers = HashMap::new();
115+
let mut inserted = HashMap::new();
102116
for (k, v) in paths.iter() {
117+
// println!("{} {} ~{:?}", k.currency.get_name(), k.date, v.len());
103118
let mut mult = BigRational::new(BigInt::from(1), BigInt::from(1));
104119
let mut currency = k.currency.clone();
120+
match inserted.get(&k.currency) {
121+
Some(x) => {
122+
if *x > k.date {
123+
continue;
124+
}
125+
}
126+
None => {
127+
inserted.insert(currency.clone(), k.date);
128+
}
129+
}
105130
for edge in v.iter().rev() {
106-
if currency == edge.from.currency {
107-
mult *= edge.price.get_price().get_amount();
108-
currency = edge.to.currency.clone();
109-
} else {
110-
mult /= edge.price.get_price().get_amount();
111-
currency = edge.from.currency.clone();
131+
match edge.as_ref().price.as_ref() {
132+
None => (), // do nothing, multiply by one and keep the same currency
133+
Some(price) => {
134+
if currency == edge.from.currency {
135+
mult *= price.get_price().get_amount();
136+
currency = edge.to.currency.clone();
137+
} else {
138+
mult /= price.get_price().get_amount();
139+
currency = edge.from.currency.clone();
140+
}
141+
}
112142
}
113143
}
114144
multipliers.insert(k.currency.clone(), mult);
@@ -124,7 +154,7 @@ pub struct Node {
124154

125155
#[derive(Debug, Clone)]
126156
pub struct Edge {
127-
price: Price,
157+
price: Option<Price>,
128158
from: Rc<Node>,
129159
to: Rc<Node>,
130160
}
@@ -150,8 +180,8 @@ impl Graph {
150180
fn from_prices(prices: &Vec<Price>, source: Node) -> Self {
151181
let mut nodes = HashMap::new();
152182
let mut edges = Vec::new();
153-
let mut currency_dates = HashMap::new();
154-
currency_dates.insert(source.currency.clone(), source.date);
183+
let mut currency_dates = HashSet::new();
184+
currency_dates.insert((source.currency.clone(), source.date));
155185
// Remove redundant prices and create the nodes
156186
let mut prices_nodup = HashMap::new();
157187
for p in prices.iter() {
@@ -174,44 +204,63 @@ impl Graph {
174204
}
175205
}
176206
}
207+
}
208+
for (_, p) in prices_nodup.iter() {
209+
let commodities =
210+
if p.price.get_commodity().unwrap().get_name() < p.commodity.as_ref().get_name() {
211+
(p.price.get_commodity().unwrap(), p.commodity.clone())
212+
} else {
213+
(p.commodity.clone(), p.price.get_commodity().unwrap())
214+
};
177215
let c_vec = vec![commodities.0.clone(), commodities.1.clone()];
178216
for c in c_vec {
179-
match currency_dates.get(c.as_ref()) {
180-
Some(v) => {
181-
if v < &p.date {
182-
currency_dates.insert(c.clone(), p.date);
183-
}
184-
}
185-
None => {
186-
currency_dates.insert(c.clone(), p.date);
187-
}
188-
}
217+
currency_dates.insert((c.clone(), p.date));
189218
}
190219
}
191-
192220
// Create the nodes
193221
for (c, d) in currency_dates.iter() {
194222
nodes.insert(
195-
c.clone(),
223+
(c.clone(), d.clone()),
196224
Rc::new(Node {
197225
currency: c.clone(),
198226
date: d.clone(),
199227
}),
200228
);
201229
}
230+
// Edges from the prices
202231
for (_, p) in prices_nodup.iter() {
203-
let from = nodes.get(p.commodity.as_ref()).unwrap().clone();
232+
let from = nodes
233+
.get(&(p.commodity.clone(), p.date.clone()))
234+
.unwrap()
235+
.clone();
204236
let to = nodes
205-
.get(p.price.get_commodity().unwrap().as_ref())
237+
.get(&(p.price.get_commodity().unwrap(), p.date.clone()))
206238
.unwrap()
207239
.clone();
208240
edges.push(Rc::new(Edge {
209-
price: p.clone(),
241+
price: Some(p.clone()),
210242
from: from.clone(),
211243
to: to.clone(),
212244
}));
213245
}
214246

247+
// println!("Nodes: {}", nodes.len());
248+
// println!("Edges: {}", edges.len());
249+
let vec_node: Vec<Rc<Node>> = nodes.iter().map(|x| x.1.clone()).collect();
250+
let n = vec_node.len();
251+
for i in 0..n {
252+
for j in i..n {
253+
if vec_node[i].currency == vec_node[j].currency {
254+
edges.push(Rc::new(Edge {
255+
price: None,
256+
from: vec_node[i].clone(),
257+
to: vec_node[j].clone(),
258+
}))
259+
}
260+
}
261+
}
262+
// println!("Edges: {}", edges.len());
263+
215264
Graph {
216265
nodes: nodes.iter().map(|x| x.1.clone()).collect(),
217266
edges,
@@ -263,7 +312,7 @@ mod tests {
263312
#[test]
264313
fn test_graph() {
265314
// Copy from balance command
266-
let path = PathBuf::from("examples/demo.ledger");
315+
let path = PathBuf::from("tests/example_files/demo.ledger");
267316
let mut tokenizer = Tokenizer::from(&path);
268317
let items = tokenizer.tokenize().unwrap();
269318
let ledger = items.to_ledger(false).unwrap();

0 commit comments

Comments
 (0)