mod arithmetic;
use crate::amounts::{AmountError, AmountQuantum, AmountRounding, DecimalAmount};
use crate::error::{IdentifierError, NonFinite};
use crate::identifier::{Entry, Identifiers};
use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SettlementEvidence {
kind: EvidenceKind,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum EvidenceKind {
Decimal {
lower: DecimalAmount,
upper: DecimalAmount,
declared: bool,
},
Binary {
lower: u64,
upper: u64,
declared: bool,
},
}
impl SettlementEvidence {
#[must_use]
pub const fn exact_decimal(value: DecimalAmount) -> Self {
Self {
kind: EvidenceKind::Decimal {
lower: value,
upper: value,
declared: false,
},
}
}
pub fn supplied_binary64(value: f64) -> Result<Self, SettlementError> {
let bits = finite_bits(value, "value")?;
Ok(Self {
kind: EvidenceKind::Binary {
lower: bits,
upper: bits,
declared: false,
},
})
}
pub fn declared_decimal_interval(
lower: DecimalAmount,
upper: DecimalAmount,
) -> Result<Self, SettlementError> {
if lower > upper {
return Err(SettlementError::DecimalIntervalOrder { lower, upper });
}
Ok(Self {
kind: EvidenceKind::Decimal {
lower,
upper,
declared: true,
},
})
}
pub fn declared_binary64_interval(lower: f64, upper: f64) -> Result<Self, SettlementError> {
check_finite(lower, "lower")?;
check_finite(upper, "upper")?;
let lower_bits = canonical_bits(lower);
let upper_bits = canonical_bits(upper);
if lower > upper {
return Err(SettlementError::BinaryIntervalOrder {
lower_bits,
upper_bits,
});
}
Ok(Self {
kind: EvidenceKind::Binary {
lower: lower_bits,
upper: upper_bits,
declared: true,
},
})
}
#[must_use]
pub const fn decimal_bounds(self) -> Option<(DecimalAmount, DecimalAmount)> {
match self.kind {
EvidenceKind::Decimal { lower, upper, .. } => Some((lower, upper)),
EvidenceKind::Binary { .. } => None,
}
}
#[must_use]
pub fn binary64_bounds(self) -> Option<(f64, f64)> {
match self.kind {
EvidenceKind::Binary { lower, upper, .. } => {
Some((f64::from_bits(lower), f64::from_bits(upper)))
}
EvidenceKind::Decimal { .. } => None,
}
}
#[must_use]
pub const fn is_declared_interval(self) -> bool {
match self.kind {
EvidenceKind::Decimal { declared, .. } | EvidenceKind::Binary { declared, .. } => {
declared
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SettlementRounding {
quantum: AmountQuantum,
mode: AmountRounding,
}
impl SettlementRounding {
#[must_use]
pub const fn new(quantum: AmountQuantum, mode: AmountRounding) -> Self {
Self { quantum, mode }
}
#[must_use]
pub const fn quantum(self) -> AmountQuantum {
self.quantum
}
#[must_use]
pub const fn mode(self) -> AmountRounding {
self.mode
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum CashAggregation {
RoundTotal,
RoundComponentsThenSum,
}
impl CashAggregation {
pub const ALL: [Self; 2] = [Self::RoundTotal, Self::RoundComponentsThenSum];
#[must_use]
pub const fn canonical_identifier(self) -> &'static str {
match self {
Self::RoundTotal => "RoundTotal",
Self::RoundComponentsThenSum => "RoundComponentsThenSum",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum SettlementGuarantee {
ForSuppliedDecimals,
ForSuppliedBinary64,
UniqueForDeclaredEnclosure,
}
impl SettlementGuarantee {
pub const ALL: [Self; 3] = [
Self::ForSuppliedDecimals,
Self::ForSuppliedBinary64,
Self::UniqueForDeclaredEnclosure,
];
#[must_use]
pub const fn canonical_identifier(self) -> &'static str {
match self {
Self::ForSuppliedDecimals => "ForSuppliedDecimals",
Self::ForSuppliedBinary64 => "ForSuppliedBinary64",
Self::UniqueForDeclaredEnclosure => "UniqueForDeclaredEnclosure",
}
}
}
const AGGREGATIONS: Identifiers<CashAggregation> = Identifiers::new(
"cash aggregation",
&[
Entry {
value: CashAggregation::RoundTotal,
canonical: "RoundTotal",
aliases: &[],
},
Entry {
value: CashAggregation::RoundComponentsThenSum,
canonical: "RoundComponentsThenSum",
aliases: &[],
},
],
&[],
);
const GUARANTEES: Identifiers<SettlementGuarantee> = Identifiers::new(
"settlement guarantee",
&[
Entry {
value: SettlementGuarantee::ForSuppliedDecimals,
canonical: "ForSuppliedDecimals",
aliases: &[],
},
Entry {
value: SettlementGuarantee::ForSuppliedBinary64,
canonical: "ForSuppliedBinary64",
aliases: &[],
},
Entry {
value: SettlementGuarantee::UniqueForDeclaredEnclosure,
canonical: "UniqueForDeclaredEnclosure",
aliases: &[],
},
],
&[],
);
impl fmt::Display for CashAggregation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_identifier())
}
}
impl FromStr for CashAggregation {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
AGGREGATIONS.parse(text)
}
}
impl fmt::Display for SettlementGuarantee {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_identifier())
}
}
impl FromStr for SettlementGuarantee {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
GUARANTEES.parse(text)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SettlementInstruction {
nominal: DecimalAmount,
primary: SettlementEvidence,
cash: SettlementRounding,
recipe: Recipe,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Recipe {
Dirty,
Coupon,
Clean {
accrued: SettlementEvidence,
aggregation: CashAggregation,
},
Staged {
accrued: SettlementEvidence,
aggregation: CashAggregation,
price: SettlementRounding,
accrued_rounding: SettlementRounding,
},
}
impl SettlementInstruction {
pub fn dirty_per_100(
nominal: DecimalAmount,
dirty: SettlementEvidence,
cash_rounding: SettlementRounding,
) -> Result<Self, SettlementError> {
positive_nominal(nominal)?;
Ok(Self {
nominal,
primary: dirty,
cash: cash_rounding,
recipe: Recipe::Dirty,
})
}
pub fn coupon_per_1000(
nominal: DecimalAmount,
coupon: SettlementEvidence,
cash_rounding: SettlementRounding,
) -> Result<Self, SettlementError> {
positive_nominal(nominal)?;
Ok(Self {
nominal,
primary: coupon,
cash: cash_rounding,
recipe: Recipe::Coupon,
})
}
pub fn clean_accrued_per_100(
nominal: DecimalAmount,
clean: SettlementEvidence,
accrued: SettlementEvidence,
aggregation: CashAggregation,
cash_rounding: SettlementRounding,
) -> Result<Self, SettlementError> {
positive_nominal(nominal)?;
Ok(Self {
nominal,
primary: clean,
cash: cash_rounding,
recipe: Recipe::Clean {
accrued,
aggregation,
},
})
}
pub fn staged_price100_accrued1000(
nominal: DecimalAmount,
clean: SettlementEvidence,
accrued: SettlementEvidence,
price_mode: AmountRounding,
accrued_mode: AmountRounding,
aggregation: CashAggregation,
cash_rounding: SettlementRounding,
) -> Result<Self, SettlementError> {
positive_nominal(nominal)?;
let price =
AmountQuantum::new(1, 6).map_err(|_| capacity("source price", "source rounding"))?;
let ai =
AmountQuantum::new(1, 5).map_err(|_| capacity("source accrued", "source rounding"))?;
Ok(Self {
nominal,
primary: clean,
cash: cash_rounding,
recipe: Recipe::Staged {
accrued,
aggregation,
price: SettlementRounding::new(price, price_mode),
accrued_rounding: SettlementRounding::new(ai, accrued_mode),
},
})
}
#[must_use]
pub const fn nominal(self) -> DecimalAmount {
self.nominal
}
#[must_use]
pub const fn primary(self) -> SettlementEvidence {
self.primary
}
#[must_use]
pub const fn secondary(self) -> Option<SettlementEvidence> {
match self.recipe {
Recipe::Clean { accrued, .. } | Recipe::Staged { accrued, .. } => Some(accrued),
Recipe::Dirty | Recipe::Coupon => None,
}
}
#[must_use]
pub const fn aggregation(self) -> Option<CashAggregation> {
match self.recipe {
Recipe::Clean { aggregation, .. } | Recipe::Staged { aggregation, .. } => {
Some(aggregation)
}
Recipe::Dirty | Recipe::Coupon => None,
}
}
#[must_use]
pub const fn cash_rounding(self) -> SettlementRounding {
self.cash
}
#[must_use]
pub const fn source_price_rounding(self) -> Option<SettlementRounding> {
match self.recipe {
Recipe::Staged { price, .. } => Some(price),
_ => None,
}
}
#[must_use]
pub const fn source_accrued_rounding(self) -> Option<SettlementRounding> {
match self.recipe {
Recipe::Staged {
accrued_rounding, ..
} => Some(accrued_rounding),
_ => None,
}
}
#[must_use]
pub const fn recipe_identifier(self) -> &'static str {
match self.recipe {
Recipe::Dirty => "DirtyPer100",
Recipe::Coupon => "CouponPer1000",
Recipe::Clean { .. } => "CleanAccruedPer100",
Recipe::Staged { .. } => "StagedPrice100Accrued1000",
}
}
pub fn evaluate(&self) -> Result<SettlementDecision, SettlementError> {
arithmetic::evaluate(self)
}
fn guarantee(self) -> SettlementGuarantee {
let secondary = self.secondary();
if self.primary.is_declared_interval()
|| secondary.is_some_and(SettlementEvidence::is_declared_interval)
{
SettlementGuarantee::UniqueForDeclaredEnclosure
} else if matches!(self.primary.kind, EvidenceKind::Binary { .. })
|| secondary.is_some_and(|e| matches!(e.kind, EvidenceKind::Binary { .. }))
{
SettlementGuarantee::ForSuppliedBinary64
} else {
SettlementGuarantee::ForSuppliedDecimals
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
#[expect(
clippy::large_enum_variant,
reason = "the frozen allocation-free Copy receipt retains its complete original instruction"
)]
pub enum SettlementDecision {
Determined(SettlementReceipt),
#[non_exhaustive]
QuantumAmbiguous {
lower_amount: DecimalAmount,
upper_amount: DecimalAmount,
},
#[non_exhaustive]
ExactStageUnresolved {
stage: &'static str,
},
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SettlementReceipt {
amount: DecimalAmount,
instruction: SettlementInstruction,
guarantee: SettlementGuarantee,
}
impl SettlementReceipt {
#[must_use]
pub const fn amount(self) -> DecimalAmount {
self.amount
}
#[must_use]
pub const fn instruction(&self) -> &SettlementInstruction {
&self.instruction
}
#[must_use]
pub const fn guarantee(self) -> SettlementGuarantee {
self.guarantee
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SettlementError {
#[non_exhaustive]
NonPositiveNominal {
nominal: DecimalAmount,
},
#[non_exhaustive]
DecimalIntervalOrder {
lower: DecimalAmount,
upper: DecimalAmount,
},
#[non_exhaustive]
BinaryIntervalOrder {
lower_bits: u64,
upper_bits: u64,
},
#[non_exhaustive]
NonFinite {
endpoint: &'static str,
value: NonFinite,
},
#[non_exhaustive]
Arithmetic {
stage: &'static str,
endpoint: &'static str,
cause: AmountError,
},
#[non_exhaustive]
InternalCapacity {
stage: &'static str,
operation: &'static str,
},
}
impl SettlementError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::NonPositiveNominal { .. } => "settlement.nonpositive_nominal",
Self::DecimalIntervalOrder { .. } => "settlement.decimal_interval_order",
Self::BinaryIntervalOrder { .. } => "settlement.binary_interval_order",
Self::NonFinite { .. } => "settlement.non_finite",
Self::Arithmetic { .. } => "settlement.arithmetic",
Self::InternalCapacity { .. } => "settlement.internal_capacity",
}
}
}
impl fmt::Display for SettlementError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonPositiveNominal { nominal } => {
write!(f, "par nominal must be positive, got {nominal}")
}
Self::DecimalIntervalOrder { lower, upper } => {
write!(f, "decimal evidence bounds reversed: {lower} > {upper}")
}
Self::BinaryIntervalOrder {
lower_bits,
upper_bits,
} => write!(
f,
"binary evidence bounds reversed: {lower_bits:#018x}, {upper_bits:#018x}"
),
Self::NonFinite { endpoint, value } => write!(
f,
"settlement {endpoint} evidence must be finite, got {value}"
),
Self::Arithmetic {
stage,
endpoint,
cause,
} => write!(f, "{stage} at {endpoint}: {cause}"),
Self::InternalCapacity { stage, operation } => write!(
f,
"{stage}: private capacity invariant failed during {operation}"
),
}
}
}
impl std::error::Error for SettlementError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Arithmetic { cause, .. } => Some(cause),
_ => None,
}
}
}
fn positive_nominal(nominal: DecimalAmount) -> Result<(), SettlementError> {
if nominal.coefficient() <= 0 {
Err(SettlementError::NonPositiveNominal { nominal })
} else {
Ok(())
}
}
fn finite_bits(value: f64, endpoint: &'static str) -> Result<u64, SettlementError> {
check_finite(value, endpoint)?;
Ok(canonical_bits(value))
}
fn check_finite(value: f64, endpoint: &'static str) -> Result<(), SettlementError> {
if let Some(value) = NonFinite::classify(value) {
return Err(SettlementError::NonFinite { endpoint, value });
}
Ok(())
}
fn canonical_bits(value: f64) -> u64 {
let bits = value.to_bits();
if bits << 1 == 0 { 0 } else { bits }
}
fn capacity(stage: &'static str, operation: &'static str) -> SettlementError {
SettlementError::InternalCapacity { stage, operation }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifier_tables_are_complete_and_exact() {
AGGREGATIONS.check().unwrap();
AGGREGATIONS
.check_round_trip(&CashAggregation::ALL, |x| x.to_string())
.unwrap();
GUARANTEES.check().unwrap();
GUARANTEES
.check_round_trip(&SettlementGuarantee::ALL, |x| x.to_string())
.unwrap();
}
}