use std::{cmp::Ordering, collections::HashMap, env::args, process};
use rust_decimal::Decimal;
use beancount_parser::{
Account, Amount, BeancountFile, Currency, Directive, DirectiveContent, Transaction,
};
type Report = HashMap<Account, HashMap<Currency, Decimal>>;
fn main() {
let directives = match BeancountFile::read_files(args().skip(1).map(Into::into)) {
Ok(file) => file.directives,
Err(err) => {
eprintln!("{err}");
process::exit(1);
}
};
let report = build_report(directives);
print(&report);
}
fn compare_directives<D>(a: &Directive<D>, b: &Directive<D>) -> Ordering {
a.date
.cmp(&b.date)
.then_with(|| match (&a.content, &b.content) {
(DirectiveContent::Balance(_), DirectiveContent::Transaction(_)) => Ordering::Less,
(DirectiveContent::Transaction(_), DirectiveContent::Balance(_)) => Ordering::Greater,
_ => Ordering::Equal,
})
}
fn build_report(mut directives: Vec<Directive<Decimal>>) -> Report {
directives.sort_by(compare_directives);
let mut report = Report::new();
for directive in directives {
match directive.content {
DirectiveContent::Transaction(trx) => add_trx(&mut report, trx),
DirectiveContent::Balance(bal) => set_balance(&mut report, bal.account, bal.amount),
_ => (),
}
}
report
}
fn add_trx(report: &mut Report, transaction: Transaction<Decimal>) {
let source_account = transaction
.postings
.iter()
.find(|p| p.amount.is_none())
.map(|p| p.account.clone());
transaction
.postings
.into_iter()
.filter_map(|p| Some((p.account, p.amount?)))
.for_each(|(account, mut amount)| {
if let Some(ref source_account) = source_account {
add_amount(report, account, amount.clone());
amount.value = -amount.value;
add_amount(report, source_account.clone(), amount);
} else {
add_amount(report, account, amount);
}
});
}
fn add_amount(report: &mut Report, account: Account, amount: Amount<Decimal>) {
let value = report
.entry(account)
.or_default()
.entry(amount.currency)
.or_default();
*value += amount.value;
}
fn set_balance(report: &mut Report, account: Account, amount: Amount<Decimal>) {
let value = report
.entry(account)
.or_default()
.entry(amount.currency)
.or_default();
*value = amount.value;
}
fn print(report: &Report) {
let mut accounts: Vec<_> = report.keys().collect();
accounts.sort();
for account in accounts {
report
.get(account)
.iter()
.flat_map(|a| a.iter())
.for_each(|(currency, value)| {
println!("{account:50} {value:>15.2} {currency}");
});
}
}