ledger_utils/
account_balance.rs

1use crate::prices::{Prices, PricesError};
2use crate::Amount;
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use rust_decimal::RoundingStrategy;
6use std::collections::HashMap;
7use std::fmt;
8use std::ops::AddAssign;
9use std::ops::SubAssign;
10
11/// Balance of an single account.
12///
13/// Maps commodity names to amounts.
14#[derive(Clone)]
15pub struct AccountBalance {
16    pub amounts: HashMap<String, Amount>,
17}
18
19impl Default for AccountBalance {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl AccountBalance {
26    pub fn new() -> AccountBalance {
27        AccountBalance {
28            amounts: HashMap::new(),
29        }
30    }
31
32    pub fn value_in_commodity(
33        &self,
34        commodity_name: &str,
35        date: NaiveDate,
36        prices: &Prices,
37    ) -> Result<Decimal, PricesError> {
38        let mut result = Decimal::new(0, 0);
39        for amount in self.amounts.values() {
40            if amount.commodity.name == commodity_name {
41                result += amount.quantity;
42            } else {
43                result += prices.convert(
44                    amount.quantity,
45                    &amount.commodity.name,
46                    commodity_name,
47                    date,
48                )?;
49            }
50        }
51        Ok(result)
52    }
53
54    pub fn value_in_commodity_rounded(
55        &self,
56        commodity_name: &str,
57        decimal_points: u32,
58        date: NaiveDate,
59        prices: &Prices,
60    ) -> Decimal {
61        let assets_value = self.value_in_commodity(commodity_name, date, prices);
62        if let Ok(value) = assets_value {
63            value.round_dp_with_strategy(decimal_points, RoundingStrategy::MidpointAwayFromZero)
64        } else {
65            panic!("{:?}", assets_value);
66        }
67    }
68
69    pub fn is_zero(&self) -> bool {
70        self.amounts
71            .iter()
72            .all(|(_, amount)| amount.quantity == Decimal::ZERO)
73    }
74
75    fn remove_empties(&mut self) {
76        let empties: Vec<String> = self
77            .amounts
78            .iter()
79            .filter(|(_, amount)| amount.quantity == Decimal::ZERO)
80            .map(|(k, _)| k.clone())
81            .collect();
82        for empty in empties {
83            self.amounts.remove(&empty);
84        }
85    }
86}
87
88impl fmt::Display for AccountBalance {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        if self.amounts.is_empty() {
91            write!(f, "0")?;
92            return Ok(());
93        }
94
95        let mut amounts: Vec<_> = self.amounts.values().collect();
96        amounts.sort_by_key(|a| &a.commodity.name);
97
98        write!(f, "{}", amounts[0])?;
99
100        for amount in amounts[1..].iter() {
101            write!(f, ", {}", amount)?;
102        }
103
104        Ok(())
105    }
106}
107
108impl<'a> AddAssign<&'a AccountBalance> for AccountBalance {
109    fn add_assign(&mut self, other: &'a AccountBalance) {
110        for (currrency_name, amount) in &other.amounts {
111            self.amounts
112                .entry(currrency_name.clone())
113                .and_modify(|a| a.quantity += amount.quantity)
114                .or_insert_with(|| amount.clone());
115        }
116        self.remove_empties();
117    }
118}
119
120impl<'a> AddAssign<&'a Amount> for AccountBalance {
121    fn add_assign(&mut self, amount: &'a Amount) {
122        self.amounts
123            .entry(amount.commodity.name.clone())
124            .and_modify(|a| a.quantity += amount.quantity)
125            .or_insert_with(|| amount.clone());
126        self.remove_empties();
127    }
128}
129
130impl<'a> SubAssign<&'a AccountBalance> for AccountBalance {
131    fn sub_assign(&mut self, other: &'a AccountBalance) {
132        for (currrency_name, amount) in &other.amounts {
133            self.amounts
134                .entry(currrency_name.clone())
135                .and_modify(|a| a.quantity -= amount.quantity)
136                .or_insert_with(|| amount.clone());
137        }
138        self.remove_empties();
139    }
140}
141
142impl<'a> SubAssign<&'a Amount> for AccountBalance {
143    fn sub_assign(&mut self, amount: &'a Amount) {
144        self.amounts
145            .entry(amount.commodity.name.clone())
146            .and_modify(|a| a.quantity -= amount.quantity)
147            .or_insert_with(|| amount.clone());
148        self.remove_empties();
149    }
150}
151
152impl fmt::Debug for AccountBalance {
153    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
154        let mut values: Vec<Amount> = self.amounts.values().cloned().collect();
155        values.sort_by(|a, b| a.commodity.name.partial_cmp(&b.commodity.name).unwrap());
156        write!(f, "{:?}", values)
157    }
158}