1use std::collections::{HashMap, HashSet};
4
5use super::account::{Account, AccountAggregate, AccountTreeKey};
6use super::balance::Balance;
7use super::context::ReportContext;
8use super::eval::Amount;
9use super::query::QueryError;
10
11#[derive(Debug)]
13pub struct BalanceTree<'ctx> {
14 values: Vec<BalanceTreeNode<'ctx>>,
17}
18
19impl<'ctx> BalanceTree<'ctx> {
20 pub fn create(ctx: &ReportContext<'ctx>, balance: Balance<'ctx>) -> Result<Self, QueryError> {
21 let mut values: Vec<BalanceTreeNode<'ctx>> = vec![BalanceTreeNode::placeholder(
22 AccountTreeKey::Root,
23 0,
24 Amount::zero(),
25 )];
26 let mut required: HashSet<AccountAggregate<'ctx>> = HashSet::new();
29 for (account, _) in balance.iter() {
30 let mut account = (*account).into();
31 required.insert(account);
32 while let Some(parent) = parent_of(ctx, account)? {
33 if !required.insert(parent) {
34 break;
36 }
37 account = parent;
38 }
39 }
40 accumulate(&mut balance.into_map(), &mut values, ctx, &required, 0)?;
41 Ok(Self { values })
42 }
43
44 pub fn nodes(&self) -> &[BalanceTreeNode<'ctx>] {
50 &self.values
51 }
52
53 pub fn into_nodes(self) -> Vec<BalanceTreeNode<'ctx>> {
56 self.values
57 }
58}
59
60fn parent_of<'ctx>(
61 ctx: &ReportContext<'ctx>,
62 account: AccountAggregate<'ctx>,
63) -> Result<Option<AccountAggregate<'ctx>>, QueryError> {
64 let parent = ctx.account_tree.parent(account).ok_or_else(|| {
65 QueryError::Internal(format!(
66 "account {} isn't registered in the AccountTree",
67 account.as_str()
68 ))
69 })?;
70 Ok(match parent {
71 AccountTreeKey::Root => None,
72 AccountTreeKey::Descendant(parent) => Some(parent),
73 })
74}
75
76fn label(account: AccountTreeKey<'_>) -> &str {
77 account
78 .as_aggregate()
79 .map(|x| x.as_str())
80 .unwrap_or_default()
81}
82
83fn accumulate<'ctx>(
84 balance: &mut HashMap<Account<'ctx>, Amount<'ctx>>,
85 values: &mut Vec<BalanceTreeNode<'ctx>>,
86 ctx: &ReportContext<'ctx>,
87 required: &HashSet<AccountAggregate<'ctx>>,
88 i: usize,
89) -> Result<(Amount<'ctx>, NodeRange), QueryError> {
90 debug_assert!(i < values.len());
91 let account = values[i].account;
92 let depth = values[i].depth;
93 let mut total = values[i].self_amount.clone();
94 let mut children: Vec<NodeRange> = Vec::new();
95 for account in ctx.account_tree.children(account).ok_or_else(|| {
96 QueryError::Internal(format!(
97 "account {} isn't registered in the AccountTree",
98 label(account)
99 ))
100 })? {
101 if !required.contains(account) {
102 continue;
103 }
104 let j = values.len();
105 let amount = match account {
106 AccountAggregate::Account(account) => balance.remove(account).unwrap_or_default(),
107 AccountAggregate::Ancestor(_) => Amount::default(),
108 };
109 values.push(BalanceTreeNode::placeholder(
110 (*account).into(),
111 depth + 1,
112 amount,
113 ));
114 let (child_amount, child_range) = accumulate(balance, values, ctx, required, j)?;
115 total += child_amount;
116 children.push(child_range);
117 }
118 let cover = NodeRange::cover(i, &children)?;
119 total.remove_zero_entries();
120 values[i].subtree_amount = total.clone();
121 values[i].children = children;
122 values[i].range = cover;
123 Ok((total, cover))
124}
125
126#[derive(Debug, PartialEq, Eq)]
128pub struct BalanceTreeNode<'ctx> {
129 pub account: AccountTreeKey<'ctx>,
131 pub depth: u16,
133 pub self_amount: Amount<'ctx>,
135 pub subtree_amount: Amount<'ctx>,
137 children: Vec<NodeRange>,
139 range: NodeRange,
141}
142
143impl<'ctx> BalanceTreeNode<'ctx> {
144 fn placeholder(account: AccountTreeKey<'ctx>, depth: u16, self_amount: Amount<'ctx>) -> Self {
146 Self {
147 account,
148 depth,
149 self_amount,
150 subtree_amount: Amount::zero(),
152 children: Vec::new(),
153 range: NodeRange { start: 0, size: 0 },
154 }
155 }
156
157 pub fn has_children(&self) -> bool {
159 !self.children.is_empty()
160 }
161
162 pub fn subtree_range(&self) -> std::ops::Range<usize> {
167 self.range.into()
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173struct NodeRange {
174 pub start: usize,
175 pub size: usize,
176}
177
178impl NodeRange {
179 fn cover(i: usize, ranges: &[NodeRange]) -> Result<Self, QueryError> {
182 let start = i;
183 let mut size = 1;
184 for range in ranges {
185 if start + size != range.start {
186 return Err(QueryError::Internal(format!(
187 "NodeRange::cover can only cover continuous and mutually exclusive ranges: total start={start}, size={size}, current start={}",
188 range.start
189 )));
190 }
191 size += range.size;
192 }
193 Ok(Self { start, size })
194 }
195}
196
197impl From<std::ops::Range<usize>> for NodeRange {
198 fn from(value: std::ops::Range<usize>) -> Self {
199 Self {
200 start: value.start,
201 size: value.end.checked_sub(value.start).unwrap(),
202 }
203 }
204}
205
206impl From<NodeRange> for std::ops::Range<usize> {
207 fn from(value: NodeRange) -> Self {
208 Self {
209 start: value.start,
210 end: value.start + value.size,
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 use assert_matches::assert_matches;
220 use bumpalo::Bump;
221 use maplit::hashmap;
222 use pretty_assertions::assert_eq;
223 use rust_decimal::Decimal;
224 use rust_decimal_macros::dec;
225
226 #[test]
227 fn construct_fails_on_unknown_account() {
228 let arena = Bump::new();
229 let mut ctx = ReportContext::new(&arena);
230 let usd = ctx.commodities.ensure("USD");
231
232 ctx.account_tree.construct(&ctx.accounts);
235 let balance = Balance::from_iter(hashmap! {
236 ctx.accounts.ensure("Assets:Bank") => Amount::from_value(usd, dec!(1)),
237 ctx.accounts.ensure("Assets:Bank:Checking") => Amount::from_value(usd, dec!(3)),
238 });
239
240 let got_err = BalanceTree::create(&ctx, balance).unwrap_err();
241 assert_matches!(got_err, QueryError::Internal(msg) if msg.contains("isn't registered in the AccountTree"));
242 }
243
244 #[test]
245 fn simple_tree_for_non_overlapping_balance() {
246 let arena = Bump::new();
247 let mut ctx = ReportContext::new(&arena);
248 let chf = ctx.commodities.ensure("CHF");
249 let jpy = ctx.commodities.ensure("JPY");
250 let usd = ctx.commodities.ensure("USD");
251
252 let balance = Balance::from_iter(hashmap! {
253 ctx.accounts.ensure("Assets") =>
254 Amount::from_iter([(chf, dec!(10)), (usd, dec!(1))]),
255 ctx.accounts.ensure("Expenses") =>
256 Amount::from_value(jpy, Decimal::ZERO),
257 });
258 ctx.account_tree.construct(&ctx.accounts);
259
260 let tree = BalanceTree::create(&ctx, balance).unwrap();
261 let got = tree.into_nodes();
262 let want = vec![
263 BalanceTreeNode {
264 account: AccountTreeKey::Root,
265 depth: 0,
266 self_amount: Amount::zero(),
267 subtree_amount: Amount::from_iter([(chf, dec!(10)), (usd, dec!(1))]),
268 children: vec![(1..2).into(), (2..3).into()],
269 range: (0..3).into(),
270 },
271 BalanceTreeNode {
272 account: ctx.accounts.resolve("Assets").unwrap().into(),
273 depth: 1,
274 self_amount: Amount::from_iter([(chf, dec!(10)), (usd, dec!(1))]),
275 subtree_amount: Amount::from_iter([(chf, dec!(10)), (usd, dec!(1))]),
276 children: vec![],
277 range: (1..2).into(),
278 },
279 BalanceTreeNode {
280 account: ctx.accounts.resolve("Expenses").unwrap().into(),
281 depth: 1,
282 self_amount: Amount::zero(),
283 subtree_amount: Amount::zero(),
284 children: vec![],
285 range: (2..3).into(),
286 },
287 ];
288 assert_eq!(want, got);
289
290 assert_eq!(2..3, got[2].subtree_range());
291 assert!(got[0].has_children());
292 assert!(!got[2].has_children());
293 }
294
295 fn account_key<'a>(ctx: &ReportContext<'a>, name: &str) -> AccountTreeKey<'a> {
298 if let Some(account) = ctx.accounts.resolve(name) {
299 account.into()
300 } else {
301 ctx.account_tree
302 .resolve_ancestor(name)
303 .expect("account or prefix")
304 .into()
305 }
306 }
307
308 #[test]
309 fn tree_with_overlapping_and_ancestor_accounts() {
310 let arena = Bump::new();
311 let mut ctx = ReportContext::new(&arena);
312 let usd = ctx.commodities.ensure("USD");
313
314 let balance = Balance::from_iter(hashmap! {
315 ctx.accounts.ensure("Assets:Bank") => Amount::from_value(usd, dec!(1)),
316 ctx.accounts.ensure("Assets:Bank:Checking") => Amount::from_value(usd, dec!(3)),
317 ctx.accounts.ensure("Assets:Cash") => Amount::from_value(usd, dec!(5)),
318 ctx.accounts.ensure("Expenses:Property") => Amount::from_value(usd, dec!(1_000)),
319 ctx.accounts.ensure("Expenses:Property:Maintenance") => Amount::from_value(usd, dec!(23)),
320 ctx.accounts.ensure("Expenses:Property Tax") => Amount::from_value(usd, dec!(13)),
321 ctx.accounts.ensure("Expenses:Travel:Taxi") => Amount::from_value(usd, dec!(11)),
322 ctx.accounts.ensure("Expenses:Travel:Train") => Amount::from_value(usd, dec!(-11)),
324 });
325 ctx.accounts.ensure("Income:Salary");
327 ctx.account_tree.construct(&ctx.accounts);
328
329 let amount = |v| Amount::from_value(usd, v);
330 let key = |name: &str| account_key(&ctx, name);
331
332 let tree = BalanceTree::create(&ctx, balance).unwrap();
333 let got = tree.into_nodes();
334 let want = vec![
335 BalanceTreeNode {
337 account: AccountTreeKey::Root,
338 depth: 0,
339 self_amount: Amount::zero(),
340 subtree_amount: amount(dec!(1045)),
341 children: vec![(1..5).into(), (5..12).into()],
342 range: (0..12).into(),
343 },
344 BalanceTreeNode {
346 account: key("Assets"),
347 depth: 1,
348 self_amount: Amount::zero(),
349 subtree_amount: amount(dec!(9)),
350 children: vec![(2..4).into(), (4..5).into()],
351 range: (1..5).into(),
352 },
353 BalanceTreeNode {
355 account: key("Assets:Bank"),
356 depth: 2,
357 self_amount: amount(dec!(1)),
358 subtree_amount: amount(dec!(4)),
359 children: vec![(3..4).into()],
360 range: (2..4).into(),
361 },
362 BalanceTreeNode {
364 account: key("Assets:Bank:Checking"),
365 depth: 3,
366 self_amount: amount(dec!(3)),
367 subtree_amount: amount(dec!(3)),
368 children: vec![],
369 range: (3..4).into(),
370 },
371 BalanceTreeNode {
373 account: key("Assets:Cash"),
374 depth: 2,
375 self_amount: amount(dec!(5)),
376 subtree_amount: amount(dec!(5)),
377 children: vec![],
378 range: (4..5).into(),
379 },
380 BalanceTreeNode {
382 account: key("Expenses"),
383 depth: 1,
384 self_amount: Amount::zero(),
385 subtree_amount: amount(dec!(1036)),
386 children: vec![(6..8).into(), (8..9).into(), (9..12).into()],
387 range: (5..12).into(),
388 },
389 BalanceTreeNode {
391 account: key("Expenses:Property"),
392 depth: 2,
393 self_amount: amount(dec!(1_000)),
394 subtree_amount: amount(dec!(1_023)),
395 children: vec![(7..8).into()],
396 range: (6..8).into(),
397 },
398 BalanceTreeNode {
401 account: key("Expenses:Property:Maintenance"),
402 depth: 3,
403 self_amount: amount(dec!(23)),
404 subtree_amount: amount(dec!(23)),
405 children: vec![],
406 range: (7..8).into(),
407 },
408 BalanceTreeNode {
410 account: key("Expenses:Property Tax"),
411 depth: 2,
412 self_amount: amount(dec!(13)),
413 subtree_amount: amount(dec!(13)),
414 children: vec![],
415 range: (8..9).into(),
416 },
417 BalanceTreeNode {
419 account: key("Expenses:Travel"),
420 depth: 2,
421 self_amount: Amount::zero(),
422 subtree_amount: Amount::zero(),
423 children: vec![(10..11).into(), (11..12).into()],
424 range: (9..12).into(),
425 },
426 BalanceTreeNode {
428 account: key("Expenses:Travel:Taxi"),
429 depth: 3,
430 self_amount: amount(dec!(11)),
431 subtree_amount: amount(dec!(11)),
432 children: vec![],
433 range: (10..11).into(),
434 },
435 BalanceTreeNode {
437 account: key("Expenses:Travel:Train"),
438 depth: 3,
439 self_amount: amount(dec!(-11)),
440 subtree_amount: amount(dec!(-11)),
441 children: vec![],
442 range: (11..12).into(),
443 },
444 ];
445 assert_eq!(want, got);
446 }
447}