1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//! Balances report - Show account balances.
use super::{OutputFormat, csv_escape, json_escape};
use anyhow::Result;
use rustledger_core::{Directive, DisplayContext};
use std::io::Write;
/// Generate a balances report.
pub(super) fn report_balances<W: Write>(
directives: &[Directive],
account_filter: Option<&str>,
ctx: &DisplayContext,
format: &OutputFormat,
writer: &mut W,
) -> Result<()> {
// Single source of truth for per-account balances (see
// `super::account_balances`); no report re-derives them itself.
let balances = super::account_balances(directives);
// Collect data for output. `cost` is the beancount-style lot annotation
// (e.g. ` {150.00 USD}`) for held commodities, empty for plain currency.
// Numbers render through the ledger's DisplayContext (per-currency
// inferred precision, `display_precision` overrides, `render_commas`)
// — the same context BQL output uses — so `100 USD` in a 2dp ledger
// prints as `100.00 USD` regardless of booking-arithmetic scale (U4).
let mut rows: Vec<(&str, String, &str, String)> = Vec::new();
for (account, inventory) in &balances {
if let Some(filter) = account_filter
&& !account.starts_with(filter)
{
continue;
}
if inventory.is_empty() {
continue;
}
for position in inventory.positions() {
let cost = position
.cost
.as_ref()
.map(|c| format!("{c}"))
.unwrap_or_default();
rows.push((
account,
ctx.format_amount_number(position.units.number, &position.units.currency),
&position.units.currency,
cost,
));
}
}
match format {
OutputFormat::Csv => {
writeln!(writer, "account,amount,currency,cost")?;
for (account, amount, currency, cost) in &rows {
// `csv_escape(amount)`: with `render_commas` the formatted
// number contains thousands separators (review catch).
writeln!(
writer,
"{},{},{},{}",
csv_escape(account),
csv_escape(amount),
currency,
csv_escape(cost)
)?;
}
}
OutputFormat::Json => {
writeln!(writer, "[")?;
for (i, (account, amount, currency, cost)) in rows.iter().enumerate() {
let comma = if i < rows.len() - 1 { "," } else { "" };
writeln!(
writer,
r#" {{"account": "{}", "amount": "{}", "currency": "{}", "cost": "{}"}}{}"#,
json_escape(account),
amount,
currency,
json_escape(cost),
comma
)?;
}
writeln!(writer, "]")?;
}
OutputFormat::Text => {
writeln!(writer, "Account Balances")?;
writeln!(writer, "{}", "=".repeat(60))?;
writeln!(writer)?;
let mut current_account = "";
for (account, amount, currency, cost) in &rows {
if *account != current_account {
writeln!(writer, "{account}")?;
current_account = account;
}
if cost.is_empty() {
writeln!(writer, " {amount:>15} {currency}")?;
} else {
writeln!(writer, " {amount:>15} {currency} {cost}")?;
}
}
}
}
Ok(())
}