use beancount_types::Amount;
use beancount_types::Balance;
use beancount_types::CostBasis;
use beancount_types::Directive;
use beancount_types::Posting;
use beancount_types::Transaction;
use rust_decimal::Decimal;
use std::io::BufWriter;
use std::io::Result;
use std::io::Write;
// TODO consider reimplementation based on pretty.rs
pub struct PrettyPrinter<
W,
const FLAG_COLUMN: usize,
const ACCOUNT_COLUMN: usize,
const AMOUNT_COLUMN: usize,
> {
inner: W,
}
impl<W, const FLAG_COLUMN: usize, const ACCOUNT_COLUMN: usize, const AMOUNT_COLUMN: usize>
PrettyPrinter<W, FLAG_COLUMN, ACCOUNT_COLUMN, AMOUNT_COLUMN>
where
W: Write,
{
pub fn unbuffered(inner: W) -> Self {
Self { inner }
}
}
impl<W, const FLAG_COLUMN: usize, const ACCOUNT_COLUMN: usize, const AMOUNT_COLUMN: usize>
PrettyPrinter<BufWriter<W>, FLAG_COLUMN, ACCOUNT_COLUMN, AMOUNT_COLUMN>
where
W: Write,
{
pub fn buffered(inner: W) -> Self {
let inner = BufWriter::new(inner);
Self { inner }
}
}
impl<W, const FLAG_COLUMN: usize, const ACCOUNT_COLUMN: usize, const AMOUNT_COLUMN: usize>
PrettyPrinter<W, FLAG_COLUMN, ACCOUNT_COLUMN, AMOUNT_COLUMN>
where
W: Write,
{
pub fn print_balance(&mut self, balance: &Balance) -> Result<()> {
let Balance {
date,
account,
amount,
} = balance;
let account_width = AMOUNT_COLUMN - 19;
write!(self.inner, "{date} balance {account:account_width$}")?;
self.print_amount(amount)
}
pub fn print_directive(&mut self, directive: &Directive) -> Result<()> {
// TODO prevent trailing whitespace
match directive {
Directive::Transaction(transaction) => self.print_transaction(transaction),
Directive::Balance(balance) => self.print_balance(balance),
}
}
pub fn print_directives<'a>(
&mut self,
directives: impl IntoIterator<Item = &'a Directive>,
) -> Result<()> {
directives
.into_iter()
.enumerate()
.try_for_each(|(index, directive)| {
if 0 < index {
// Create empty line between directives
self.inner.write_all(b"\n\n")?;
}
self.print_directive(directive)
})
.and_then(|_| self.inner.flush())
}
pub fn print_transaction(&mut self, transaction: &Transaction) -> Result<()> {
let Transaction {
date,
flag,
payee,
narration,
postings,
} = transaction;
write!(self.inner, "{date} {flag}")?;
match (payee, narration) {
(Some(payee), Some(narration)) => write!(self.inner, r#" "{payee}" "{narration}""#)?,
(Some(payee), None) => write!(self.inner, r#" "{payee}" """#)?,
(None, Some(narration)) => write!(self.inner, r#" "{narration}""#)?,
(None, None) => {}
}
postings
.iter()
.try_for_each(|posting| self.print_posting(posting))
}
}
impl<W, const FLAG_COLUMN: usize, const ACCOUNT_COLUMN: usize, const AMOUNT_COLUMN: usize>
PrettyPrinter<W, FLAG_COLUMN, ACCOUNT_COLUMN, AMOUNT_COLUMN>
where
W: Write,
{
fn print_amount(&mut self, amount: &Amount) -> Result<()> {
let Amount { amount, commodity } = amount;
self.print_decimal_aligned(amount, 15, 4)?;
write!(self.inner, " {commodity:12}")?;
Ok(())
}
fn print_cost(&mut self, cost: &Option<CostBasis>) -> Result<()> {
// TODO align amounts?
if let Some(cost) = cost {
write!(self.inner, " {cost}")?;
}
Ok(())
}
fn print_decimal_aligned(
&mut self,
decimal: &Decimal,
total_width: usize,
decimals: usize,
) -> Result<()> {
// TODO handle this with icu or similar (e.g. for digit grouping)?
let mut buffer = [b' '; 32];
let width = total_width - (decimals - (decimal.scale() as usize));
write!(&mut buffer[..width], "{decimal:width$}")?;
self.inner.write_all(&buffer[..total_width])?;
Ok(())
}
fn print_posting(&mut self, posting: &Posting) -> Result<()> {
let Posting {
flag: _,
account,
amount,
cost,
price,
} = posting;
write!(self.inner, "\n ")?;
let account_width = if amount.is_some() {
AMOUNT_COLUMN - ACCOUNT_COLUMN
} else {
0
};
write!(self.inner, "{account:account_width$}")?;
if let Some(amount) = amount {
self.print_amount(amount)?;
self.print_cost(cost)?;
if let Some(price) = price {
write!(self.inner, " @ {price}")?;
}
}
Ok(())
}
}