ledger_utils/
prices.rs

1use chrono::NaiveDate;
2use ledger_parser::*;
3use rust_decimal::Decimal;
4use std::collections::BTreeMap;
5use std::collections::HashMap;
6
7#[derive(Debug)]
8pub enum PricesError {
9    NoSuchCommoditiesPair(CommoditiesPair),
10    DateTooEarly(NaiveDate),
11}
12
13#[derive(Eq, PartialEq, Hash, Clone, Debug)]
14pub struct CommoditiesPair {
15    pub src_commodity_name: String,
16    pub dst_commodity_name: String,
17}
18
19impl CommoditiesPair {
20    pub fn new(src_commodity_name: &str, dst_commodity_name: &str) -> CommoditiesPair {
21        CommoditiesPair {
22            src_commodity_name: src_commodity_name.to_string(),
23            dst_commodity_name: dst_commodity_name.to_string(),
24        }
25    }
26}
27
28#[derive(Debug)]
29pub struct RatesTable {
30    pub table: BTreeMap<NaiveDate, Decimal>,
31}
32
33impl RatesTable {
34    fn new() -> RatesTable {
35        RatesTable {
36            table: BTreeMap::new(),
37        }
38    }
39
40    fn get_rate(&self, date: NaiveDate) -> Result<Decimal, PricesError> {
41        let mut rate: Option<Decimal> = None;
42        for (key, value) in self.table.iter() {
43            if *key <= date {
44                rate = Some(*value)
45            } else {
46                break;
47            }
48        }
49        rate.ok_or(PricesError::DateTooEarly(date))
50    }
51}
52
53#[derive(Debug)]
54pub struct Prices {
55    pub rates: HashMap<CommoditiesPair, RatesTable>,
56}
57
58impl Default for Prices {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl Prices {
65    pub fn new() -> Self {
66        Self {
67            rates: HashMap::new(),
68        }
69    }
70
71    pub fn insert_from(&mut self, ledger: &Ledger) {
72        self.add_prices(&get_commodity_prices(ledger));
73        self.add_prices(&get_prices_from_transactions(ledger));
74    }
75
76    pub fn convert(
77        &self,
78        amount: Decimal,
79        src_commodity_name: &str,
80        dst_commodity_name: &str,
81        date: NaiveDate,
82    ) -> Result<Decimal, PricesError> {
83        let rate = self.get_rate(src_commodity_name, dst_commodity_name, date)?;
84        Ok(amount * rate)
85    }
86
87    pub fn get_rate(
88        &self,
89        src_commodity_name: &str,
90        dst_commodity_name: &str,
91        date: NaiveDate,
92    ) -> Result<Decimal, PricesError> {
93        let commodities_pair = CommoditiesPair::new(src_commodity_name, dst_commodity_name);
94
95        self.get_rates_table(&commodities_pair)?.get_rate(date)
96    }
97
98    fn get_rates_table(
99        &self,
100        commodities_pair: &CommoditiesPair,
101    ) -> Result<&RatesTable, PricesError> {
102        self.rates
103            .get(commodities_pair)
104            .ok_or_else(|| PricesError::NoSuchCommoditiesPair(commodities_pair.clone()))
105    }
106
107    fn add_prices(&mut self, prices: &[CommodityPrice]) {
108        for price in prices {
109            self.add_price(
110                &price.commodity_name,
111                &price.amount.commodity.name,
112                price.amount.quantity,
113                price.datetime.date(),
114            );
115            self.add_price(
116                &price.amount.commodity.name,
117                &price.commodity_name,
118                Decimal::new(1, 0) / price.amount.quantity,
119                price.datetime.date(),
120            );
121        }
122    }
123
124    fn add_price(
125        &mut self,
126        src_commodity_name: &str,
127        dst_commodity_name: &str,
128        rate: Decimal,
129        date: NaiveDate,
130    ) {
131        let commodities_pair = CommoditiesPair::new(src_commodity_name, dst_commodity_name);
132        self.rates
133            .entry(commodities_pair)
134            .or_insert_with(RatesTable::new)
135            .table
136            .entry(date)
137            .and_modify(|r| *r = rate)
138            .or_insert(rate);
139    }
140}
141
142fn get_commodity_prices(ledger: &Ledger) -> Vec<CommodityPrice> {
143    let mut result = Vec::new();
144    for item in &ledger.items {
145        if let LedgerItem::CommodityPrice(commodity_price) = item {
146            result.push(commodity_price.clone());
147        }
148    }
149    result
150}
151
152fn get_prices_from_transactions(ledger: &Ledger) -> Vec<CommodityPrice> {
153    let mut result = Vec::new();
154    for item in &ledger.items {
155        if let LedgerItem::Transaction(transaction) = item {
156            // TODO: handle empty amounts & balance verifications
157            if transaction.postings.len() == 2
158                && transaction.postings[0]
159                    .amount
160                    .clone()
161                    .unwrap()
162                    .amount
163                    .commodity
164                    .name
165                    != transaction.postings[1]
166                        .amount
167                        .clone()
168                        .unwrap()
169                        .amount
170                        .commodity
171                        .name
172                && transaction.postings[0]
173                    .amount
174                    .clone()
175                    .unwrap()
176                    .amount
177                    .quantity
178                    != Decimal::new(0, 0)
179                && transaction.postings[1]
180                    .amount
181                    .clone()
182                    .unwrap()
183                    .amount
184                    .quantity
185                    != Decimal::new(0, 0)
186            {
187                result.push(CommodityPrice {
188                    datetime: transaction.date.and_hms_opt(0, 0, 0).unwrap(),
189                    commodity_name: (transaction.postings[0])
190                        .amount
191                        .clone()
192                        .unwrap()
193                        .amount
194                        .commodity
195                        .name,
196                    amount: Amount {
197                        quantity: -transaction.postings[1]
198                            .amount
199                            .clone()
200                            .unwrap()
201                            .amount
202                            .quantity
203                            / transaction.postings[0]
204                                .amount
205                                .clone()
206                                .unwrap()
207                                .amount
208                                .quantity,
209                        commodity: (transaction.postings[1])
210                            .amount
211                            .clone()
212                            .unwrap()
213                            .amount
214                            .commodity,
215                    },
216                })
217            }
218        }
219    }
220    result
221}