use crate::model::Commodity;
use crate::model::{AccountTreeNode, Posts};
use rust_decimal::Decimal;
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Posting {
pub acctn: AccountTreeNode,
pub amount: Decimal,
pub txn_amount: Decimal,
pub is_total_amount: bool,
pub txn_commodity: Option<Commodity>,
pub comment: Option<String>,
pub atn_key: String,
}
impl Posting {
pub(crate) fn from(
acctn: AccountTreeNode,
amount: Decimal,
txn_amount: Decimal,
is_total_amount: bool,
txn_commodity: Option<Commodity>,
comment: Option<String>,
) -> Result<Posting, Box<dyn Error>> {
if amount.is_zero() {
let msg = format!("Zero sum postings are not allowed: {}", acctn.account);
return Err(msg.into());
}
let atn_key = acctn.get_full();
Ok(Posting {
acctn,
amount,
txn_amount,
is_total_amount,
txn_commodity,
comment,
atn_key,
})
}
}
pub fn txn_sum(posts: &Posts) -> Decimal {
posts.iter().map(|p| p.txn_amount).sum()
}
impl Display for Posting {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let sign_space = if self.amount.is_sign_negative() {
""
} else {
" "
};
write!(
f,
"{} {}{}{}{}{}",
self.acctn,
sign_space,
self.amount,
self.acctn
.commodity
.as_ref()
.map(|c| format!(" {}", c.name))
.unwrap_or_default(),
self.txn_commodity
.as_ref()
.map(|txn_c| {
#[allow(clippy::collapsible_else_if)]
if txn_c.name == self.acctn.commodity_str {
String::default()
} else {
if self.is_total_amount {
format!(" = {} {}", self.txn_amount, txn_c.name)
} else {
format!(" @ {} {}", (self.txn_amount / self.amount), txn_c.name)
}
}
})
.unwrap_or_default(),
self.comment
.as_ref()
.map(|c| format!(" ; {c}"))
.unwrap_or_default()
)
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn id_42ad9d32_64aa_4fcd_a4ab_1e8521b921e3__reject_zero_posting() {
{
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(
acctn,
Decimal::new(0, 0),
Decimal::new(0, 0),
false,
None,
None,
);
assert!(p.is_err());
}
{
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(
acctn,
Decimal::new(0, 28),
Decimal::new(0, 28),
false,
None,
None,
);
assert!(p.is_err());
}
}
#[test]
fn id_e3c97b66_318c_4396_8857_0cd2c1dfb0d2__preserve_precision_1E20() {
let v_str =
"12345678901234567890.123456789";
let ref_str = format!("a:b {}", v_str);
let v = Decimal::from_str_exact(v_str).unwrap();
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(acctn, v, v, false, None, None).unwrap();
let p_str = format!("{}", p);
assert_eq!(p_str, ref_str);
assert_eq!(p.to_string(), ref_str);
}
#[test]
fn id_26da0769_de5f_4344_b1d4_d3ddbf3f7f5a__preserve_precision_1E15() {
let v_str =
"678901234567890.12345678901234";
let ref_str = format!("a:b {}", v_str);
let v = Decimal::from_str_exact(v_str).unwrap();
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(acctn, v, v, false, None, None).unwrap();
let p_str = format!("{}", p);
assert_eq!(p_str, ref_str);
assert_eq!(p.to_string(), ref_str);
}
#[test]
fn id_6ce68af4_5349_44e0_8fbc_35bebd8ac1ac__display() {
let v = Decimal::new(12301, 2);
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p =
Posting::from(acctn, v, v, false, None, Some("comment".to_string())).unwrap();
let p_str = format!("{}", p);
assert_eq!(p_str, "a:b 123.01 ; comment");
}
#[test]
fn id_16b54e8c_5ea6_420c_bd72_157dbcc06a49__unit_price() {
let pv = Decimal::new(12300, 2);
let tv = Decimal::new(24600, 2);
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(
acctn,
pv,
tv,
false,
Some(Commodity {
name: "€".to_string(),
}),
None,
)
.unwrap();
assert_eq!(p.to_string(), "a:b 123.00 @ 2 €");
}
#[test]
fn id_22059d1d_7c10_42dc_831f_03bd1f1d6257__unit_price_w_comment() {
let pv = Decimal::new(12300, 2);
let tv = Decimal::new(24600, 2);
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(
acctn,
pv,
tv,
false,
Some(Commodity {
name: "€".to_string(),
}),
Some("comment".to_string()),
)
.unwrap();
assert_eq!(p.to_string(), "a:b 123.00 @ 2 € ; comment");
}
#[test]
fn id_0fef204a_19da_418f_b7d0_86b5211c2182__total_price() {
let pv = Decimal::new(12300, 2);
let tv = Decimal::new(24600, 2);
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(
acctn,
pv,
tv,
true,
Some(Commodity {
name: "€".to_string(),
}),
None,
)
.unwrap();
assert_eq!(p.to_string(), "a:b 123.00 = 246.00 €");
}
#[test]
fn id_718dd25c_aebc_4f29_9903_67942c6ba531__total_price_w_comment() {
let pv = Decimal::new(12300, 2);
let tv = Decimal::new(24600, 2);
let acctn = AccountTreeNode::from("a:b".to_string(), None).unwrap();
let p = Posting::from(
acctn,
pv,
tv,
true,
Some(Commodity {
name: "€".to_string(),
}),
Some("comment".to_string()),
)
.unwrap();
assert_eq!(p.to_string(), "a:b 123.00 = 246.00 € ; comment");
}
}