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 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}