use core::fmt;
use core::iter::Sum;
use core::str::FromStr;
use super::{Decode, DecodeError, Encode, Reader, Side, Sink, ValueError, Writer};
pub(crate) const MAX_PARTS: usize = 65_536;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum AmountError {
#[error("the amount is out of range")]
Overflow,
#[error("not a decimal amount literal")]
InvalidLiteral,
#[error("more decimal places than the scale {scale} allows")]
PrecisionLoss {
scale: u8,
},
#[error("the weights are empty or all zero")]
ZeroWeight,
#[error("more than {max} parts")]
TooManyParts {
max: usize,
},
#[error("division by zero")]
DivideByZero,
#[error("the amount must be positive")]
NotPositive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Rounding {
HalfUp,
HalfEven,
HalfDown,
TowardZero,
AwayFromZero,
Floor,
Ceiling,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Amount<const P: u8 = 2>(i128);
impl<const P: u8> Amount<P> {
const SCALE_OK: () = assert!(P <= 18, "P must be <= 18");
const UNIT: i128 = {
let () = Self::SCALE_OK;
pow10(P)
};
pub const MAX_PRECISION: u8 = 18;
pub const ZERO: Self = {
let () = Self::SCALE_OK;
Self(0)
};
pub const MAX: Self = {
let () = Self::SCALE_OK;
Self(i128::MAX)
};
pub const MIN: Self = {
let () = Self::SCALE_OK;
Self(-i128::MAX)
};
pub const fn from_minor(minor: i128) -> Result<Self, AmountError> {
let () = Self::SCALE_OK;
if minor == i128::MIN {
Err(AmountError::Overflow)
} else {
Ok(Self(minor))
}
}
#[must_use]
pub const fn to_minor(self) -> i128 {
self.0
}
pub fn from_major(major: i128) -> Result<Self, AmountError> {
major
.checked_mul(Self::UNIT)
.ok_or(AmountError::Overflow)
.and_then(Self::from_minor)
}
pub const fn parse_const(text: &str) -> Result<Self, AmountError> {
let () = Self::SCALE_OK;
let mut rest = text.as_bytes();
let negative = if let [b'-', tail @ ..] = rest {
rest = tail;
true
} else {
false
};
let mut magnitude: i128 = 0;
let mut overflow = false;
let mut integer_digits = 0usize;
while let [digit @ b'0'..=b'9', tail @ ..] = rest {
magnitude = push_digit(magnitude, *digit, &mut overflow);
integer_digits = integer_digits.saturating_add(1);
rest = tail;
}
if integer_digits == 0 {
return Err(AmountError::InvalidLiteral);
}
let mut fraction_digits: u8 = 0;
let mut excess = false;
if let [b'.', tail @ ..] = rest {
rest = tail;
while let [digit @ b'0'..=b'9', tail @ ..] = rest {
if fraction_digits >= P {
excess = true;
} else {
magnitude = push_digit(magnitude, *digit, &mut overflow);
fraction_digits = fraction_digits.saturating_add(1);
}
rest = tail;
}
if fraction_digits == 0 && !excess {
return Err(AmountError::InvalidLiteral);
}
}
if !rest.is_empty() {
return Err(AmountError::InvalidLiteral);
}
if excess {
return Err(AmountError::PrecisionLoss { scale: P });
}
while fraction_digits < P {
magnitude = push_digit(magnitude, b'0', &mut overflow);
fraction_digits = fraction_digits.saturating_add(1);
}
if overflow {
return Err(AmountError::Overflow);
}
Ok(Self(if negative {
magnitude.wrapping_neg()
} else {
magnitude
}))
}
pub fn parse(text: &str) -> Result<Self, AmountError> {
Self::parse_const(text)
}
pub fn checked_add(self, rhs: Self) -> Result<Self, AmountError> {
self.0
.checked_add(rhs.0)
.ok_or(AmountError::Overflow)
.and_then(Self::from_minor)
}
pub fn checked_sub(self, rhs: Self) -> Result<Self, AmountError> {
self.0
.checked_sub(rhs.0)
.ok_or(AmountError::Overflow)
.and_then(Self::from_minor)
}
pub fn checked_mul_int(self, factor: i128) -> Result<Self, AmountError> {
self.0
.checked_mul(factor)
.ok_or(AmountError::Overflow)
.and_then(Self::from_minor)
}
pub fn checked_mul_ratio(
self,
numerator: i128,
denominator: i128,
rounding: Rounding,
) -> Result<Self, AmountError> {
if denominator == 0 {
return Err(AmountError::DivideByZero);
}
let product = self.0.checked_mul(numerator).ok_or(AmountError::Overflow)?;
div_rounded(product, denominator, rounding).and_then(Self::from_minor)
}
pub fn rescale<const Q: u8>(self, rounding: Rounding) -> Result<Amount<Q>, AmountError> {
let minor = if Q >= P {
self.0
.checked_mul(pow10(Q.saturating_sub(P)))
.ok_or(AmountError::Overflow)?
} else {
div_rounded(self.0, pow10(P.saturating_sub(Q)), rounding)?
};
Amount::<Q>::from_minor(minor)
}
pub fn checked_sum<I: IntoIterator<Item = Self>>(amounts: I) -> Result<Self, AmountError> {
amounts.into_iter().try_fold(Self::ZERO, Self::checked_add)
}
#[must_use]
pub const fn negate(self) -> Self {
Self(self.0.wrapping_neg())
}
#[must_use]
pub const fn abs(self) -> Self {
Self(self.0.wrapping_abs())
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn is_positive(self) -> bool {
self.0 > 0
}
#[must_use]
pub const fn is_negative(self) -> bool {
self.0 < 0
}
#[must_use]
pub const fn fits_exponent(self, exponent: u8) -> bool {
if exponent > P {
return false;
}
matches!(
self.0.checked_rem(pow10(P.saturating_sub(exponent))),
Some(0)
)
}
pub fn allocate(self, weights: &[u64]) -> Result<Vec<Self>, AmountError> {
if weights.len() > MAX_PARTS {
return Err(AmountError::TooManyParts { max: MAX_PARTS });
}
let total = weights
.iter()
.try_fold(0u64, |sum, weight| sum.checked_add(*weight))
.ok_or(AmountError::Overflow)?;
if total == 0 {
return Err(AmountError::ZeroWeight);
}
let total = u128::from(total);
let magnitude = self.0.unsigned_abs();
let q = magnitude
.checked_div(total)
.ok_or(AmountError::ZeroWeight)?;
let r = magnitude
.checked_rem(total)
.ok_or(AmountError::ZeroWeight)?;
let mut shares = Vec::with_capacity(weights.len());
let mut remainders = Vec::with_capacity(weights.len());
let mut assigned: u128 = 0;
for weight in weights {
let weight = u128::from(*weight);
let spread = r.checked_mul(weight).ok_or(AmountError::Overflow)?;
let share = q
.checked_mul(weight)
.and_then(|base| base.checked_add(spread.checked_div(total)?))
.ok_or(AmountError::Overflow)?;
assigned = assigned.checked_add(share).ok_or(AmountError::Overflow)?;
shares.push(share);
remainders.push(spread.checked_rem(total).ok_or(AmountError::Overflow)?);
}
let leftover = magnitude
.checked_sub(assigned)
.ok_or(AmountError::Overflow)?;
let mut order: Vec<usize> = (0..weights.len()).collect();
order.sort_by(|a, b| remainders.get(*b).cmp(&remainders.get(*a)));
let leftover = usize::try_from(leftover).map_err(|_| AmountError::Overflow)?;
for index in order.into_iter().take(leftover) {
if let Some(share) = shares.get_mut(index) {
*share = share.checked_add(1).ok_or(AmountError::Overflow)?;
}
}
shares
.into_iter()
.map(|share| self.signed_part(share))
.collect()
}
pub fn distribute(self, parts: usize) -> Result<Vec<Self>, AmountError> {
if parts == 0 {
return Err(AmountError::ZeroWeight);
}
if parts > MAX_PARTS {
return Err(AmountError::TooManyParts { max: MAX_PARTS });
}
let count =
u128::try_from(parts).map_err(|_| AmountError::TooManyParts { max: MAX_PARTS })?;
let magnitude = self.0.unsigned_abs();
let q = magnitude
.checked_div(count)
.ok_or(AmountError::ZeroWeight)?;
let r = magnitude
.checked_rem(count)
.ok_or(AmountError::ZeroWeight)?;
let r = usize::try_from(r).map_err(|_| AmountError::Overflow)?;
let larger = q.checked_add(1).ok_or(AmountError::Overflow)?;
(0..parts)
.map(|index| self.signed_part(if index < r { larger } else { q }))
.collect()
}
fn signed_part(self, magnitude: u128) -> Result<Self, AmountError> {
let part = i128::try_from(magnitude).map_err(|_| AmountError::Overflow)?;
Self::from_minor(if self.0 < 0 {
part.wrapping_neg()
} else {
part
})
}
}
const fn pow10(exponent: u8) -> i128 {
let mut result: i128 = 1;
let mut left = exponent;
while left > 0 {
result = match result.checked_mul(10) {
Some(next) => next,
None => return i128::MAX,
};
left = left.saturating_sub(1);
}
result
}
const fn push_digit(magnitude: i128, digit: u8, overflow: &mut bool) -> i128 {
let value = digit.saturating_sub(b'0') as i128;
match magnitude.checked_mul(10) {
Some(shifted) => match shifted.checked_add(value) {
Some(next) => next,
None => {
*overflow = true;
0
}
},
None => {
*overflow = true;
0
}
}
}
fn div_rounded(
numerator: i128,
denominator: i128,
rounding: Rounding,
) -> Result<i128, AmountError> {
let quotient = numerator
.checked_div(denominator)
.ok_or(AmountError::Overflow)?;
let remainder = numerator
.checked_rem(denominator)
.ok_or(AmountError::Overflow)?;
if remainder == 0 {
return Ok(quotient);
}
let negative = (numerator < 0) != (denominator < 0);
let away = if negative {
quotient.checked_sub(1)
} else {
quotient.checked_add(1)
}
.ok_or(AmountError::Overflow)?;
let r = remainder.unsigned_abs();
let rest = denominator
.unsigned_abs()
.checked_sub(r)
.ok_or(AmountError::Overflow)?;
let half = r.cmp(&rest);
let round_away = match rounding {
Rounding::TowardZero => false,
Rounding::AwayFromZero => true,
Rounding::Floor => negative,
Rounding::Ceiling => !negative,
Rounding::HalfUp => half.is_ge(),
Rounding::HalfDown => half.is_gt(),
Rounding::HalfEven => half.is_gt() || (half.is_eq() && quotient & 1 != 0),
};
Ok(if round_away { away } else { quotient })
}
impl<const P: u8> Default for Amount<P> {
fn default() -> Self {
Self::ZERO
}
}
impl<const P: u8> fmt::Display for Amount<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad_integral(self.0 >= 0, "", &unsigned_text(self.0.unsigned_abs(), P))
}
}
fn unsigned_text(magnitude: u128, scale: u8) -> String {
let digits = magnitude.to_string();
let scale = usize::from(scale);
let mut text = String::with_capacity(digits.len().saturating_add(scale).saturating_add(2));
let padding = scale.saturating_add(1).saturating_sub(digits.len());
text.extend(core::iter::repeat_n('0', padding));
text.push_str(&digits);
if scale > 0 {
text.insert(text.len().saturating_sub(scale), '.');
}
text
}
struct AtExponent<const P: u8> {
amount: Amount<P>,
exponent: u8,
}
impl<const P: u8> fmt::Display for AtExponent<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.amount.0;
let exponent = self.exponent.min(P);
let unit = pow10(P.saturating_sub(exponent));
let (magnitude, scale) = if value.checked_rem(unit) == Some(0) {
(
value
.unsigned_abs()
.checked_div(unit.unsigned_abs())
.unwrap_or(0),
exponent,
)
} else {
(value.unsigned_abs(), P)
};
f.pad_integral(value >= 0, "", &unsigned_text(magnitude, scale))
}
}
impl<const P: u8> Amount<P> {
pub const MAX_PARTS: usize = MAX_PARTS;
#[must_use]
pub fn display_at(self, exponent: u8) -> impl fmt::Display {
AtExponent {
amount: self,
exponent,
}
}
}
impl<const P: u8> fmt::Debug for Amount<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Amount<{P}>({self})")
}
}
impl<const P: u8> FromStr for Amount<P> {
type Err = AmountError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::parse_const(text)
}
}
impl<const P: u8> Sum<Amount<P>> for Result<Amount<P>, AmountError> {
fn sum<I: Iterator<Item = Amount<P>>>(iter: I) -> Self {
Amount::checked_sum(iter)
}
}
impl<'a, const P: u8> Sum<&'a Amount<P>> for Result<Amount<P>, AmountError> {
fn sum<I: Iterator<Item = &'a Amount<P>>>(iter: I) -> Self {
Amount::checked_sum(iter.copied())
}
}
impl<const P: u8> Encode for Amount<P> {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.i128(self.0);
}
}
impl<const P: u8> Decode for Amount<P> {
fn decode(r: &mut Reader<'_>, field: &'static str) -> Result<Self, DecodeError> {
Self::from_minor(r.i128()?).map_err(|_| DecodeError::Value {
field,
error: ValueError::OutOfRange,
})
}
}
#[cfg(feature = "serde")]
impl<const P: u8> serde::Serialize for Amount<P> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de, const P: u8> serde::Deserialize<'de> for Amount<P> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
super::serde_str::deserialize(deserializer)
}
}
#[macro_export]
macro_rules! amount {
($literal:literal) => {
const {
#[allow(clippy::panic)]
let amount = match $crate::Amount::parse_const($literal) {
::core::result::Result::Ok(amount) => amount,
::core::result::Result::Err($crate::error::AmountError::PrecisionLoss {
..
}) => {
::core::panic!(::core::concat!(
"amount!(",
::core::stringify!($literal),
"): more decimal places than the scale P allows"
))
}
::core::result::Result::Err($crate::error::AmountError::Overflow) => {
::core::panic!(::core::concat!(
"amount!(",
::core::stringify!($literal),
"): out of range"
))
}
::core::result::Result::Err(_) => ::core::panic!(::core::concat!(
"amount!(",
::core::stringify!($literal),
"): not a decimal amount (expected -?[0-9]+(.[0-9]+)?)"
)),
};
amount
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Balance<const P: u8 = 2> {
debit: Amount<P>,
credit: Amount<P>,
postings: u64,
}
impl<const P: u8> fmt::Display for Balance<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let postings = match self.postings {
1 => String::from("1 posting"),
count => format!("{count} postings"),
};
let text = format!(
"dr {} cr {} net {} ({postings})",
self.debit,
self.credit,
self.net()
);
super::encoding::pad(f, &text)
}
}
impl<const P: u8> Balance<P> {
pub const ZERO: Self = Self {
debit: Amount::ZERO,
credit: Amount::ZERO,
postings: 0,
};
pub fn from_parts(
debit: Amount<P>,
credit: Amount<P>,
postings: u64,
) -> Result<Self, ValueError> {
let empty = debit.is_zero() && credit.is_zero();
if debit.is_negative() || credit.is_negative() || empty != (postings == 0) {
return Err(ValueError::OutOfRange);
}
Ok(Self {
debit,
credit,
postings,
})
}
#[must_use]
pub const fn debit(&self) -> Amount<P> {
self.debit
}
#[must_use]
pub const fn credit(&self) -> Amount<P> {
self.credit
}
#[must_use]
pub const fn postings(&self) -> u64 {
self.postings
}
pub fn post(self, side: Side, amount: Amount<P>) -> Result<Self, AmountError> {
if !amount.is_positive() {
return Err(AmountError::NotPositive);
}
let postings = self.postings.checked_add(1).ok_or(AmountError::Overflow)?;
Ok(match side {
Side::Debit => Self {
debit: self.debit.checked_add(amount)?,
postings,
..self
},
Side::Credit => Self {
credit: self.credit.checked_add(amount)?,
postings,
..self
},
})
}
pub fn checked_add(self, other: &Self) -> Result<Self, AmountError> {
Ok(Self {
debit: self.debit.checked_add(other.debit)?,
credit: self.credit.checked_add(other.credit)?,
postings: self
.postings
.checked_add(other.postings)
.ok_or(AmountError::Overflow)?,
})
}
#[must_use]
pub const fn net(&self) -> Amount<P> {
Amount(self.debit.0.wrapping_sub(self.credit.0))
}
#[must_use]
pub fn nets_equal(&self, other: &Self) -> bool {
self.net() == other.net()
}
#[must_use]
pub const fn is_zero(&self) -> bool {
self.postings == 0
}
}
impl<const P: u8> Default for Balance<P> {
fn default() -> Self {
Self::ZERO
}
}
impl<const P: u8> Encode for Balance<P> {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.value(&self.debit);
w.value(&self.credit);
w.u64(self.postings);
}
}
impl<const P: u8> Decode for Balance<P> {
fn decode(r: &mut Reader<'_>, field: &'static str) -> Result<Self, DecodeError> {
let debit = r.value(field)?;
let credit = r.value(field)?;
let postings = r.u64()?;
Self::from_parts(debit, credit, postings)
.map_err(|error| DecodeError::Value { field, error })
}
}
#[cfg(feature = "serde")]
mod balance_serde {
use super::{Amount, Balance, ValueError};
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "Balance")]
pub(super) struct Parts<const P: u8> {
debit: Amount<P>,
credit: Amount<P>,
postings: u64,
}
impl<const P: u8> From<Balance<P>> for Parts<P> {
fn from(balance: Balance<P>) -> Self {
Self {
debit: balance.debit,
credit: balance.credit,
postings: balance.postings,
}
}
}
impl<const P: u8> TryFrom<Parts<P>> for Balance<P> {
type Error = ValueError;
fn try_from(parts: Parts<P>) -> Result<Self, Self::Error> {
Self::from_parts(parts.debit, parts.credit, parts.postings)
}
}
impl<const P: u8> serde::Serialize for Balance<P> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Parts::from(*self).serialize(serializer)
}
}
impl<'de, const P: u8> serde::Deserialize<'de> for Balance<P> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Parts::deserialize(deserializer)?
.try_into()
.map_err(serde::de::Error::custom)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pow10_covers_every_scale() {
assert_eq!(pow10(0), 1);
assert_eq!(pow10(2), 100);
assert_eq!(pow10(18), 1_000_000_000_000_000_000);
}
#[test]
fn half_modes_compare_against_the_midpoint_without_doubling() {
let d = i128::MAX;
let n = d - 1;
assert_eq!(div_rounded(n, d, Rounding::HalfUp), Ok(1));
assert_eq!(div_rounded(n, d, Rounding::TowardZero), Ok(0));
assert_eq!(
div_rounded(i128::MIN, 2, Rounding::HalfUp),
Ok(i128::MIN / 2)
);
}
}