use core::fmt;
use std::collections::BTreeMap;
use super::balances::TrialBalance;
use crate::error::{OverflowContext, QueryError};
use crate::model::{AccountHandle, AccountKind, AccountPath, Balance, Currency, Layer};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct RollupNode<const P: u8 = 2> {
pub path: AccountPath,
pub account: Option<AccountHandle>,
pub kind: Option<AccountKind>,
pub own: Balance<P>,
pub subtree: Balance<P>,
}
impl<const P: u8> RollupNode<P> {
#[must_use]
pub fn depth(&self) -> usize {
self.path.depth()
}
#[must_use]
pub const fn is_registered(&self) -> bool {
self.account.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rollup<const P: u8 = 2> {
currency: Currency,
exponent: u8,
layer: Layer,
nodes: Vec<RollupNode<P>>,
}
impl<const P: u8> Rollup<P> {
pub fn of(trial: &TrialBalance<P>, currency: Currency) -> Result<Self, QueryError> {
let overflow = |path: &AccountPath| QueryError::Overflow {
context: OverflowContext::Subtree(path.clone()),
};
let mut own: BTreeMap<AccountPath, Balance<P>> = BTreeMap::new();
for row in trial.rows().iter().filter(|row| row.currency == currency) {
own.insert(row.path.clone(), row.balance);
}
let mut subtree: BTreeMap<AccountPath, Balance<P>> = BTreeMap::new();
for (path, balance) in &own {
let mut node = Some(path.clone());
while let Some(path) = node {
let total = subtree.entry(path.clone()).or_insert(Balance::ZERO);
*total = total.checked_add(balance).map_err(|_| overflow(&path))?;
node = path.parent();
}
}
let nodes = subtree
.into_iter()
.map(|(path, subtree)| {
let registered = trial.accounts().get(&path);
RollupNode {
account: registered.map(|(handle, _)| *handle),
kind: registered.map(|(_, kind)| *kind),
own: own.get(&path).copied().unwrap_or(Balance::ZERO),
subtree,
path,
}
})
.collect();
Ok(Self {
currency,
exponent: trial.exponent(currency).unwrap_or(P),
layer: trial.layer(),
nodes,
})
}
#[must_use]
pub const fn currency(&self) -> Currency {
self.currency
}
#[must_use]
pub const fn layer(&self) -> Layer {
self.layer
}
#[must_use]
pub fn nodes(&self) -> &[RollupNode<P>] {
&self.nodes
}
#[must_use]
pub fn get(&self, path: &AccountPath) -> Option<&RollupNode<P>> {
self.nodes.iter().find(|node| node.path == *path)
}
pub fn roots(&self) -> impl Iterator<Item = &RollupNode<P>> {
self.nodes.iter().filter(|node| node.depth() == 1)
}
#[must_use]
pub fn to_depth(&self, depth: usize) -> Self {
Self {
currency: self.currency,
exponent: self.exponent,
layer: self.layer,
nodes: self
.nodes
.iter()
.filter(|node| node.depth() <= depth)
.cloned()
.collect(),
}
}
pub fn total(&self) -> Result<Balance<P>, QueryError> {
self.roots().try_fold(Balance::ZERO, |total, root| {
total
.checked_add(&root.subtree)
.map_err(|_| QueryError::Overflow {
context: OverflowContext::Subtree(root.path.clone()),
})
})
}
}
impl<const P: u8> fmt::Display for Rollup<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lines: Vec<(String, String)> = self
.nodes
.iter()
.map(|node| {
let indent = " ".repeat(node.depth().saturating_sub(1));
let name = node.path.segments().last().unwrap_or_default();
(
format!("{indent}{name}"),
node.subtree.net().display_at(self.exponent).to_string(),
)
})
.collect();
let names = lines.iter().map(|(name, _)| name.chars().count()).max();
let amounts = lines.iter().map(|(_, amount)| amount.len()).max();
for (name, amount) in &lines {
let gap = names
.unwrap_or(0)
.saturating_sub(name.chars().count())
.saturating_add(2)
.saturating_add(amounts.unwrap_or(0).saturating_sub(amount.len()));
writeln!(f, "{name}{}{amount}", " ".repeat(gap))?;
}
Ok(())
}
}