use core::fmt;
use time::Date;
use super::balances::{inconsistent, write_table};
use super::query::{DateBasis, Selection};
use crate::error::IntegrityError;
use crate::model::{
AccountHandle, AccountPath, Amount, Balance, Currency, EntryId, Layer, Side, pad,
};
use crate::store::PostingLine;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct StatementLine<const P: u8 = 2> {
pub position: u64,
pub entry: EntryId,
pub posting: u16,
pub booking_date: Date,
pub value_date: Date,
pub side: Side,
pub amount: Amount<P>,
pub running: Balance<P>,
}
impl<const P: u8> fmt::Display for StatementLine<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
pad(
f,
&format!(
"{} {}/{} {} {}, balance {}",
self.booking_date,
self.position,
self.posting,
self.side,
self.amount,
self.running.net()
),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Statement<const P: u8 = 2> {
pub account: AccountHandle,
pub path: AccountPath,
pub currency: Currency,
pub layer: Layer,
pub basis: DateBasis,
pub start: Option<Date>,
pub end: Option<Date>,
pub opening: Balance<P>,
pub lines: Vec<StatementLine<P>>,
pub closing: Balance<P>,
pub exponent: u8,
}
impl<const P: u8> fmt::Display for Statement<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let from = self
.start
.map_or_else(|| String::from("the beginning"), |d| d.to_string());
let to = self
.end
.map_or_else(|| String::from("the head"), |d| d.to_string());
writeln!(
f,
"{} {}, {}, by {}, {from} to {to}",
self.path, self.currency, self.layer, self.basis
)?;
let blank = String::new;
let mut table = vec![[
String::from("opening"),
blank(),
blank(),
blank(),
self.opening.net().display_at(self.exponent).to_string(),
]];
for line in &self.lines {
let date = match self.basis {
DateBasis::Booking => line.booking_date,
DateBasis::Value => line.value_date,
};
table.push([
date.to_string(),
format!("{}/{}", line.position, line.posting),
line.side.to_string(),
line.amount.display_at(self.exponent).to_string(),
line.running.net().display_at(self.exponent).to_string(),
]);
}
table.push([
String::from("closing"),
blank(),
blank(),
blank(),
self.closing.net().display_at(self.exponent).to_string(),
]);
write_table(f, &table, &[false, true, false, true, true])
}
}
#[derive(Debug)]
pub(crate) struct StatementFold<'a, const P: u8> {
selection: &'a Selection,
opening: Balance<P>,
lines: Vec<(Date, PostingLine<P>)>,
}
impl<'a, const P: u8> StatementFold<'a, P> {
pub(crate) const fn new(selection: &'a Selection) -> Self {
Self {
selection,
opening: Balance::ZERO,
lines: Vec::new(),
}
}
pub(crate) fn feed(&mut self, line: PostingLine<P>) -> Result<(), IntegrityError> {
if !self.selection.filters(&line) {
return Ok(());
}
let date = self.selection.date_of(&line);
if self.selection.start().is_some_and(|start| date < start) {
self.opening = self
.opening
.post(line.side, line.amount)
.map_err(|_| inconsistent(line.position))?;
} else if self.selection.holds(date) {
self.lines.push((date, line));
}
Ok(())
}
pub(crate) fn finish(
self,
account: AccountHandle,
path: AccountPath,
currency: Currency,
exponent: u8,
) -> Result<Statement<P>, IntegrityError> {
let mut lines = self.lines;
lines.sort_by_key(|(date, line)| (*date, line.position, line.index));
let mut running = self.opening;
let lines = lines
.into_iter()
.map(|(_, line)| {
running = running
.post(line.side, line.amount)
.map_err(|_| inconsistent(line.position))?;
Ok(StatementLine {
position: line.position,
entry: line.entry,
posting: line.index,
booking_date: line.booking,
value_date: line.value,
side: line.side,
amount: line.amount,
running,
})
})
.collect::<Result<Vec<_>, IntegrityError>>()?;
Ok(Statement {
account,
path,
currency,
exponent,
layer: self.selection.layer(),
basis: self.selection.basis(),
start: self.selection.start(),
end: self.selection.end(),
opening: self.opening,
lines,
closing: running,
})
}
}