use core::fmt;
use time::Date;
use super::query::{AccountName, BalanceQuery, DateBasis};
use crate::model::{AccountRef, Amount, Balance, Currency, Layer, ValueError, pad};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum AssertAt {
EndOfDay {
date: Date,
basis: DateBasis,
},
Position(u64),
}
impl fmt::Display for AssertAt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EndOfDay { date, basis } => write!(f, "at the end of {date} by {basis}"),
Self::Position(position) => write!(f, "after position {position}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BalanceAssertion<const P: u8 = 2> {
account: AccountName,
currency: Currency,
layer: Layer,
at: AssertAt,
expected: Amount<P>,
}
impl<const P: u8> BalanceAssertion<P> {
pub fn new<A>(account: A, currency: Currency, at: AssertAt, expected: Amount<P>) -> Self
where
A: TryInto<AccountRef> + fmt::Display,
A::Error: Into<ValueError>,
{
Self {
account: AccountName::new(account),
currency,
layer: Layer::Settled,
at,
expected,
}
}
#[must_use]
pub fn pending(self) -> Self {
Self {
layer: Layer::Pending,
..self
}
}
#[must_use]
pub fn account(&self) -> &str {
self.account.text()
}
#[must_use]
pub const fn currency(&self) -> Currency {
self.currency
}
#[must_use]
pub const fn layer(&self) -> Layer {
self.layer
}
#[must_use]
pub const fn at(&self) -> AssertAt {
self.at
}
#[must_use]
pub const fn expected(&self) -> Amount<P> {
self.expected
}
pub(crate) const fn name(&self) -> &AccountName {
&self.account
}
pub(crate) fn query(&self) -> BalanceQuery {
let query = match self.at {
AssertAt::EndOfDay { date, basis } => {
let query = BalanceQuery::through(date);
match basis {
DateBasis::Booking => query,
DateBasis::Value => query.by_value_date(),
}
}
AssertAt::Position(position) => BalanceQuery::through_position(position),
};
match self.layer {
Layer::Settled => query,
Layer::Pending => query.pending(),
}
}
#[must_use]
pub fn check(&self, actual: &Balance<P>) -> AssertionOutcome<P> {
let actual = actual.net();
if actual == self.expected {
return AssertionOutcome::Held { actual };
}
let direction = if actual > self.expected {
Deviation::Excess
} else {
Deviation::Shortfall
};
AssertionOutcome::Failed(AssertionFailure {
assertion: self.clone(),
actual,
difference: Difference {
direction,
units: actual.to_minor().abs_diff(self.expected.to_minor()),
},
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Deviation {
Excess,
Shortfall,
}
impl fmt::Display for Deviation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
pad(
f,
match self {
Self::Excess => "excess",
Self::Shortfall => "shortfall",
},
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Difference<const P: u8 = 2> {
pub direction: Deviation,
pub units: u128,
}
impl<const P: u8> fmt::Display for Difference<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let digits = self.units.to_string();
let scale = usize::from(P);
let mut magnitude = "0".repeat(scale.saturating_add(1).saturating_sub(digits.len()));
magnitude.push_str(&digits);
if scale > 0 {
magnitude.insert(magnitude.len().saturating_sub(scale), '.');
}
pad(f, &format!("{} {magnitude}", self.direction))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AssertionOutcome<const P: u8 = 2> {
Held {
actual: Amount<P>,
},
Failed(AssertionFailure<P>),
}
impl<const P: u8> AssertionOutcome<P> {
#[must_use]
pub const fn is_held(&self) -> bool {
matches!(self, Self::Held { .. })
}
}
impl<const P: u8> fmt::Display for AssertionOutcome<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Held { actual } => pad(f, &format!("held at {actual}")),
Self::Failed(failure) => fmt::Display::fmt(failure, f),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AssertionFailure<const P: u8 = 2> {
pub assertion: BalanceAssertion<P>,
pub actual: Amount<P>,
pub difference: Difference<P>,
}
impl<const P: u8> fmt::Display for AssertionFailure<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let claim = &self.assertion;
pad(
f,
&format!(
"{} {} {} {}: expected {}, actual {}, {}",
claim.account(),
claim.currency,
claim.layer,
claim.at,
claim.expected,
self.actual,
self.difference
),
)
}
}