ledger-utils 0.6.0

Ledger-cli (https://www.ledger-cli.org/) file processing Rust library, useful for calculating balances, creating reports etc.
Documentation
use crate::prices::{Prices, PricesError};
use crate::Amount;
use chrono::NaiveDate;
use rust_decimal::Decimal;
use rust_decimal::RoundingStrategy;
use std::collections::HashMap;
use std::fmt;
use std::ops::AddAssign;
use std::ops::SubAssign;

/// Balance of an single account.
///
/// Maps commodity names to amounts.
#[derive(Clone)]
pub struct AccountBalance {
    pub amounts: HashMap<String, Amount>,
}

impl Default for AccountBalance {
    fn default() -> Self {
        Self::new()
    }
}

impl AccountBalance {
    pub fn new() -> AccountBalance {
        AccountBalance {
            amounts: HashMap::new(),
        }
    }

    pub fn value_in_commodity(
        &self,
        commodity_name: &str,
        date: NaiveDate,
        prices: &Prices,
    ) -> Result<Decimal, PricesError> {
        let mut result = Decimal::new(0, 0);
        for amount in self.amounts.values() {
            if amount.commodity.name == commodity_name {
                result += amount.quantity;
            } else {
                result += prices.convert(
                    amount.quantity,
                    &amount.commodity.name,
                    commodity_name,
                    date,
                )?;
            }
        }
        Ok(result)
    }

    pub fn value_in_commodity_rounded(
        &self,
        commodity_name: &str,
        decimal_points: u32,
        date: NaiveDate,
        prices: &Prices,
    ) -> Decimal {
        let assets_value = self.value_in_commodity(commodity_name, date, prices);
        if let Ok(value) = assets_value {
            value.round_dp_with_strategy(decimal_points, RoundingStrategy::MidpointAwayFromZero)
        } else {
            panic!("{:?}", assets_value);
        }
    }

    pub fn is_zero(&self) -> bool {
        self.amounts
            .iter()
            .all(|(_, amount)| amount.quantity == Decimal::ZERO)
    }

    fn remove_empties(&mut self) {
        let empties: Vec<String> = self
            .amounts
            .iter()
            .filter(|(_, amount)| amount.quantity == Decimal::ZERO)
            .map(|(k, _)| k.clone())
            .collect();
        for empty in empties {
            self.amounts.remove(&empty);
        }
    }
}

impl fmt::Display for AccountBalance {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.amounts.is_empty() {
            write!(f, "0")?;
            return Ok(());
        }

        let mut amounts: Vec<_> = self.amounts.values().collect();
        amounts.sort_by_key(|a| &a.commodity.name);

        write!(f, "{}", amounts[0])?;

        for amount in amounts[1..].iter() {
            write!(f, ", {}", amount)?;
        }

        Ok(())
    }
}

impl<'a> AddAssign<&'a AccountBalance> for AccountBalance {
    fn add_assign(&mut self, other: &'a AccountBalance) {
        for (currrency_name, amount) in &other.amounts {
            self.amounts
                .entry(currrency_name.clone())
                .and_modify(|a| a.quantity += amount.quantity)
                .or_insert_with(|| amount.clone());
        }
        self.remove_empties();
    }
}

impl<'a> AddAssign<&'a Amount> for AccountBalance {
    fn add_assign(&mut self, amount: &'a Amount) {
        self.amounts
            .entry(amount.commodity.name.clone())
            .and_modify(|a| a.quantity += amount.quantity)
            .or_insert_with(|| amount.clone());
        self.remove_empties();
    }
}

impl<'a> SubAssign<&'a AccountBalance> for AccountBalance {
    fn sub_assign(&mut self, other: &'a AccountBalance) {
        for (currrency_name, amount) in &other.amounts {
            self.amounts
                .entry(currrency_name.clone())
                .and_modify(|a| a.quantity -= amount.quantity)
                .or_insert_with(|| amount.clone());
        }
        self.remove_empties();
    }
}

impl<'a> SubAssign<&'a Amount> for AccountBalance {
    fn sub_assign(&mut self, amount: &'a Amount) {
        self.amounts
            .entry(amount.commodity.name.clone())
            .and_modify(|a| a.quantity -= amount.quantity)
            .or_insert_with(|| amount.clone());
        self.remove_empties();
    }
}

impl fmt::Debug for AccountBalance {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        let mut values: Vec<Amount> = self.amounts.values().cloned().collect();
        values.sort_by(|a, b| a.commodity.name.partial_cmp(&b.commodity.name).unwrap());
        write!(f, "{:?}", values)
    }
}