use rust_decimal::Decimal;
use std::io::Write;
use std::path::PathBuf;
use std::{collections::HashMap, io};
use beancount_parser_lima::{
BeancountParser, BeancountSources, Directive, DirectiveVariant, Error, ParseError,
ParseSuccess, Posting, Spanned,
};
fn main() -> io::Result<()> {
let flags = xflags::parse_or_exit! {
required path: PathBuf
};
let stderr = &mut io::stderr();
let sources = BeancountSources::try_from(flags.path)?;
let parser = BeancountParser::new(&sources);
parse(&sources, &parser, stderr);
Ok(())
}
fn parse<W>(sources: &BeancountSources, parser: &BeancountParser, error_w: &mut W)
where
W: Write,
{
match parser.parse() {
Ok(ParseSuccess {
directives,
options: _,
plugins: _,
mut warnings,
}) => {
let mut accounts = HashMap::new();
let mut errors = Vec::new();
for d in directives {
use DirectiveVariant::*;
match d.variant() {
Transaction(x) => {
check_postings_amounts(&d, x.postings(), &mut errors);
check_postings_accounts(&d, x.postings(), &accounts, &mut errors);
}
Open(x) => {
let account_key = x.account().to_string();
match accounts.get(&account_key) {
None => {
accounts.insert(account_key, AccountStatus::opened(d));
}
Some(AccountStatus {
opened,
closed: None,
}) => {
warnings.push(d.warning("duplicate open").related_to(opened));
}
Some(AccountStatus {
closed: Some(closed),
..
}) => {
errors.push(d.error("account was closed").related_to(closed));
}
}
}
Close(x) => {
let account = x.account();
let account_key = account.to_string();
match accounts.get(&account_key) {
None => {
errors.push(account.error("no such account").in_context(&d));
}
Some(AccountStatus { closed: None, .. }) => {
let account_status = accounts.get_mut(&account_key).unwrap();
account_status.closed = Some(d);
}
Some(AccountStatus {
closed: Some(closed),
..
}) => {
errors.push(d.error("account already closed").related_to(closed));
}
}
}
_ => (),
}
}
sources.write_errors_or_warnings(error_w, warnings).unwrap();
if !errors.is_empty() {
sources.write_errors_or_warnings(error_w, errors).unwrap();
}
}
Err(ParseError { errors, warnings }) => {
sources.write_errors_or_warnings(error_w, errors).unwrap();
sources.write_errors_or_warnings(error_w, warnings).unwrap();
}
}
}
struct AccountStatus<'a> {
opened: Spanned<Directive<'a>>,
closed: Option<Spanned<Directive<'a>>>,
}
impl<'a> AccountStatus<'a> {
fn opened(d: Spanned<Directive<'a>>) -> Self {
AccountStatus {
opened: d,
closed: None,
}
}
}
fn check_postings_amounts<'a>(
d: &'a Spanned<Directive<'a>>,
postings: impl ExactSizeIterator<Item = &'a Spanned<Posting<'a>>>,
errors: &mut Vec<Error>,
) {
let n_postings = postings.len();
let mut amounts_with_value = postings.filter_map(|p| p.amount()).collect::<Vec<_>>();
if n_postings > 0 && amounts_with_value.len() == n_postings {
let total: Decimal = amounts_with_value.iter().map(|x| x.value()).sum();
if total != Decimal::ZERO {
let last_amount = amounts_with_value.pop().unwrap();
errors.push(
last_amount
.error(format!("sum is {}, expected zero", total))
.related_to_all(amounts_with_value)
.in_context(d),
)
}
}
}
fn check_postings_accounts<'a>(
d: &'a Spanned<Directive<'a>>,
postings: impl ExactSizeIterator<Item = &'a Spanned<Posting<'a>>>,
accounts: &HashMap<String, AccountStatus>,
errors: &mut Vec<Error>,
) {
for posting in postings {
let account = posting.account();
let account_key = account.to_string();
match accounts.get(&account_key) {
None => {
errors.push(
account
.error("no such account")
.in_context(posting)
.in_context(d),
);
}
Some(AccountStatus {
closed: Some(closed),
..
}) => {
errors.push(
account
.error("account is closed")
.in_context(posting)
.in_context(d)
.related_to(closed),
);
}
_ => (),
}
}
}