use core::fmt;
use std::collections::BTreeMap;
use super::accounts::AccountBook;
use super::query::{DateBasis, Selection};
use crate::core::BalanceKey;
use crate::error::{IntegrityError, IntegrityKind, OverflowContext, QueryError};
use crate::model::{AccountHandle, AccountKind, AccountPath, Balance, Currency, Label, Layer};
use crate::store::PostingLine;
pub(crate) const fn inconsistent(position: u64) -> IntegrityError {
IntegrityError::new(IntegrityKind::BalanceMismatch, Some(position))
}
#[derive(Debug)]
pub(crate) struct BalanceFold<'a, const P: u8> {
selection: &'a Selection,
balance: Balance<P>,
}
impl<'a, const P: u8> BalanceFold<'a, P> {
pub(crate) const fn new(selection: &'a Selection) -> Self {
Self {
selection,
balance: Balance::ZERO,
}
}
pub(crate) fn feed(&mut self, line: &PostingLine<P>) -> Result<(), IntegrityError> {
if self.selection.selects(line) {
self.balance = self
.balance
.post(line.side, line.amount)
.map_err(|_| inconsistent(line.position))?;
}
Ok(())
}
pub(crate) const fn finish(self) -> Balance<P> {
self.balance
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct TrialBalanceRow<const P: u8 = 2> {
pub account: AccountHandle,
pub path: AccountPath,
pub kind: AccountKind,
pub class: Option<Label>,
pub currency: Currency,
pub balance: Balance<P>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrialBalance<const P: u8 = 2> {
layer: Layer,
basis: DateBasis,
filtered: bool,
rows: Vec<TrialBalanceRow<P>>,
exponents: BTreeMap<Currency, u8>,
accounts: BTreeMap<AccountPath, (AccountHandle, AccountKind)>,
}
impl<const P: u8> TrialBalance<P> {
pub(crate) fn of(
selection: &Selection,
balances: impl IntoIterator<Item = (BalanceKey, Balance<P>)>,
accounts: &AccountBook,
exponents: BTreeMap<Currency, u8>,
) -> Result<Self, IntegrityError> {
let mut rows = BTreeMap::new();
for (key, balance) in balances {
if key.layer != selection.layer() || balance.is_zero() {
continue;
}
let account = accounts
.get(key.handle)
.ok_or_else(|| IntegrityError::new(IntegrityKind::MissingRecord, None))?;
rows.insert(
(account.path().clone(), key.currency),
TrialBalanceRow {
account: key.handle,
path: account.path().clone(),
kind: account.kind(),
class: account.class().cloned(),
currency: key.currency,
balance,
},
);
}
Ok(Self {
layer: selection.layer(),
basis: selection.basis(),
filtered: selection.is_filtered(),
rows: rows.into_values().collect(),
exponents,
accounts: accounts.by_path(),
})
}
#[must_use]
pub fn rows(&self) -> &[TrialBalanceRow<P>] {
&self.rows
}
#[must_use]
pub fn get(&self, path: &AccountPath, currency: Currency) -> Option<&TrialBalanceRow<P>> {
self.rows
.iter()
.find(|row| row.path == *path && row.currency == currency)
}
#[must_use]
pub const fn layer(&self) -> Layer {
self.layer
}
#[must_use]
pub const fn basis(&self) -> DateBasis {
self.basis
}
#[must_use]
pub const fn is_filtered(&self) -> bool {
self.filtered
}
#[must_use]
pub fn exponent(&self, currency: Currency) -> Option<u8> {
self.exponents.get(¤cy).copied()
}
fn show(&self, currency: Currency, amount: crate::model::Amount<P>) -> String {
amount
.display_at(self.exponent(currency).unwrap_or(P))
.to_string()
}
pub fn currencies(&self) -> impl Iterator<Item = Currency> + '_ {
let mut currencies: Vec<Currency> = self.rows.iter().map(|row| row.currency).collect();
currencies.sort_unstable();
currencies.dedup();
currencies.into_iter()
}
pub fn totals(&self, currency: Currency) -> Result<Balance<P>, QueryError> {
self.rows
.iter()
.filter(|row| row.currency == currency)
.try_fold(Balance::ZERO, |total, row| total.checked_add(&row.balance))
.map_err(|_| QueryError::Overflow {
context: OverflowContext::Totals(currency),
})
}
#[must_use]
pub fn is_balanced(&self) -> bool {
self.currencies().all(|currency| {
self.totals(currency)
.is_ok_and(|total| total.debit() == total.credit())
})
}
pub(crate) const fn accounts(&self) -> &BTreeMap<AccountPath, (AccountHandle, AccountKind)> {
&self.accounts
}
}
impl<const P: u8> fmt::Display for TrialBalance<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table: Vec<[String; 4]> = vec![[
String::from("account"),
String::from("currency"),
String::from("debit"),
String::from("credit"),
]];
for row in &self.rows {
table.push([
row.path.to_string(),
row.currency.to_string(),
self.show(row.currency, row.balance.debit()),
self.show(row.currency, row.balance.credit()),
]);
}
for currency in self.currencies() {
let (debit, credit) = match self.totals(currency) {
Ok(total) => (
self.show(currency, total.debit()),
self.show(currency, total.credit()),
),
Err(_) => (String::from("overflow"), String::from("overflow")),
};
table.push([String::from("total"), currency.to_string(), debit, credit]);
}
let filtered = if self.filtered { ", filtered" } else { "" };
writeln!(
f,
"trial balance, {}, by {}{filtered}",
self.layer, self.basis
)?;
write_table(f, &table, &[false, false, true, true])
}
}
pub(crate) fn write_table<const N: usize>(
f: &mut fmt::Formatter<'_>,
rows: &[[String; N]],
right: &[bool; N],
) -> fmt::Result {
let mut widths = [0usize; N];
for row in rows {
for (width, cell) in widths.iter_mut().zip(row) {
*width = (*width).max(cell.chars().count());
}
}
for row in rows {
let mut line = String::new();
for (column, ((cell, width), right)) in row.iter().zip(widths).zip(right).enumerate() {
if column > 0 {
line.push_str(" ");
}
let missing = width.saturating_sub(cell.chars().count());
if *right {
line.extend(core::iter::repeat_n(' ', missing));
line.push_str(cell);
} else {
line.push_str(cell);
line.extend(core::iter::repeat_n(' ', missing));
}
}
writeln!(f, "{}", line.trim_end())?;
}
Ok(())
}