use super::align::{FormatLine, render_lines};
use super::directives::metadata_lines;
use super::{FormatConfig, format_cost_spec, format_price_annotation};
use crate::{IncompleteAmount, Posting, Transaction};
use std::fmt::Write;
pub fn format_transaction_lines(txn: &Transaction, config: &FormatConfig) -> Vec<FormatLine> {
let mut lines = Vec::new();
let mut header = format!("{} {}", txn.date, txn.flag);
if let Some(payee) = &txn.payee {
write!(header, " \"{}\"", super::escape_string(payee))
.expect("write to String is infallible");
}
write!(header, " \"{}\"", super::escape_string(&txn.narration))
.expect("write to String is infallible");
for tag in &txn.tags {
write!(header, " #{tag}").expect("write to String is infallible");
}
for link in &txn.links {
write!(header, " ^{link}").expect("write to String is infallible");
}
lines.push(FormatLine::Plain(header));
metadata_lines(&txn.meta, &config.indent, &mut lines);
let meta_indent = format!("{}{}", &config.indent, &config.indent);
for posting in &txn.postings {
for comment in &posting.comments {
lines.push(FormatLine::Plain(format!("{}{}", &config.indent, comment)));
}
lines.push(posting_format_line(posting, config));
for trailing in posting.trailing_comments.iter().skip(1) {
lines.push(FormatLine::Plain(format!("{}{}", &config.indent, trailing)));
}
if !posting.meta.is_empty() {
metadata_lines(&posting.meta, &meta_indent, &mut lines);
}
}
for comment in &txn.trailing_comments {
lines.push(FormatLine::Plain(format!("{}{}", &config.indent, comment)));
}
lines
}
#[cfg(test)]
pub(super) fn format_transaction(txn: &Transaction, config: &FormatConfig) -> String {
render_lines(&format_transaction_lines(txn, config), &config.alignment)
}
fn posting_prefix(posting: &Posting, config: &FormatConfig) -> String {
let mut prefix = String::new();
prefix.push_str(&config.indent);
if let Some(flag) = posting.flag {
write!(prefix, "{flag} ").expect("write to String is infallible");
}
prefix.push_str(&posting.account);
prefix
}
fn posting_amount_split(posting: &Posting) -> (Option<String>, String) {
let (number, mut rest) = match &posting.units {
None => (None, String::new()),
Some(IncompleteAmount::Complete(amount)) => {
(Some(amount.number.to_string()), amount.currency.to_string())
}
Some(IncompleteAmount::NumberOnly(n)) => (Some(n.to_string()), String::new()),
Some(IncompleteAmount::CurrencyOnly(c)) => (None, c.to_string()),
};
if let Some(cost) = &posting.cost {
if !rest.is_empty() {
rest.push(' ');
}
rest.push_str(&format_cost_spec(cost));
}
if let Some(price) = &posting.price {
if !rest.is_empty() {
rest.push(' ');
}
rest.push_str(&format_price_annotation(price));
}
(number, rest)
}
#[must_use]
pub fn posting_format_line(posting: &Posting, config: &FormatConfig) -> FormatLine {
build_posting_line(posting, config, true)
}
fn build_posting_line(
posting: &Posting,
config: &FormatConfig,
include_trailing_comment: bool,
) -> FormatLine {
let prefix = posting_prefix(posting, config);
let (number, rest) = posting_amount_split(posting);
let comment = if include_trailing_comment {
posting.trailing_comments.first()
} else {
None
};
if let Some(number) = number {
let mut suffix = rest;
if let Some(c) = comment {
if !suffix.is_empty() {
suffix.push(' ');
}
suffix.push_str(c);
}
FormatLine::Aligned {
prefix,
number,
suffix,
}
} else {
let mut line = prefix;
if !rest.is_empty() {
line.push_str(" ");
line.push_str(&rest);
}
if let Some(c) = comment {
line.push(' ');
line.push_str(c);
}
FormatLine::Plain(line)
}
}
#[must_use]
pub fn format_posting_line(posting: &Posting, config: &FormatConfig) -> String {
render_lines(
&[build_posting_line(posting, config, true)],
&config.alignment,
)
.trim_end_matches('\n')
.to_string()
}
#[cfg(test)]
pub(super) fn format_posting(posting: &Posting, config: &FormatConfig) -> String {
render_lines(
&[build_posting_line(posting, config, false)],
&config.alignment,
)
.trim_end_matches('\n')
.to_string()
}
pub fn format_incomplete_amount(amount: &IncompleteAmount) -> String {
match amount {
IncompleteAmount::Complete(a) => format!("{} {}", a.number, a.currency),
IncompleteAmount::NumberOnly(n) => n.to_string(),
IncompleteAmount::CurrencyOnly(c) => c.to_string(),
}
}