use std::{
collections::{BTreeMap, btree_map},
fmt,
iter::FusedIterator,
};
use crate::{Currency, Decimal, Format, Money, MoneyError, format::write_padded};
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct MoneyBag {
balances: BTreeMap<Currency, Decimal>,
}
impl MoneyBag {
#[must_use]
pub fn new() -> Self {
Self {
balances: BTreeMap::new(),
}
}
#[must_use]
pub fn balance(&self, currency: &Currency) -> Money {
Money::from_decimal(self.amount_of(currency), currency)
}
pub fn take(&mut self, currency: &Currency) -> Money {
let amount = self.balances.remove(currency).unwrap_or(Decimal::ZERO);
Money::from_decimal(amount, currency)
}
#[must_use]
pub fn covers<B: Into<MoneyBag>>(&self, other: B) -> bool {
other
.into()
.balances
.iter()
.all(|(currency, owed)| self.amount_of(currency) >= *owed)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.balances.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.balances.len()
}
pub fn iter(&self) -> Balances<'_> {
Balances(self.balances.iter())
}
pub fn checked_add<B: Into<MoneyBag>>(&self, other: B) -> Result<Self, MoneyError> {
self.combined_with(&other.into(), Decimal::checked_add)
}
pub fn checked_sub<B: Into<MoneyBag>>(&self, other: B) -> Result<Self, MoneyError> {
self.combined_with(&other.into(), Decimal::checked_sub)
}
#[must_use]
pub fn format_with(&self, format: &Format) -> impl fmt::Display + use<> {
FormattedBag {
bag: self.clone(),
format: *format,
}
}
fn amount_of(&self, currency: &Currency) -> Decimal {
self.balances
.get(currency)
.copied()
.unwrap_or(Decimal::ZERO)
}
fn set(&mut self, balance: Money) {
if balance.is_amount_zero() {
self.balances.remove(&balance.currency());
} else {
self.balances.insert(balance.currency(), balance.amount());
}
}
fn balance_after(
&self,
money: Money,
op: fn(Decimal, Decimal) -> Option<Decimal>,
) -> Result<Money, MoneyError> {
let currency = money.currency();
op(self.amount_of(¤cy), money.amount())
.map(|total| Money::from_decimal(total, ¤cy))
.ok_or(MoneyError::Overflow)
}
fn combined_with(
&self,
other: &MoneyBag,
op: fn(Decimal, Decimal) -> Option<Decimal>,
) -> Result<Self, MoneyError> {
let mut combined = self.clone();
for money in other {
let balance = combined.balance_after(money, op)?;
combined.set(balance);
}
Ok(combined)
}
fn render(&self, format: &Format) -> String {
self.iter()
.map(|money| money.format_with(format).to_string())
.collect::<Vec<_>>()
.join(", ")
}
}
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Balances<'a>(btree_map::Iter<'a, Currency, Decimal>);
impl Iterator for Balances<'_> {
type Item = Money;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(borrowed_entry_to_money)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for Balances<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(borrowed_entry_to_money)
}
}
impl ExactSizeIterator for Balances<'_> {
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for Balances<'_> {}
#[derive(Debug)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct IntoBalances(btree_map::IntoIter<Currency, Decimal>);
impl Iterator for IntoBalances {
type Item = Money;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(entry_to_money)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for IntoBalances {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(entry_to_money)
}
}
impl ExactSizeIterator for IntoBalances {
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for IntoBalances {}
fn entry_to_money((currency, amount): (Currency, Decimal)) -> Money {
Money::from_decimal(amount, ¤cy)
}
fn borrowed_entry_to_money((currency, amount): (&Currency, &Decimal)) -> Money {
entry_to_money((*currency, *amount))
}
impl fmt::Display for MoneyBag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_padded(f, &self.render(&Format::default()))
}
}
struct FormattedBag {
bag: MoneyBag,
format: Format,
}
impl fmt::Display for FormattedBag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_padded(f, &self.bag.render(&self.format))
}
}
impl From<Money> for MoneyBag {
fn from(money: Money) -> Self {
let mut bag = Self::new();
bag.set(money);
bag
}
}
impl From<&Money> for MoneyBag {
fn from(money: &Money) -> Self {
Self::from(*money)
}
}
impl From<&MoneyBag> for MoneyBag {
fn from(bag: &MoneyBag) -> Self {
bag.clone()
}
}
impl std::ops::AddAssign<Money> for MoneyBag {
fn add_assign(&mut self, rhs: Money) {
let balance = self
.balance_after(rhs, Decimal::checked_add)
.unwrap_or_else(|e| panic!("addition error: {e}"));
self.set(balance);
}
}
impl std::ops::AddAssign<&Money> for MoneyBag {
fn add_assign(&mut self, rhs: &Money) {
*self += *rhs;
}
}
impl std::ops::SubAssign<Money> for MoneyBag {
fn sub_assign(&mut self, rhs: Money) {
let balance = self
.balance_after(rhs, Decimal::checked_sub)
.unwrap_or_else(|e| panic!("subtraction error: {e}"));
self.set(balance);
}
}
impl std::ops::SubAssign<&Money> for MoneyBag {
fn sub_assign(&mut self, rhs: &Money) {
*self -= *rhs;
}
}
impl std::ops::AddAssign<&MoneyBag> for MoneyBag {
fn add_assign(&mut self, rhs: &MoneyBag) {
*self = self
.combined_with(rhs, Decimal::checked_add)
.unwrap_or_else(|e| panic!("addition error: {e}"));
}
}
impl std::ops::AddAssign<MoneyBag> for MoneyBag {
fn add_assign(&mut self, rhs: MoneyBag) {
*self += &rhs;
}
}
impl std::ops::SubAssign<&MoneyBag> for MoneyBag {
fn sub_assign(&mut self, rhs: &MoneyBag) {
*self = self
.combined_with(rhs, Decimal::checked_sub)
.unwrap_or_else(|e| panic!("subtraction error: {e}"));
}
}
impl std::ops::SubAssign<MoneyBag> for MoneyBag {
fn sub_assign(&mut self, rhs: MoneyBag) {
*self -= &rhs;
}
}
impl std::ops::Add<&MoneyBag> for &MoneyBag {
type Output = MoneyBag;
fn add(self, rhs: &MoneyBag) -> Self::Output {
self.combined_with(rhs, Decimal::checked_add)
.unwrap_or_else(|e| panic!("addition error: {e}"))
}
}
impl std::ops::Add for MoneyBag {
type Output = MoneyBag;
fn add(self, rhs: Self) -> Self::Output {
&self + &rhs
}
}
impl std::ops::Add<&MoneyBag> for MoneyBag {
type Output = MoneyBag;
fn add(self, rhs: &MoneyBag) -> Self::Output {
&self + rhs
}
}
impl std::ops::Add<MoneyBag> for &MoneyBag {
type Output = MoneyBag;
fn add(self, rhs: MoneyBag) -> Self::Output {
self + &rhs
}
}
impl std::ops::Sub<&MoneyBag> for &MoneyBag {
type Output = MoneyBag;
fn sub(self, rhs: &MoneyBag) -> Self::Output {
self.combined_with(rhs, Decimal::checked_sub)
.unwrap_or_else(|e| panic!("subtraction error: {e}"))
}
}
impl std::ops::Sub for MoneyBag {
type Output = MoneyBag;
fn sub(self, rhs: Self) -> Self::Output {
&self - &rhs
}
}
impl std::ops::Sub<&MoneyBag> for MoneyBag {
type Output = MoneyBag;
fn sub(self, rhs: &MoneyBag) -> Self::Output {
&self - rhs
}
}
impl std::ops::Sub<MoneyBag> for &MoneyBag {
type Output = MoneyBag;
fn sub(self, rhs: MoneyBag) -> Self::Output {
self - &rhs
}
}
impl IntoIterator for MoneyBag {
type Item = Money;
type IntoIter = IntoBalances;
fn into_iter(self) -> Self::IntoIter {
IntoBalances(self.balances.into_iter())
}
}
impl<'a> IntoIterator for &'a MoneyBag {
type Item = Money;
type IntoIter = Balances<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl Extend<Money> for MoneyBag {
fn extend<I: IntoIterator<Item = Money>>(&mut self, iter: I) {
for money in iter {
*self += money;
}
}
}
impl<'a> Extend<&'a Money> for MoneyBag {
fn extend<I: IntoIterator<Item = &'a Money>>(&mut self, iter: I) {
self.extend(iter.into_iter().copied());
}
}
impl Extend<MoneyBag> for MoneyBag {
fn extend<I: IntoIterator<Item = MoneyBag>>(&mut self, iter: I) {
for bag in iter {
*self += &bag;
}
}
}
impl FromIterator<Money> for MoneyBag {
fn from_iter<I: IntoIterator<Item = Money>>(iter: I) -> Self {
let mut bag = Self::new();
bag.extend(iter);
bag
}
}
impl<'a> FromIterator<&'a Money> for MoneyBag {
fn from_iter<I: IntoIterator<Item = &'a Money>>(iter: I) -> Self {
iter.into_iter().copied().collect()
}
}
impl FromIterator<MoneyBag> for MoneyBag {
fn from_iter<I: IntoIterator<Item = MoneyBag>>(iter: I) -> Self {
let mut bag = Self::new();
bag.extend(iter);
bag
}
}
impl std::iter::Sum<Money> for MoneyBag {
fn sum<I: Iterator<Item = Money>>(iter: I) -> Self {
iter.collect()
}
}
impl<'a> std::iter::Sum<&'a Money> for MoneyBag {
fn sum<I: Iterator<Item = &'a Money>>(iter: I) -> Self {
iter.copied().collect()
}
}
impl std::iter::Sum<MoneyBag> for MoneyBag {
fn sum<I: Iterator<Item = MoneyBag>>(iter: I) -> Self {
iter.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn usd(major: i64) -> Money {
Money::from_major(major, &Currency::USD)
}
fn eur(major: i64) -> Money {
Money::from_major(major, &Currency::EUR)
}
#[test]
fn accumulates_each_currency_separately_test() {
let mut bag = MoneyBag::new();
bag += usd(10);
bag += eur(4);
bag += usd(5);
assert_eq!(bag.balance(&Currency::USD), usd(15));
assert_eq!(bag.balance(&Currency::EUR), eur(4));
assert_eq!(bag.len(), 2);
}
#[test]
fn untouched_currency_balances_zero_test() {
let bag = MoneyBag::from(usd(10));
assert_eq!(
bag.balance(&Currency::JPY),
Money::from_major(0, &Currency::JPY)
);
}
#[test]
fn zeroed_balance_is_dropped_test() {
let mut bag = MoneyBag::from(usd(10));
bag -= usd(10);
assert!(bag.is_empty());
assert_eq!(bag, MoneyBag::new());
}
#[test]
fn equal_positions_compare_equal_regardless_of_history_test() {
let mut spent = MoneyBag::new();
spent += usd(10);
spent += eur(7);
spent -= eur(7);
assert_eq!(spent, MoneyBag::from(usd(10)));
}
#[test]
fn balances_may_go_negative_test() {
let mut bag = MoneyBag::new();
bag -= usd(3);
assert_eq!(bag.balance(&Currency::USD), usd(-3));
assert_eq!(bag.len(), 1);
}
#[test]
fn take_removes_and_returns_the_balance_test() {
let mut bag = MoneyBag::from(usd(10));
assert_eq!(bag.take(&Currency::USD), usd(10));
assert_eq!(bag.take(&Currency::USD), usd(0));
assert!(bag.is_empty());
}
#[test]
fn covers_weighs_currencies_separately_test() {
let mut wallet = MoneyBag::new();
wallet += usd(50);
wallet += eur(20);
assert!(wallet.covers(usd(50)));
assert!(wallet.covers(MoneyBag::from(eur(20))));
assert!(!wallet.covers(eur(21)));
assert!(wallet.covers(MoneyBag::new()));
}
#[test]
fn a_surplus_cannot_cover_another_currencys_shortfall_test() {
let wallet = MoneyBag::from(usd(1_000));
assert!(!wallet.covers(eur(1)));
}
#[test]
fn covers_is_not_an_ordering_test() {
let rich_in_usd = MoneyBag::from(usd(10));
let rich_in_eur = MoneyBag::from(eur(10));
assert!(!rich_in_usd.covers(&rich_in_eur));
assert!(!rich_in_eur.covers(&rich_in_usd));
}
#[test]
fn iterates_in_alphabetic_code_order_test() {
let mut bag = MoneyBag::new();
bag += Money::from_major(1, &Currency::ZAR);
bag += usd(1);
bag += Money::from_major(1, &Currency::AUD);
let codes: Vec<String> = bag.iter().map(|m| m.currency().to_string()).collect();
assert_eq!(codes, ["AUD", "USD", "ZAR"]);
}
#[test]
fn checked_add_accepts_money_and_bags_test() {
let wallet = MoneyBag::from(usd(10));
let deposit = MoneyBag::from(eur(5));
assert_eq!(wallet.checked_add(usd(5)), Ok(MoneyBag::from(usd(15))));
assert_eq!(
wallet.checked_add(deposit.clone()),
Ok([usd(10), eur(5)].into_iter().collect())
);
assert_eq!(
wallet.checked_add(&deposit),
Ok([usd(10), eur(5)].into_iter().collect())
);
assert_eq!(wallet.checked_sub(&wallet), Ok(MoneyBag::new()));
}
#[test]
fn checked_add_reports_overflow_test() {
let ceiling = MoneyBag::from(Money::from_decimal(Decimal::MAX, &Currency::USD));
assert_eq!(ceiling.checked_add(usd(1)), Err(MoneyError::Overflow));
}
#[test]
fn checked_sub_reports_overflow_test() {
let floor = MoneyBag::from(Money::from_decimal(Decimal::MIN, &Currency::USD));
assert_eq!(floor.checked_sub(usd(1)), Err(MoneyError::Overflow));
}
#[test]
fn overflow_leaves_the_bag_untouched_test() {
let ceiling = MoneyBag::from(Money::from_decimal(Decimal::MAX, &Currency::USD));
let mut attempt = ceiling.clone();
attempt += eur(1);
assert!(attempt.checked_add(usd(1)).is_err());
assert_eq!(attempt.balance(&Currency::EUR), eur(1));
}
#[test]
#[should_panic(expected = "addition error: overflow")]
fn addition_panics_on_overflow_test() {
let mut bag = MoneyBag::from(Money::from_decimal(Decimal::MAX, &Currency::USD));
bag += usd(1);
}
#[test]
fn bags_add_and_subtract_test() {
let left: MoneyBag = [usd(10), eur(5)].into_iter().collect();
let right: MoneyBag = [usd(3), eur(5)].into_iter().collect();
assert_eq!(&left + &right, [usd(13), eur(10)].into_iter().collect());
assert_eq!(left - right, MoneyBag::from(usd(7)));
}
#[test]
fn bags_accumulate_in_place_test() {
let mut running: MoneyBag = [usd(10), eur(5)].into_iter().collect();
running += MoneyBag::from(usd(3));
running -= &MoneyBag::from(eur(5));
assert_eq!(running, MoneyBag::from(usd(13)));
}
#[test]
#[allow(clippy::op_ref)]
fn borrowed_operands_test() {
let wallet: MoneyBag = [usd(10), eur(5)].into_iter().collect();
let refund = MoneyBag::from(eur(5));
assert_eq!(&wallet + &refund, [usd(10), eur(10)].into_iter().collect());
assert_eq!(
wallet.clone() + &refund,
[usd(10), eur(10)].into_iter().collect()
);
assert_eq!(
&wallet + refund.clone(),
[usd(10), eur(10)].into_iter().collect()
);
assert_eq!(&wallet - &refund, MoneyBag::from(usd(10)));
}
#[test]
fn sums_mixed_currencies_test() {
let cart = [usd(10), eur(5), usd(2)];
assert_eq!(
cart.iter().sum::<MoneyBag>(),
[usd(12), eur(5)].into_iter().collect()
);
}
#[test]
fn sums_bags_test() {
let accounts = [MoneyBag::from(usd(10)), MoneyBag::from(eur(5))];
assert_eq!(
accounts.into_iter().sum::<MoneyBag>(),
[usd(10), eur(5)].into_iter().collect()
);
}
#[test]
fn zero_money_makes_an_empty_bag_test() {
assert!(MoneyBag::from(usd(0)).is_empty());
}
#[test]
fn empty_bag_renders_as_nothing_test() {
assert_eq!(MoneyBag::new().to_string(), "");
}
#[test]
fn rendering_honors_width_and_alignment_test() {
let bag = MoneyBag::from(usd(10));
assert_eq!(format!("{bag:>14}"), " 10.00 USD");
assert_eq!(
format!("{:*<12}", bag.format_with(&Format::new())),
"10.00 USD***"
);
}
#[test]
fn renders_balances_in_order_test() {
let bag: MoneyBag = [usd(10), eur(5)].into_iter().collect();
assert_eq!(bag.to_string(), "5.00 EUR, 10.00 USD");
}
#[test]
fn into_iterator_yields_balances_test() {
let bag: MoneyBag = [usd(10), eur(5)].into_iter().collect();
let owned: Vec<Money> = bag.clone().into_iter().collect();
let borrowed: Vec<Money> = (&bag).into_iter().collect();
assert_eq!(owned, vec![eur(5), usd(10)]);
assert_eq!(owned, borrowed);
}
#[test]
fn balances_count_and_reverse_without_walking_forward_test() {
let bag: MoneyBag = [usd(10), eur(5)].into_iter().collect();
assert_eq!(bag.iter().len(), 2);
assert_eq!(bag.iter().rev().collect::<Vec<_>>(), vec![usd(10), eur(5)]);
assert_eq!(
bag.into_iter().rev().collect::<Vec<_>>(),
vec![usd(10), eur(5)]
);
}
}