Skip to main content

dinero/models/
price.rs

1use crate::models::{Currency, HasName, Money};
2use chrono::{Duration, NaiveDate};
3use num::rational::BigRational;
4use num::BigInt;
5use std::cmp::Ordering;
6use std::collections::{HashMap, HashSet};
7use std::fmt;
8use std::fmt::{Display, Formatter};
9use std::rc::Rc;
10
11/// A price relates two commodities
12#[derive(Debug, Clone)]
13pub struct Price {
14    date: NaiveDate,
15    commodity: Rc<Currency>,
16    price: Money,
17}
18
19impl Price {
20    pub fn new(date: NaiveDate, commodity: Rc<Currency>, price: Money) -> Price {
21        Price {
22            date,
23            commodity,
24            price,
25        }
26    }
27    pub fn get_price(&self) -> Money {
28        self.price.clone()
29    }
30}
31
32impl Display for Price {
33    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
34        write!(f, "{} {} {}", self.date, self.commodity, self.get_price())
35    }
36}
37
38#[derive(Debug, Copy, Clone)]
39pub enum PriceType {
40    Total,
41    PerUnit,
42}
43
44/// Convert from one currency to every other currency
45///
46/// This uses an implementation of the [Dijkstra algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) to find the shortest path from every
47/// currency to the desired one.
48///
49/// The return value is a conversion factor to every other currency
50pub fn conversion(
51    currency: Rc<Currency>,
52    date: NaiveDate,
53    prices: &[Price],
54) -> HashMap<Rc<Currency>, BigRational> {
55    // Build the graph
56    let source = Node {
57        currency: currency.clone(),
58        date,
59    };
60    let mut graph = Graph::from_prices(prices, source);
61    let mut distances = HashMap::new();
62    let mut paths: HashMap<Rc<Node>, Vec<Rc<Edge>>> = HashMap::new();
63    let mut queue = vec![];
64
65    // Initialize distances
66    for node in graph.nodes.iter() {
67        // println!("{} {} ", node.currency.get_name(), node.date);
68        if node.currency == currency {
69            distances.insert(node.clone(), Some(date - node.date));
70            // distances.insert(node.clone(), Some(date - date));
71            // println!("{}", date - node.date);
72        } else {
73            distances.insert(node.clone(), None);
74            // println!("None");
75        }
76        queue.push(node.clone());
77    }
78    while !queue.is_empty() {
79        // Sort largest to smallest
80        queue.sort_by(|a, b| cmp(distances.get(b).unwrap(), distances.get(a).unwrap()));
81
82        // Take the closest node
83        let v = queue.pop().unwrap();
84        // This means there is no path to the node
85        if distances.get(v.as_ref()).unwrap().is_none() {
86            break;
87        }
88
89        // The path from the starting currency to the node
90        let current_path = if let Some(path) = paths.get(v.as_ref()) {
91            path.clone()
92        } else {
93            Vec::new()
94        };
95
96        // Update the distances
97        for (u, e) in graph.get_neighbours(v.as_ref()).iter() {
98            // println!("Neighbour: {} {}", u.currency.get_name(), u.date);
99            let alt = distances.get(v.as_ref()).unwrap().unwrap() + e.length();
100            let distance = distances.get(u.as_ref()).unwrap();
101            let mut update = distance.is_none();
102            if !update {
103                update = alt < distance.unwrap();
104            }
105            if !update {
106                continue;
107            }
108            distances.insert(u.clone(), Some(alt));
109            let mut u_path = current_path.clone();
110            u_path.push(e.clone());
111            paths.insert(u.clone(), u_path);
112        }
113    }
114
115    // Return not the paths but the multipliers
116    let mut multipliers = HashMap::new();
117    let mut inserted = HashMap::new();
118    for (k, v) in paths.iter() {
119        let mut mult = BigRational::new(BigInt::from(1), BigInt::from(1));
120        let mut currency = k.currency.clone();
121        match inserted.get(&k.currency) {
122            Some(x) => {
123                if *x > k.date {
124                    continue;
125                }
126            }
127            None => {
128                inserted.insert(currency.clone(), k.date);
129            }
130        }
131        for edge in v.iter().rev() {
132            match edge.as_ref().price.as_ref() {
133                None => (), // do nothing, multiply by one and keep the same currency
134                Some(price) => {
135                    if currency == edge.from.currency {
136                        mult *= price.get_price().get_amount();
137                        currency = edge.to.currency.clone();
138                    } else {
139                        mult /= price.get_price().get_amount();
140                        currency = edge.from.currency.clone();
141                    }
142                }
143            }
144        }
145        multipliers.insert(k.currency.clone(), mult);
146    }
147    multipliers
148}
149
150#[derive(Debug, Eq, PartialEq, Hash, Clone)]
151pub struct Node {
152    pub(crate) currency: Rc<Currency>,
153    date: NaiveDate,
154}
155
156#[derive(Debug, Clone)]
157pub struct Edge {
158    price: Option<Price>,
159    from: Rc<Node>,
160    to: Rc<Node>,
161}
162
163impl Edge {
164    fn length(&self) -> Duration {
165        if self.from.date > self.to.date {
166            self.from.date - self.to.date
167        } else {
168            self.to.date - self.from.date
169        }
170    }
171}
172
173#[derive(Debug, Clone)]
174struct NodeEdge {
175    node: Rc<Node>,
176    edge: Rc<Edge>,
177}
178/// A graph
179#[derive(Debug, Clone)]
180struct Graph {
181    nodes: Vec<Rc<Node>>,
182    edges: Vec<Rc<Edge>>,
183}
184
185impl Graph {
186    /// Build the graph from prices
187    /// every price is a potential connection between two currencies,
188    /// so the prices are actually the edges of the graph
189    fn from_prices(prices: &[Price], source: Node) -> Self {
190        let mut nodes = HashMap::new();
191        let mut edges = Vec::new();
192
193        // keep the dates for which there is a price
194        let mut currency_dates = HashSet::new();
195        currency_dates.insert((source.currency.clone(), source.date));
196        // Remove redundant prices and create the nodes
197        let mut prices_nodup = HashMap::new();
198        for p in prices.iter() {
199            // Do not use prices from the future
200            if p.date >= source.date {
201                continue;
202            };
203            let commodities =
204                if p.price.get_commodity().unwrap().get_name() < p.commodity.as_ref().get_name() {
205                    (p.price.get_commodity().unwrap(), p.commodity.clone())
206                } else {
207                    (p.commodity.clone(), p.price.get_commodity().unwrap())
208                };
209            match prices_nodup.get(&commodities) {
210                None => {
211                    prices_nodup.insert(commodities.clone(), p.clone());
212                }
213                Some(x) => {
214                    if x.date < p.date {
215                        prices_nodup.insert(commodities.clone(), p.clone());
216                    }
217                }
218            }
219        }
220        for (_, p) in prices_nodup.iter() {
221            let commodities =
222                if p.price.get_commodity().unwrap().get_name() < p.commodity.as_ref().get_name() {
223                    (p.price.get_commodity().unwrap(), p.commodity.clone())
224                } else {
225                    (p.commodity.clone(), p.price.get_commodity().unwrap())
226                };
227            let c_vec = vec![commodities.0.clone(), commodities.1.clone()];
228            for c in c_vec {
229                currency_dates.insert((c.clone(), p.date));
230            }
231        }
232        // Create the nodes
233        for (c, d) in currency_dates.iter() {
234            nodes.insert(
235                (c.clone(), *d),
236                Rc::new(Node {
237                    currency: c.clone(),
238                    date: *d,
239                }),
240            );
241        }
242        // Edges from the prices
243        for (_, p) in prices_nodup.iter() {
244            let from = nodes.get(&(p.commodity.clone(), p.date)).unwrap().clone();
245            let to = nodes
246                .get(&(p.price.get_commodity().unwrap(), p.date))
247                .unwrap()
248                .clone();
249            edges.push(Rc::new(Edge {
250                price: Some(p.clone()),
251                from: from.clone(),
252                to: to.clone(),
253            }));
254        }
255
256        // println!("Nodes: {}", nodes.len());
257        // println!("Edges: {}", edges.len());
258        let vec_node: Vec<Rc<Node>> = nodes.iter().map(|x| x.1.clone()).collect();
259        let n = vec_node.len();
260        for i in 0..n {
261            for j in i..n {
262                if vec_node[i].currency == vec_node[j].currency {
263                    edges.push(Rc::new(Edge {
264                        price: None,
265                        from: vec_node[i].clone(),
266                        to: vec_node[j].clone(),
267                    }))
268                }
269            }
270        }
271        // println!("Edges: {}", edges.len());
272
273        Graph {
274            nodes: nodes.iter().map(|x| x.1.clone()).collect(),
275            edges,
276        }
277    }
278
279    fn get_neighbours(&mut self, node: &Node) -> Vec<(Rc<Node>, Rc<Edge>)> {
280        let mut neighbours = Vec::new();
281        for edge in self.edges.iter() {
282            if edge.from.as_ref() == node {
283                neighbours.push((edge.to.clone(), edge.clone()));
284            } else if edge.to.as_ref() == node {
285                neighbours.push((edge.from.clone(), edge.clone()));
286            }
287        }
288        neighbours
289    }
290}
291
292fn cmp(this: &Option<Duration>, other: &Option<Duration>) -> Ordering {
293    match this {
294        None => match other {
295            None => Ordering::Equal,
296            Some(_) => Ordering::Greater,
297        },
298        Some(s) => match other {
299            None => Ordering::Less,
300            Some(o) => s.cmp(o),
301        },
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::parser::Tokenizer;
309    use crate::CommonOpts;
310    use chrono::Utc;
311    use std::convert::TryFrom;
312    use std::path::PathBuf;
313    use structopt::StructOpt;
314
315    #[test]
316    fn test_graph() {
317        // Copy from balance command
318        let path = PathBuf::from("tests/example_files/demo.ledger");
319        let mut tokenizer = Tokenizer::try_from(&path).unwrap();
320        let options = CommonOpts::from_iter(["", "-f", ""].iter());
321        let items = tokenizer.tokenize(&options);
322        let ledger = items.to_ledger(&options).unwrap();
323
324        let currency = ledger.commodities.get("EUR").unwrap();
325        let multipliers = conversion(
326            currency.clone(),
327            Utc::now().naive_local().date(),
328            &ledger.prices,
329        );
330        assert_eq!(multipliers.len(), 7);
331    }
332}