use std::io;
use crate::resolution::{Entry, HIR};
pub fn write(hir: &HIR, writer: &mut dyn io::Write) -> io::Result<()> {
let mut first = true;
for price in &hir.prices {
if !first {
writeln!(writer)?;
}
first = false;
let price_str = format_beancount_amount(&price.price);
writeln!(
writer,
"{} price {} {}",
price.date, price.commodity, price_str
)?;
}
for entry in &hir.entries {
match &entry.data {
Entry::Transaction(txn) => {
if !first {
writeln!(writer)?;
}
first = false;
write_transaction(txn, writer)?;
}
Entry::Assertion(a) => {
if !first {
writeln!(writer)?;
}
first = false;
let amount_str = format_beancount_amount(&a.amount);
writeln!(writer, "{} balance {} {}", a.date, a.account, amount_str)?;
}
Entry::Pad(p) => {
if !first {
writeln!(writer)?;
}
first = false;
writeln!(
writer,
"{} pad {} {}",
p.date, p.target_account, p.source_account
)?;
}
}
}
Ok(())
}
fn format_beancount_amount(expr: &crate::ast::ValueExpr) -> String {
use crate::ast::ValueExpr;
match expr {
ValueExpr::Amount { value, commodity } => {
if let Some(c) = commodity {
format!("{value} {c}")
} else {
format!("{value}")
}
}
other => format!("{other}"),
}
}
fn beancount_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn write_transaction(
txn: &crate::resolution::Transaction,
writer: &mut dyn io::Write,
) -> io::Result<()> {
let flag = match txn.state {
crate::ast::TransactionState::Cleared => "*",
crate::ast::TransactionState::Pending => "!",
crate::ast::TransactionState::Uncleared => "txn",
};
let tags_str: String = txn
.tags
.iter()
.map(|t| {
if t.starts_with('^') {
format!(" {t}")
} else {
format!(" #{t}")
}
})
.collect();
let escaped_desc = beancount_escape(&txn.description);
writeln!(
writer,
"{} {} \"{}\"{}",
txn.date, flag, escaped_desc, tags_str
)?;
if let Some(sec) = txn.secondary_date {
writeln!(writer, " ; [ledger] secondary-date {sec}")?;
}
if let Some(ref code) = txn.code {
writeln!(writer, " ; [ledger] code ({code})")?;
}
for (key, value) in &txn.metadata {
let escaped = beancount_escape(value);
writeln!(writer, " {key}: \"{escaped}\"")?;
}
for comment in &txn.comments {
writeln!(writer, " ; {comment}")?;
}
for posting in &txn.postings {
write_posting(posting, writer)?;
}
Ok(())
}
fn write_posting(
posting: &crate::resolution::Posting,
writer: &mut dyn io::Write,
) -> io::Result<()> {
match posting.kind {
crate::ast::PostingKind::Real => {}
crate::ast::PostingKind::VirtualUnbalanced => {
writeln!(writer, " ; [ledger] virtual-unbalanced posting follows")?;
}
crate::ast::PostingKind::VirtualBalanced => {
writeln!(writer, " ; [ledger] virtual-balanced posting follows")?;
}
}
let flag = match posting.state {
crate::ast::TransactionState::Uncleared => None,
crate::ast::TransactionState::Pending => Some("!"),
crate::ast::TransactionState::Cleared => Some("*"),
};
if let Some(f) = flag {
write!(writer, " {f} {}", posting.account)?;
} else {
write!(writer, " {}", posting.account)?;
}
if let Some(ref amount) = posting.amount {
let amount_str = format_beancount_posting_amount(amount);
write!(writer, " {amount_str}")?;
}
writeln!(writer)?;
for (key, value) in &posting.metadata {
let escaped = beancount_escape(value);
writeln!(writer, " {key}: \"{escaped}\"")?;
}
for comment in &posting.comments {
writeln!(writer, " ; {comment}")?;
}
Ok(())
}
fn format_beancount_posting_amount(amount: &crate::ast::AmountDetails) -> String {
use crate::ast::{AmountDetails, LotPricing};
#[allow(unreachable_patterns)]
match amount {
AmountDetails::Amount {
value,
lot_annotation,
lot_pricing,
balance_assertion,
} => {
let mut s = format_beancount_amount(value);
if let Some(ann) = lot_annotation {
let mut parts = Vec::new();
if let Some(cost) = &ann.cost {
parts.push(format_beancount_amount(cost));
}
if let Some(date) = ann.date {
parts.push(format!("{date}"));
}
if let Some(note) = &ann.note {
let escaped = beancount_escape(note);
parts.push(format!("\"{escaped}\""));
}
if !parts.is_empty() {
s.push_str(" {");
s.push_str(&parts.join(", "));
s.push('}');
}
}
if let Some(pricing) = lot_pricing {
match pricing {
LotPricing::Unit(p) => {
s.push_str(&format!(" @ {}", format_beancount_amount(p)));
}
LotPricing::Total(p) => {
s.push_str(&format!(" @@ {}", format_beancount_amount(p)));
}
}
}
if let Some(ba) = balance_assertion {
s.push_str(&format!(" ; [hledger] = {}", format_beancount_amount(ba)));
}
s
}
AmountDetails::BalanceAssignment(target) => {
format!("; [hledger] = {}", format_beancount_amount(target))
}
AmountDetails::BalanceAssignmentAllCommodities(target) => {
format!("; [hledger] ==* {}", format_beancount_amount(target))
}
_ => String::new(),
}
}