#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
#[cfg(feature = "db")]
pub mod ledger;
#[cfg(test)]
mod tests;
use core::fmt;
use core::marker::PhantomData;
use rust_decimal::Decimal;
use rust_decimal::RoundingStrategy;
use rust_decimal::prelude::ToPrimitive as _;
const MAX_EXPONENT: u32 = 4;
pub const MAX_PARTS: usize = 10_000;
const fn scale_for(exponent: u32) -> i64 {
match exponent {
0 => 1,
1 => 10,
2 => 100,
3 => 1_000,
_ => 10_000,
}
}
mod sealed {
pub trait Sealed {}
}
pub trait Currency:
sealed::Sealed
+ Copy
+ Clone
+ fmt::Debug
+ PartialEq
+ Eq
+ core::hash::Hash
+ Send
+ Sync
+ 'static
{
const CODE: &'static str;
const EXPONENT: u32;
const SYMBOL: &'static str;
#[must_use]
fn currency() -> CurrencyCode {
CurrencyCode {
code: Self::CODE,
exponent: Self::EXPONENT,
symbol: Self::SYMBOL,
}
}
}
#[derive(Clone, Copy)]
pub struct CurrencyCode {
code: &'static str,
exponent: u32,
symbol: &'static str,
}
impl CurrencyCode {
#[must_use]
pub const fn code(self) -> &'static str {
self.code
}
#[must_use]
pub const fn exponent(self) -> u32 {
self.exponent
}
#[must_use]
pub const fn symbol(self) -> &'static str {
self.symbol
}
#[must_use]
pub const fn scale(self) -> i64 {
scale_for(self.exponent)
}
pub fn parse(code: &str) -> Result<Self, MoneyError> {
KNOWN
.iter()
.find(|known| known.code.eq_ignore_ascii_case(code))
.copied()
.ok_or_else(|| MoneyError::UnknownCurrency {
code: code.to_owned(),
})
}
#[must_use]
pub const fn known() -> &'static [Self] {
KNOWN
}
}
impl PartialEq for CurrencyCode {
fn eq(&self, other: &Self) -> bool {
self.code == other.code
}
}
impl Eq for CurrencyCode {}
impl core::hash::Hash for CurrencyCode {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.code.hash(state);
}
}
impl PartialOrd for CurrencyCode {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CurrencyCode {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.code.cmp(other.code)
}
}
impl fmt::Debug for CurrencyCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.code)
}
}
impl fmt::Display for CurrencyCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.code)
}
}
macro_rules! currencies {
($( $ty:ident => ($code:literal, $exp:literal, $sym:literal) ),* $(,)?) => {
$(
impl sealed::Sealed for $ty {}
impl Currency for $ty {
const CODE: &'static str = $code;
const EXPONENT: u32 = $exp;
const SYMBOL: &'static str = $sym;
}
impl $ty {
/// This currency, runtime-tagged.
///
/// Inherent as well as on [`Currency`], so `Usd::currency()`
#[must_use]
pub const fn currency() -> CurrencyCode {
CurrencyCode { code: $code, exponent: $exp, symbol: $sym }
}
}
const _: () = assert!(matches!($exp, 0..=MAX_EXPONENT));
)*
static KNOWN: &[CurrencyCode] = &[
$( CurrencyCode { code: $code, exponent: $exp, symbol: $sym } ),*
];
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Aed;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Aud;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Bhd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Brl;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Cad;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Chf;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Clp;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Cny;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Czk;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Dkk;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Eur;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Gbp;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Hkd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Huf;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Ils;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Inr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Isk;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Jod;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Jpy;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Krw;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Kwd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Mxn;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Nok;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Nzd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Omr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Pln;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Sar;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Sek;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Sgd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Try;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Twd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Usd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Vnd;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Zar;
currencies! {
Aed => ("AED", 2, "د.إ"),
Aud => ("AUD", 2, "$"),
Bhd => ("BHD", 3, ".د.ب"),
Brl => ("BRL", 2, "R$"),
Cad => ("CAD", 2, "$"),
Chf => ("CHF", 2, "CHF"),
Clp => ("CLP", 0, "$"),
Cny => ("CNY", 2, "¥"),
Czk => ("CZK", 2, "Kč"),
Dkk => ("DKK", 2, "kr"),
Eur => ("EUR", 2, "€"),
Gbp => ("GBP", 2, "£"),
Hkd => ("HKD", 2, "$"),
Huf => ("HUF", 2, "Ft"),
Ils => ("ILS", 2, "₪"),
Inr => ("INR", 2, "₹"),
Isk => ("ISK", 0, "kr"),
Jod => ("JOD", 3, "د.ا"),
Jpy => ("JPY", 0, "¥"),
Krw => ("KRW", 0, "₩"),
Kwd => ("KWD", 3, "د.ك"),
Mxn => ("MXN", 2, "$"),
Nok => ("NOK", 2, "kr"),
Nzd => ("NZD", 2, "$"),
Omr => ("OMR", 3, "ر.ع."),
Pln => ("PLN", 2, "zł"),
Sar => ("SAR", 2, "﷼"),
Sek => ("SEK", 2, "kr"),
Sgd => ("SGD", 2, "$"),
Try => ("TRY", 2, "₺"),
Twd => ("TWD", 2, "NT$"),
Usd => ("USD", 2, "$"),
Vnd => ("VND", 0, "₫"),
Zar => ("ZAR", 2, "R"),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Rounding {
HalfUp,
HalfEven,
HalfDown,
TowardZero,
AwayFromZero,
Floor,
Ceiling,
}
impl Rounding {
const fn strategy(self) -> RoundingStrategy {
match self {
Self::HalfUp => RoundingStrategy::MidpointAwayFromZero,
Self::HalfEven => RoundingStrategy::MidpointNearestEven,
Self::HalfDown => RoundingStrategy::MidpointTowardZero,
Self::TowardZero => RoundingStrategy::ToZero,
Self::AwayFromZero => RoundingStrategy::AwayFromZero,
Self::Floor => RoundingStrategy::ToNegativeInfinity,
Self::Ceiling => RoundingStrategy::ToPositiveInfinity,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MoneyError {
Overflow,
CurrencyMismatch {
expected: &'static str,
found: &'static str,
},
UnknownCurrency {
code: String,
},
Inexact {
value: String,
currency: &'static str,
},
InvalidWeights,
}
impl fmt::Display for MoneyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Overflow => f.write_str("money amount is out of range"),
Self::CurrencyMismatch { expected, found } => {
write!(f, "currency mismatch: expected {expected}, found {found}")
}
Self::UnknownCurrency { code } => write!(f, "unknown currency code: {code}"),
Self::Inexact { value, currency } => {
write!(f, "{value} has more precision than {currency} allows")
}
Self::InvalidWeights => f.write_str("allocation weights must be positive"),
}
}
}
impl std::error::Error for MoneyError {}
impl MoneyError {
#[must_use]
pub const fn http_status(&self) -> axum::http::StatusCode {
match *self {
Self::Overflow => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
_ => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
}
}
}
#[derive(Clone, Copy)]
pub struct Money<C: Currency> {
minor: i64,
currency: PhantomData<C>,
}
impl<C: Currency> Money<C> {
pub const ZERO: Self = Self {
minor: 0,
currency: PhantomData,
};
#[must_use]
pub const fn from_minor(minor: i64) -> Self {
Self {
minor,
currency: PhantomData,
}
}
#[must_use]
pub const fn minor(self) -> i64 {
self.minor
}
#[must_use]
pub fn currency(self) -> CurrencyCode {
C::currency()
}
pub fn from_major(major: i64) -> Result<Self, MoneyError> {
major
.checked_mul(scale_for(C::EXPONENT))
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn checked_add(self, other: Self) -> Result<Self, MoneyError> {
self.minor
.checked_add(other.minor)
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn checked_sub(self, other: Self) -> Result<Self, MoneyError> {
self.minor
.checked_sub(other.minor)
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn checked_neg(self) -> Result<Self, MoneyError> {
self.minor
.checked_neg()
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn checked_abs(self) -> Result<Self, MoneyError> {
self.minor
.checked_abs()
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn checked_mul(self, factor: i64) -> Result<Self, MoneyError> {
self.minor
.checked_mul(factor)
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn try_sum<I: IntoIterator<Item = Self>>(values: I) -> Result<Self, MoneyError> {
values.into_iter().try_fold(Self::ZERO, Self::checked_add)
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.minor == 0
}
#[must_use]
pub const fn is_negative(self) -> bool {
self.minor < 0
}
#[must_use]
pub const fn is_positive(self) -> bool {
self.minor > 0
}
#[must_use]
pub fn to_any(self) -> AnyMoney {
AnyMoney {
minor: self.minor,
currency: C::currency(),
}
}
#[must_use]
pub fn to_decimal(self) -> Decimal {
Decimal::new(self.minor, C::EXPONENT)
}
pub fn from_decimal(value: Decimal, rounding: Rounding) -> Result<Self, MoneyError> {
let scale = Decimal::from(scale_for(C::EXPONENT));
let scaled = value.checked_mul(scale).ok_or(MoneyError::Overflow)?;
scaled
.round_dp_with_strategy(0, rounding.strategy())
.to_i64()
.map(Self::from_minor)
.ok_or(MoneyError::Overflow)
}
pub fn from_decimal_exact(value: Decimal) -> Result<Self, MoneyError> {
let converted = Self::from_decimal(value, Rounding::TowardZero)?;
if converted.to_decimal() == value.normalize() {
Ok(converted)
} else {
Err(MoneyError::Inexact {
value: value.to_string(),
currency: C::CODE,
})
}
}
pub fn allocate(self, weights: &[i64]) -> Result<Vec<Self>, MoneyError> {
if weights.is_empty() || weights.iter().any(|weight| *weight < 0) {
return Err(MoneyError::InvalidWeights);
}
let total_weight: i64 = weights
.iter()
.try_fold(0_i64, |sum, weight| sum.checked_add(*weight))
.ok_or(MoneyError::Overflow)?;
if total_weight == 0 {
return Err(MoneyError::InvalidWeights);
}
let amount = i128::from(self.minor);
let divisor = i128::from(total_weight);
let mut shares: Vec<(i64, i128)> = Vec::with_capacity(weights.len());
let mut floor_total: i128 = 0;
for weight in weights {
let numerator = amount
.checked_mul(i128::from(*weight))
.ok_or(MoneyError::Overflow)?;
let share = numerator.div_euclid(divisor);
let remainder = numerator.rem_euclid(divisor);
floor_total = floor_total.checked_add(share).ok_or(MoneyError::Overflow)?;
let share = i64::try_from(share).map_err(|_| MoneyError::Overflow)?;
shares.push((share, remainder));
}
let mut leftover = amount
.checked_sub(floor_total)
.ok_or(MoneyError::Overflow)?;
let mut order: Vec<usize> = (0..shares.len()).collect();
order.sort_by(|left, right| {
let left_remainder = shares.get(*left).map_or(0, |share| share.1);
let right_remainder = shares.get(*right).map_or(0, |share| share.1);
right_remainder.cmp(&left_remainder).then(left.cmp(right))
});
for index in order {
if leftover <= 0 {
break;
}
if let Some(share) = shares.get_mut(index) {
share.0 = share.0.checked_add(1).ok_or(MoneyError::Overflow)?;
leftover = leftover.saturating_sub(1);
}
}
Ok(shares
.into_iter()
.map(|(share, _)| Self::from_minor(share))
.collect())
}
pub fn split(self, parts: usize) -> Result<Vec<Self>, MoneyError> {
if parts == 0 || parts > MAX_PARTS {
return Err(MoneyError::InvalidWeights);
}
self.allocate(&vec![1_i64; parts])
}
}
impl<C: Currency> PartialEq for Money<C> {
fn eq(&self, other: &Self) -> bool {
self.minor == other.minor
}
}
impl<C: Currency> Eq for Money<C> {}
impl<C: Currency> PartialOrd for Money<C> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<C: Currency> Ord for Money<C> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.minor.cmp(&other.minor)
}
}
impl<C: Currency> core::hash::Hash for Money<C> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.minor.hash(state);
C::CODE.hash(state);
}
}
impl<C: Currency> Default for Money<C> {
fn default() -> Self {
Self::ZERO
}
}
impl<C: Currency> fmt::Debug for Money<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Money({} {})", self.minor, C::CODE)
}
}
impl<C: Currency> fmt::Display for Money<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_amount(f, self.minor, C::EXPONENT)?;
write!(f, " {}", C::CODE)
}
}
fn write_amount(f: &mut fmt::Formatter<'_>, minor: i64, exponent: u32) -> fmt::Result {
let scale = scale_for(exponent).unsigned_abs();
let magnitude = minor.unsigned_abs();
let whole = magnitude.checked_div(scale).unwrap_or(magnitude);
let fraction = magnitude.checked_rem(scale).unwrap_or(0);
if minor < 0 {
f.write_str("-")?;
}
write!(f, "{whole}")?;
if exponent > 0 {
write!(f, ".{fraction:0width$}", width = exponent as usize)?;
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AnyMoney {
minor: i64,
currency: CurrencyCode,
}
impl AnyMoney {
#[must_use]
pub const fn new(minor: i64, currency: CurrencyCode) -> Self {
Self { minor, currency }
}
#[must_use]
pub const fn zero(currency: CurrencyCode) -> Self {
Self { minor: 0, currency }
}
#[must_use]
pub const fn minor(self) -> i64 {
self.minor
}
#[must_use]
pub const fn currency(self) -> CurrencyCode {
self.currency
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.minor == 0
}
#[must_use]
pub const fn is_negative(self) -> bool {
self.minor < 0
}
#[must_use]
pub fn to_decimal(self) -> Decimal {
Decimal::new(self.minor, self.currency.exponent)
}
pub fn checked_add(self, other: Self) -> Result<Self, MoneyError> {
self.require_same_currency(other)?;
self.minor
.checked_add(other.minor)
.map(|minor| Self::new(minor, self.currency))
.ok_or(MoneyError::Overflow)
}
pub fn checked_sub(self, other: Self) -> Result<Self, MoneyError> {
self.require_same_currency(other)?;
self.minor
.checked_sub(other.minor)
.map(|minor| Self::new(minor, self.currency))
.ok_or(MoneyError::Overflow)
}
pub fn checked_neg(self) -> Result<Self, MoneyError> {
self.minor
.checked_neg()
.map(|minor| Self::new(minor, self.currency))
.ok_or(MoneyError::Overflow)
}
pub fn try_sum<I: IntoIterator<Item = Self>>(
currency: CurrencyCode,
values: I,
) -> Result<Self, MoneyError> {
values
.into_iter()
.try_fold(Self::zero(currency), |total, value| {
total.checked_add(value)
})
}
pub fn try_into_typed<C: Currency>(self) -> Result<Money<C>, MoneyError> {
if self.currency.code == C::CODE {
Ok(Money::from_minor(self.minor))
} else {
Err(MoneyError::CurrencyMismatch {
expected: C::CODE,
found: self.currency.code,
})
}
}
fn require_same_currency(self, other: Self) -> Result<(), MoneyError> {
if self.currency == other.currency {
Ok(())
} else {
Err(MoneyError::CurrencyMismatch {
expected: self.currency.code,
found: other.currency.code,
})
}
}
}
impl<C: Currency> From<Money<C>> for AnyMoney {
fn from(value: Money<C>) -> Self {
value.to_any()
}
}
impl fmt::Display for AnyMoney {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_amount(f, self.minor, self.currency.exponent)?;
write!(f, " {}", self.currency.code)
}
}