//! Balances (SPEC §9.3, §14.5.2). use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// A non-deleted expense as far as balances are concerned. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BalanceExpense { pub account_id: String, pub amount_ore: i64, pub payer_user_id: String, /// user id → share in öre. pub shares: BTreeMap, } /// A non-deleted settlement as far as balances are concerned. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BalanceSettlement { pub from_user_id: String, pub to_user_id: String, /// account id → öre. pub allocations: BTreeMap, } /// Net per account and user, and the global net per user. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Balances { /// account id → user id → net öre. pub per_account: BTreeMap>, /// user id → net öre over every account. pub global: BTreeMap, } /// Computes `net(u)` per account and globally (SPEC §9.3). Inputs hold only non-deleted rows. /// /// `per_account` has an entry for every id of `account_ids` (zeros included) and for any other account id the rows /// reference; each inner map has every id of `user_ids`. Arithmetic saturates instead of overflowing; real data is /// many orders of magnitude below `i64::MAX`. pub fn compute_balances( user_ids: &[String], account_ids: &[String], expenses: &[BalanceExpense], settlements: &[BalanceSettlement], ) -> Balances { let zeros: BTreeMap = user_ids.iter().map(|u| (u.clone(), 0)).collect(); let mut per_account: BTreeMap> = BTreeMap::new(); for account in account_ids { per_account .entry(account.clone()) .or_insert_with(|| zeros.clone()); } for expense in expenses { let nets = per_account .entry(expense.account_id.clone()) .or_insert_with(|| zeros.clone()); add(nets, &expense.payer_user_id, expense.amount_ore); for (user, share) in &expense.shares { add(nets, user, share.saturating_neg()); } } for settlement in settlements { for (account, amount) in &settlement.allocations { let nets = per_account .entry(account.clone()) .or_insert_with(|| zeros.clone()); add(nets, &settlement.from_user_id, *amount); add(nets, &settlement.to_user_id, amount.saturating_neg()); } } let mut global = zeros; for nets in per_account.values() { for (user, net) in nets { add(&mut global, user, *net); } } Balances { per_account, global, } } fn add(nets: &mut BTreeMap, user: &str, delta: i64) { let net = nets.entry(user.to_owned()).or_insert(0); *net = net.saturating_add(delta); }