#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{cmp::Ordering, fmt::Display, num::NonZeroU32, str::FromStr};
#[doc(no_inline)]
pub use rust_decimal::Decimal;
use thiserror::Error;
#[doc(no_inline)]
pub use vec1::{Vec1, vec1};
mod bag;
mod exchange;
mod format;
mod parse;
#[cfg(feature = "serde")]
mod serde;
pub use bag::{Balances, IntoBalances, MoneyBag};
pub use exchange::{Exchange, ExchangeRate, InvalidRateError};
pub use format::Format;
pub use parse::{ParseMoneyError, Parser};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Money {
amount: Decimal,
currency: Currency,
}
impl Money {
#[must_use]
pub fn from_major(major: i64, currency: &Currency) -> Self {
let mut amount = Decimal::new(major, 0);
amount.rescale(currency.minor_digits);
Self {
currency: *currency,
amount,
}
}
#[must_use]
pub fn from_minor(minor: i64, currency: &Currency) -> Self {
let amount = Decimal::new(minor, currency.minor_digits);
Self {
currency: *currency,
amount,
}
}
#[must_use]
pub fn from_decimal(amount: Decimal, currency: &Currency) -> Self {
Self {
currency: *currency,
amount,
}
}
#[must_use]
pub fn amount(&self) -> Decimal {
self.amount
}
#[must_use]
pub fn currency(&self) -> Currency {
self.currency
}
#[must_use]
pub fn is_amount_zero(&self) -> bool {
self.amount.is_zero()
}
#[must_use]
pub fn is_amount_positive(&self) -> bool {
self.amount > Decimal::ZERO
}
#[must_use]
pub fn is_amount_negative(&self) -> bool {
self.amount < Decimal::ZERO
}
pub fn checked_add(&self, other: &Money) -> Result<Self, MoneyError> {
Self::check_currency_match(self, other)?;
Ok(Self {
amount: self
.amount
.checked_add(other.amount)
.ok_or(MoneyError::Overflow)?,
currency: self.currency,
})
}
pub fn checked_sub(&self, other: &Money) -> Result<Self, MoneyError> {
Self::check_currency_match(self, other)?;
Ok(Self {
amount: self
.amount
.checked_sub(other.amount)
.ok_or(MoneyError::Overflow)?,
currency: self.currency,
})
}
pub fn checked_mul<N: Into<Decimal>>(&self, other: N) -> Result<Self, MoneyError> {
Ok(Self {
amount: self
.amount
.checked_mul(other.into())
.ok_or(MoneyError::Overflow)?,
currency: self.currency,
})
}
pub fn checked_div<N: Into<Decimal>>(&self, other: N) -> Result<Self, MoneyError> {
let other_dec: Decimal = other.into();
if other_dec.is_zero() {
return Err(MoneyError::DivisionByZero);
}
Ok(Self {
amount: self
.amount
.checked_div(other_dec)
.ok_or(MoneyError::Overflow)?,
currency: self.currency,
})
}
#[must_use]
pub fn round(&self, digits: u32, strategy: RoundingMode) -> Self {
let dec_strategy = match strategy {
RoundingMode::HalfUp => rust_decimal::RoundingStrategy::MidpointAwayFromZero,
RoundingMode::HalfDown => rust_decimal::RoundingStrategy::MidpointTowardZero,
RoundingMode::HalfEven => rust_decimal::RoundingStrategy::MidpointNearestEven,
};
let amount = self.amount.round_dp_with_strategy(digits, dec_strategy);
Self {
amount,
currency: self.currency,
}
}
#[must_use]
pub fn split(&self, n: NonZeroU32) -> Vec1<Money> {
let count = n.get() as usize;
self.distribute(u128::from(n.get()), std::iter::repeat_n(1, count))
}
#[must_use]
pub fn allocate(&self, weights: &Vec1<NonZeroU32>) -> Vec1<Money> {
let sum = weights.iter().map(|w| u128::from(w.get())).sum();
self.distribute(sum, weights.iter().map(|w| u128::from(w.get())))
}
fn distribute(&self, weight_sum: u128, weights: impl Iterator<Item = u128>) -> Vec1<Money> {
let mut scale = self.amount.scale();
let mut magnitude = self.amount.mantissa().unsigned_abs();
let sign = if self.amount.mantissa() < 0 {
-1i128
} else {
1
};
let max_magnitude = Decimal::MAX.mantissa().unsigned_abs();
while scale < self.currency.minor_digits && magnitude <= max_magnitude / 10 {
magnitude *= 10;
scale += 1;
}
let whole = magnitude / weight_sum;
let partial = magnitude % weight_sum;
let mut units: Vec<u128> = weights
.map(|weight| whole * weight + partial * weight / weight_sum)
.collect();
let mut leftover = magnitude - units.iter().sum::<u128>();
for unit in &mut units {
if leftover == 0 {
break;
}
*unit += 1;
leftover -= 1;
}
let parts = units
.into_iter()
.map(|unit| Self {
amount: Decimal::from_i128_with_scale(sign * i128::try_from(unit).unwrap(), scale),
currency: self.currency,
})
.collect();
Vec1::try_from_vec(parts).expect("one part per weight, and weights cannot be empty")
}
fn check_currency_match(a: &Money, b: &Money) -> Result<(), MoneyError> {
if a.currency == b.currency {
Ok(())
} else {
Err(MoneyError::CurrencyMismatch)
}
}
}
impl std::ops::Add for Money {
type Output = Money;
fn add(self, rhs: Self) -> Self::Output {
self.checked_add(&rhs)
.unwrap_or_else(|e| panic!("addition error: {e}"))
}
}
impl std::ops::Add<&Money> for Money {
type Output = Money;
fn add(self, rhs: &Money) -> Self::Output {
self + *rhs
}
}
impl std::ops::Add<Money> for &Money {
type Output = Money;
fn add(self, rhs: Money) -> Self::Output {
*self + rhs
}
}
impl std::ops::Add<&Money> for &Money {
type Output = Money;
fn add(self, rhs: &Money) -> Self::Output {
*self + *rhs
}
}
impl std::ops::Sub for Money {
type Output = Money;
fn sub(self, rhs: Self) -> Self::Output {
self.checked_sub(&rhs)
.unwrap_or_else(|e| panic!("subtraction error: {e}"))
}
}
impl std::ops::Sub<&Money> for Money {
type Output = Money;
fn sub(self, rhs: &Money) -> Self::Output {
self - *rhs
}
}
impl std::ops::Sub<Money> for &Money {
type Output = Money;
fn sub(self, rhs: Money) -> Self::Output {
*self - rhs
}
}
impl std::ops::Sub<&Money> for &Money {
type Output = Money;
fn sub(self, rhs: &Money) -> Self::Output {
*self - *rhs
}
}
impl<N> std::ops::Mul<N> for Money
where
N: Into<Decimal>,
{
type Output = Money;
fn mul(self, rhs: N) -> Self::Output {
self.checked_mul(rhs)
.unwrap_or_else(|e| panic!("multiplication error: {e}"))
}
}
impl<N> std::ops::Div<N> for Money
where
N: Into<Decimal>,
{
type Output = Money;
fn div(self, rhs: N) -> Self::Output {
self.checked_div(rhs)
.unwrap_or_else(|e| panic!("division error: {e}"))
}
}
impl std::ops::Neg for Money {
type Output = Money;
fn neg(self) -> Self::Output {
Self {
amount: -self.amount,
currency: self.currency,
}
}
}
impl std::ops::Neg for &Money {
type Output = Money;
fn neg(self) -> Self::Output {
-*self
}
}
macro_rules! impl_scalar_mul {
($($scalar:ty),*) => {$(
impl std::ops::Mul<Money> for $scalar {
type Output = Money;
fn mul(self, rhs: Money) -> Self::Output {
rhs * self
}
}
)*};
}
impl_scalar_mul!(Decimal, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
impl std::ops::AddAssign for Money {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
impl std::ops::AddAssign<&Money> for Money {
fn add_assign(&mut self, rhs: &Money) {
*self = *self + *rhs;
}
}
impl std::ops::SubAssign for Money {
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}
impl std::ops::SubAssign<&Money> for Money {
fn sub_assign(&mut self, rhs: &Money) {
*self = *self - *rhs;
}
}
impl<N: Into<Decimal>> std::ops::MulAssign<N> for Money {
fn mul_assign(&mut self, rhs: N) {
*self = *self * rhs;
}
}
impl<N: Into<Decimal>> std::ops::DivAssign<N> for Money {
fn div_assign(&mut self, rhs: N) {
*self = *self / rhs;
}
}
impl std::iter::Sum<Money> for Option<Money> {
fn sum<I: Iterator<Item = Money>>(iter: I) -> Self {
iter.reduce(|total, amount| total + amount)
}
}
impl<'a> std::iter::Sum<&'a Money> for Option<Money> {
fn sum<I: Iterator<Item = &'a Money>>(iter: I) -> Self {
iter.copied().reduce(|total, amount| total + amount)
}
}
impl std::cmp::PartialOrd for Money {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.currency != other.currency {
return None;
}
Some(self.amount.cmp(&other.amount))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct IsoNumericCode(u32);
impl IsoNumericCode {
#[must_use]
pub fn value(&self) -> u32 {
self.0
}
}
impl Display for IsoNumericCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:03}", self.0)
}
}
impl From<IsoNumericCode> for u32 {
fn from(code: IsoNumericCode) -> Self {
code.0
}
}
impl TryFrom<u32> for IsoNumericCode {
type Error = IsoNumericCodeError;
fn try_from(code: u32) -> Result<Self, Self::Error> {
if code > 999 {
return Err(IsoNumericCodeError::InvalidCode);
}
Ok(Self(code))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct IsoAlphabeticCode([u8; 3]);
impl IsoAlphabeticCode {
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).expect("every construction path admits only ASCII letters")
}
}
impl TryFrom<[u8; 3]> for IsoAlphabeticCode {
type Error = IsoAlphabeticCodeError;
fn try_from(code: [u8; 3]) -> Result<Self, Self::Error> {
if !code.iter().all(u8::is_ascii_uppercase) {
return Err(IsoAlphabeticCodeError::InvalidCode);
}
Ok(Self(code))
}
}
impl TryFrom<&str> for IsoAlphabeticCode {
type Error = IsoAlphabeticCodeError;
fn try_from(code: &str) -> Result<Self, Self::Error> {
let bytes: [u8; 3] = code
.as_bytes()
.try_into()
.map_err(|_| IsoAlphabeticCodeError::InvalidCode)?;
Self::try_from(bytes)
}
}
impl FromStr for IsoAlphabeticCode {
type Err = IsoAlphabeticCodeError;
fn from_str(code: &str) -> Result<Self, Self::Err> {
Self::try_from(code)
}
}
impl AsRef<str> for IsoAlphabeticCode {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<IsoAlphabeticCode> for [u8; 3] {
fn from(code: IsoAlphabeticCode) -> Self {
code.0
}
}
impl Display for IsoAlphabeticCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Currency {
alphabetic_code: IsoAlphabeticCode,
numeric_code: IsoNumericCode,
minor_digits: u32,
symbol: &'static str,
}
impl Currency {
#[must_use]
pub fn alphabetic_code(&self) -> IsoAlphabeticCode {
self.alphabetic_code
}
#[must_use]
pub fn numeric_code(&self) -> IsoNumericCode {
self.numeric_code
}
#[must_use]
pub fn minor_digits(&self) -> u32 {
self.minor_digits
}
#[must_use]
pub fn symbol(&self) -> &'static str {
self.symbol
}
}
include!(concat!(env!("OUT_DIR"), "/iso_currencies.rs"));
impl Display for Currency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.alphabetic_code)
}
}
impl Ord for Currency {
fn cmp(&self, other: &Self) -> Ordering {
self.alphabetic_code.cmp(&other.alphabetic_code)
}
}
impl PartialOrd for Currency {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl From<Currency> for IsoAlphabeticCode {
fn from(currency: Currency) -> Self {
currency.alphabetic_code
}
}
impl From<Currency> for IsoNumericCode {
fn from(currency: Currency) -> Self {
currency.numeric_code
}
}
impl TryFrom<IsoAlphabeticCode> for Currency {
type Error = UnknownCurrencyError;
fn try_from(code: IsoAlphabeticCode) -> Result<Self, Self::Error> {
Currency::from_alphabetic_code(code.as_str()).ok_or(UnknownCurrencyError)
}
}
impl TryFrom<IsoNumericCode> for Currency {
type Error = UnknownCurrencyError;
fn try_from(code: IsoNumericCode) -> Result<Self, Self::Error> {
Currency::from_numeric_code(code.0).ok_or(UnknownCurrencyError)
}
}
impl FromStr for Currency {
type Err = UnknownCurrencyError;
fn from_str(code: &str) -> Result<Self, Self::Err> {
Currency::from_alphabetic_code(code).ok_or(UnknownCurrencyError)
}
}
impl TryFrom<&str> for Currency {
type Error = UnknownCurrencyError;
fn try_from(code: &str) -> Result<Self, Self::Error> {
code.parse()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum RoundingMode {
HalfUp,
HalfDown,
HalfEven,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum MoneyError {
#[error("mismatched currencies")]
CurrencyMismatch,
#[error("overflow")]
Overflow,
#[error("division by zero")]
DivisionByZero,
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no ISO 4217 currency bears this code")]
pub struct UnknownCurrencyError;
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum IsoAlphabeticCodeError {
#[error("invalid ISO 4217 alphabetic code")]
InvalidCode,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum IsoNumericCodeError {
#[error("invalid ISO 4217 numeric code")]
InvalidCode,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::*;
#[test]
fn from_major_minor_decimal_are_equal_test() {
let from_minor = Money::from_minor(100, &Currency::USD);
let from_major = Money::from_major(1, &Currency::USD);
let from_decimal = Money::from_decimal(dec!(1.00), &Currency::USD);
assert_eq!(from_minor, from_major);
assert_eq!(from_major, from_decimal);
assert_eq!(from_minor, from_decimal);
}
#[test]
fn from_major_carries_the_currencys_minor_digits_test() {
assert_eq!(Money::from_major(1, &Currency::USD).amount().scale(), 2);
assert_eq!(Money::from_major(1, &Currency::BHD).amount().scale(), 3);
assert_eq!(Money::from_major(1, &Currency::JPY).amount().scale(), 0);
}
#[test]
fn eq_with_different_currencies_are_not_equal() {
let a = Money::from_major(100, &Currency::USD);
let b = Money::from_major(100, &Currency::EUR);
assert_ne!(a, b);
}
#[test]
fn not_equal_with_different_amounts_test() {
let a = Money::from_major(100, &Currency::USD);
let b = Money::from_major(200, &Currency::USD);
assert_ne!(a, b);
}
#[test]
fn add_test() {
let a = Money::from_minor(100, &Currency::USD);
let b = Money::from_minor(200, &Currency::USD);
let expected = Money::from_minor(300, &Currency::USD);
assert_eq!(a + b, expected);
}
#[test]
fn add_overflow_test() {
let a = Money::from_decimal(Decimal::MAX, &Currency::USD);
let b = Money::from_major(1, &Currency::USD);
assert!(a.checked_add(&b).is_err());
}
#[test]
#[should_panic]
fn add_operator_overflow_panics_test() {
let a = Money::from_decimal(Decimal::MAX, &Currency::USD);
let b = Money::from_major(1, &Currency::USD);
let _ = a + b;
}
#[test]
fn add_mismatched_currencies_test() {
let a = Money::from_minor(100, &Currency::USD);
let b = Money::from_minor(100, &Currency::EUR);
assert!(a.checked_add(&b).is_err());
}
#[test]
#[should_panic]
fn add_operator_mismatched_currencies_panics_test() {
let _ = Money::from_minor(100, &Currency::USD) + Money::from_minor(100, &Currency::EUR);
}
#[test]
fn sub_test() {
let a = Money::from_minor(200, &Currency::USD);
let b = Money::from_minor(100, &Currency::USD);
assert_eq!(a - b, Money::from_minor(100, &Currency::USD));
}
#[test]
fn sub_overflow_test() {
let a = Money::from_decimal(Decimal::MIN, &Currency::USD);
let b = Money::from_major(1, &Currency::USD);
assert!(a.checked_sub(&b).is_err());
}
#[test]
fn sub_mismatched_currencies_test() {
let a = Money::from_minor(100, &Currency::USD);
let b = Money::from_minor(100, &Currency::EUR);
assert!(a.checked_sub(&b).is_err());
}
#[test]
#[should_panic]
fn sub_operator_mismatched_currencies_panics_test() {
let _ = Money::from_minor(100, &Currency::USD) - Money::from_minor(100, &Currency::EUR);
}
#[test]
fn mul_test() {
let a = Money::from_major(100, &Currency::USD);
assert_eq!(a * 5, Money::from_major(500, &Currency::USD));
}
#[test]
fn mul_overflow_test() {
let a = Money::from_decimal(Decimal::MAX, &Currency::USD);
assert!(a.checked_mul(2).is_err());
}
#[test]
fn div_test() {
let a = Money::from_major(100, &Currency::USD);
assert_eq!(a / 5, Money::from_major(20, &Currency::USD));
}
#[test]
fn div_overflow_test() {
let a = Money::from_decimal(Decimal::MAX, &Currency::USD);
assert!(a.checked_div(dec!(0.5)).is_err());
}
#[test]
fn div_by_zero_test() {
let a = Money::from_major(100, &Currency::USD);
assert!(a.checked_div(0).is_err());
}
#[test]
#[should_panic]
fn div_operator_by_zero_panics_test() {
let _ = Money::from_major(100, &Currency::USD) / 0;
}
#[test]
fn partial_cmp_test() {
let one = Money::from_major(1, &Currency::USD);
let two = Money::from_major(2, &Currency::USD);
assert_eq!(one.partial_cmp(&two), Some(Ordering::Less));
assert_eq!(one.partial_cmp(&one), Some(Ordering::Equal));
assert_eq!(two.partial_cmp(&one), Some(Ordering::Greater));
}
#[test]
fn partial_cmp_mismatched_currencies_test() {
let a = Money::from_major(1, &Currency::USD);
let b = Money::from_major(1, &Currency::EUR);
assert_eq!(a.partial_cmp(&b), None);
}
#[test]
#[allow(clippy::neg_cmp_op_on_partial_ord)]
fn ordering_operators_test() {
let one = Money::from_major(1, &Currency::USD);
let two = Money::from_major(2, &Currency::USD);
assert!(one < two);
assert!(one <= two);
assert!(one <= one);
assert!(two > one);
assert!(two >= one);
assert!(two >= two);
assert!(!(two < one));
assert!(!(one > two));
}
#[test]
#[allow(clippy::neg_cmp_op_on_partial_ord)]
fn ordering_operators_mismatched_currencies_test() {
let a = Money::from_major(1, &Currency::USD);
let b = Money::from_major(2, &Currency::EUR);
assert!(!(a < b));
assert!(!(a > b));
assert!(!(a <= b));
assert!(!(a >= b));
}
#[test]
fn round_half_up_test() {
let a = Money::from_decimal(dec!(0.125), &Currency::USD);
assert_eq!(
a.round(2, RoundingMode::HalfUp),
Money::from_decimal(dec!(0.13), &Currency::USD)
);
}
#[test]
fn round_half_down_test() {
let a = Money::from_decimal(dec!(0.125), &Currency::USD);
assert_eq!(
a.round(2, RoundingMode::HalfDown),
Money::from_decimal(dec!(0.12), &Currency::USD)
);
}
#[test]
fn round_half_even_test() {
assert_eq!(
Money::from_decimal(dec!(0.125), &Currency::USD).round(2, RoundingMode::HalfEven),
Money::from_decimal(dec!(0.12), &Currency::USD)
);
assert_eq!(
Money::from_decimal(dec!(0.135), &Currency::USD).round(2, RoundingMode::HalfEven),
Money::from_decimal(dec!(0.14), &Currency::USD)
);
}
#[test]
#[should_panic]
fn sub_operator_overflow_panics_test() {
let a = Money::from_decimal(Decimal::MIN, &Currency::USD);
let b = Money::from_major(1, &Currency::USD);
let _ = a - b;
}
#[test]
#[should_panic]
fn mul_operator_overflow_panics_test() {
let _ = Money::from_decimal(Decimal::MAX, &Currency::USD) * 2;
}
#[test]
#[should_panic]
fn div_operator_overflow_panics_test() {
let _ = Money::from_decimal(Decimal::MAX, &Currency::USD) / dec!(0.5);
}
#[test]
fn arithmetic_with_negative_amounts_test() {
let credit = Money::from_major(5, &Currency::USD);
let debit = Money::from_major(-2, &Currency::USD);
assert_eq!(credit + debit, Money::from_major(3, &Currency::USD));
assert_eq!(debit - credit, Money::from_major(-7, &Currency::USD));
assert!(debit < credit);
}
#[test]
fn mul_by_decimal_scalar_test() {
let a = Money::from_major(5, &Currency::USD);
assert_eq!(
a * dec!(0.5),
Money::from_decimal(dec!(2.5), &Currency::USD)
);
}
#[test]
fn div_keeps_fractional_result_test() {
let a = Money::from_major(10, &Currency::USD);
assert_eq!(a / 4, Money::from_decimal(dec!(2.5), &Currency::USD));
}
#[test]
fn round_negative_midpoint_goes_away_from_zero_test() {
let a = Money::from_decimal(dec!(-0.125), &Currency::USD);
assert_eq!(
a.round(2, RoundingMode::HalfUp),
Money::from_decimal(dec!(-0.13), &Currency::USD)
);
}
#[test]
fn round_beyond_scale_is_identity_test() {
let a = Money::from_decimal(dec!(1.5), &Currency::USD);
assert_eq!(a.round(2, RoundingMode::HalfUp), a);
}
#[test]
fn money_accessors_test() {
let a = Money::from_minor(150, &Currency::USD);
assert_eq!(a.amount(), dec!(1.50));
assert_eq!(a.currency(), Currency::USD);
}
#[test]
fn currency_accessors_test() {
let currency = Currency::USD;
assert_eq!(currency.alphabetic_code().as_str(), "USD");
assert_eq!(currency.numeric_code().value(), 840);
assert_eq!(currency.minor_digits(), 2);
assert_eq!(currency.symbol(), "$");
}
#[test]
fn display_test() {
assert_eq!(
Money::from_minor(150, &Currency::USD).to_string(),
"1.50 USD"
);
assert_eq!(
Money::from_major(-3, &Currency::USD).to_string(),
"-3.00 USD"
);
}
#[test]
fn currency_lookup_test() {
assert_eq!(Currency::from_alphabetic_code("USD"), Some(Currency::USD));
assert_eq!(Currency::from_alphabetic_code("ZZZ"), None);
assert_eq!(Currency::from_numeric_code(978), Some(Currency::EUR));
assert_eq!(Currency::from_numeric_code(1), None);
}
#[test]
fn currency_catalog_test() {
assert!(Currency::all().contains(&Currency::USD));
assert!(Currency::all().contains(&Currency::XAU));
assert_eq!(Currency::BHD.minor_digits(), 3);
assert_eq!(Currency::XAU.minor_digits(), 0);
}
#[test]
fn is_amount_zero_test() {
assert!(Money::from_major(0, &Currency::USD).is_amount_zero());
assert!(!Money::from_major(1, &Currency::USD).is_amount_zero());
assert!(!Money::from_major(-1, &Currency::USD).is_amount_zero());
}
#[test]
fn is_amount_positive_test() {
assert!(Money::from_major(1, &Currency::USD).is_amount_positive());
assert!(!Money::from_major(-1, &Currency::USD).is_amount_positive());
assert!(!Money::from_major(0, &Currency::USD).is_amount_positive());
}
#[test]
fn is_amount_negative_test() {
assert!(Money::from_major(-1, &Currency::USD).is_amount_negative());
assert!(!Money::from_major(1, &Currency::USD).is_amount_negative());
assert!(!Money::from_major(0, &Currency::USD).is_amount_negative());
}
#[test]
fn negative_zero_is_zero_not_negative_test() {
let a = Money::from_decimal(dec!(-0.00), &Currency::USD);
assert!(a.is_amount_zero());
assert!(!a.is_amount_negative());
assert!(!a.is_amount_positive());
}
#[test]
fn from_minor_with_three_minor_digits_test() {
let a = Money::from_minor(1500, &Currency::BHD);
assert_eq!(a.amount(), dec!(1.500));
}
#[test]
fn neg_test() {
let credit = Money::from_minor(2550, &Currency::USD);
assert_eq!(-credit, Money::from_minor(-2550, &Currency::USD));
assert_eq!(-(-credit), credit);
assert_eq!(-&credit, -credit);
}
#[test]
fn neg_extremes_do_not_overflow_test() {
let most = Money::from_decimal(Decimal::MAX, &Currency::USD);
let least = Money::from_decimal(Decimal::MIN, &Currency::USD);
assert_eq!(-most, least);
assert_eq!(-least, most);
}
#[test]
#[allow(clippy::op_ref)]
fn borrowed_operands_test() {
let a = Money::from_minor(300, &Currency::USD);
let b = Money::from_minor(100, &Currency::USD);
assert_eq!(&a + &b, Money::from_minor(400, &Currency::USD));
assert_eq!(&a + b, Money::from_minor(400, &Currency::USD));
assert_eq!(a + &b, Money::from_minor(400, &Currency::USD));
assert_eq!(&a - &b, Money::from_minor(200, &Currency::USD));
}
#[test]
fn assign_operators_test() {
let mut balance = Money::from_major(100, &Currency::USD);
balance += Money::from_minor(2550, &Currency::USD);
assert_eq!(balance, Money::from_minor(12550, &Currency::USD));
balance -= &Money::from_minor(550, &Currency::USD);
assert_eq!(balance, Money::from_minor(12000, &Currency::USD));
balance *= 2;
assert_eq!(balance, Money::from_minor(24000, &Currency::USD));
balance /= 4;
assert_eq!(balance, Money::from_minor(6000, &Currency::USD));
}
#[test]
#[should_panic(expected = "mismatched currencies")]
fn add_assign_mismatched_currencies_panics_test() {
let mut balance = Money::from_major(1, &Currency::USD);
balance += Money::from_major(1, &Currency::EUR);
}
#[test]
fn scalar_on_the_left_test() {
let price = Money::from_minor(1999, &Currency::USD);
assert_eq!(3 * price, price * 3);
assert_eq!(dec!(0.5) * price, price * dec!(0.5));
assert_eq!(3u8 * price, price * 3);
assert_eq!(3usize * price, price * 3);
}
#[test]
fn sum_test() {
let cart = [
Money::from_minor(1999, &Currency::USD),
Money::from_minor(550, &Currency::USD),
];
assert_eq!(
cart.iter().sum::<Option<Money>>(),
Some(Money::from_minor(2549, &Currency::USD))
);
assert_eq!(
cart.into_iter().sum::<Option<Money>>(),
Some(Money::from_minor(2549, &Currency::USD))
);
}
#[test]
fn sum_of_nothing_is_none_test() {
let empty: [Money; 0] = [];
assert_eq!(empty.iter().sum::<Option<Money>>(), None);
}
#[test]
#[should_panic(expected = "mismatched currencies")]
fn sum_mismatched_currencies_panics_test() {
let mixed = [
Money::from_major(1, &Currency::USD),
Money::from_major(1, &Currency::EUR),
];
let _ = mixed.iter().sum::<Option<Money>>();
}
#[test]
fn alphabetic_code_try_from_bytes_test() {
assert_eq!(
IsoAlphabeticCode::try_from(*b"USD"),
Ok(Currency::USD.alphabetic_code())
);
assert_eq!(
IsoAlphabeticCode::try_from(*b"usd"),
Err(IsoAlphabeticCodeError::InvalidCode)
);
assert_eq!(
IsoAlphabeticCode::try_from(*b"840"),
Err(IsoAlphabeticCodeError::InvalidCode)
);
assert_eq!(
IsoAlphabeticCode::try_from([0xC3, 0xA9, b'A']),
Err(IsoAlphabeticCodeError::InvalidCode)
);
}
#[test]
fn alphabetic_code_try_from_str_test() {
assert_eq!(
IsoAlphabeticCode::try_from("USD"),
Ok(Currency::USD.alphabetic_code())
);
assert_eq!("USD".parse(), Ok(Currency::USD.alphabetic_code()));
assert_eq!(
"US".parse::<IsoAlphabeticCode>(),
Err(IsoAlphabeticCodeError::InvalidCode)
);
assert_eq!(
"USDD".parse::<IsoAlphabeticCode>(),
Err(IsoAlphabeticCodeError::InvalidCode)
);
assert_eq!(
"€UR".parse::<IsoAlphabeticCode>(),
Err(IsoAlphabeticCodeError::InvalidCode)
);
}
#[test]
fn code_conversions_test() {
let currency = Currency::USD;
assert_eq!(
IsoAlphabeticCode::from(currency),
currency.alphabetic_code()
);
assert_eq!(IsoNumericCode::from(currency), currency.numeric_code());
assert_eq!(<[u8; 3]>::from(currency.alphabetic_code()), *b"USD");
assert_eq!(u32::from(currency.numeric_code()), 840);
assert_eq!(currency.alphabetic_code().as_ref() as &str, "USD");
}
#[test]
fn numeric_code_try_from_u32_test() {
assert_eq!(
IsoNumericCode::try_from(840),
Ok(Currency::USD.numeric_code())
);
assert_eq!(IsoNumericCode::try_from(0).map(|c| c.value()), Ok(0));
assert_eq!(IsoNumericCode::try_from(999).map(|c| c.value()), Ok(999));
assert_eq!(
IsoNumericCode::try_from(1000),
Err(IsoNumericCodeError::InvalidCode)
);
}
#[test]
fn every_numeric_code_round_trips_through_u32_test() {
for currency in Currency::all() {
let code = currency.numeric_code();
assert_eq!(IsoNumericCode::try_from(code.value()), Ok(code));
}
}
#[test]
fn currency_try_from_codes_test() {
let usd = Currency::USD.alphabetic_code();
assert_eq!(Currency::try_from(usd), Ok(Currency::USD));
assert_eq!(
Currency::try_from(Currency::EUR.numeric_code()),
Ok(Currency::EUR)
);
let unassigned = IsoAlphabeticCode::try_from(*b"ZZZ").unwrap();
assert_eq!(Currency::try_from(unassigned), Err(UnknownCurrencyError));
}
#[test]
fn currency_from_str_test() {
assert_eq!("USD".parse(), Ok(Currency::USD));
assert_eq!(Currency::try_from("EUR"), Ok(Currency::EUR));
assert_eq!("ZZZ".parse::<Currency>(), Err(UnknownCurrencyError));
assert_eq!("usd".parse::<Currency>(), Err(UnknownCurrencyError));
}
#[test]
fn currency_display_round_trips_through_from_str_test() {
for currency in Currency::all() {
assert_eq!(currency.to_string().parse(), Ok(*currency));
}
}
fn n(value: u32) -> NonZeroU32 {
NonZeroU32::new(value).unwrap()
}
fn usd(amount: Decimal) -> Money {
Money::from_decimal(amount, &Currency::USD)
}
#[test]
fn split_evenly_test() {
let parts = usd(dec!(9.00)).split(n(3));
assert_eq!(*parts, vec![usd(dec!(3.00)); 3]);
}
#[test]
fn split_gives_extra_units_to_earlier_parts_test() {
let parts = usd(dec!(10.00)).split(n(3));
assert_eq!(
*parts,
vec![usd(dec!(3.34)), usd(dec!(3.33)), usd(dec!(3.33))]
);
}
#[test]
fn split_into_one_part_is_identity_test() {
let a = usd(dec!(10.01));
assert_eq!(*a.split(n(1)), vec![a]);
}
#[test]
fn split_negative_is_symmetric_test() {
let parts = usd(dec!(-10.01)).split(n(2));
assert_eq!(*parts, vec![usd(dec!(-5.01)), usd(dec!(-5.00))]);
}
#[test]
fn split_zero_amount_test() {
let parts = usd(dec!(0.00)).split(n(3));
assert_eq!(*parts, vec![usd(dec!(0.00)); 3]);
}
#[test]
fn split_keeps_sub_minor_precision_test() {
let parts = usd(dec!(10.005)).split(n(2));
assert_eq!(*parts, vec![usd(dec!(5.003)), usd(dec!(5.002))]);
}
#[test]
fn split_refines_coarse_amounts_to_minor_units_test() {
let parts = Money::from_major(10, &Currency::USD).split(n(3));
assert_eq!(
*parts,
vec![usd(dec!(3.34)), usd(dec!(3.33)), usd(dec!(3.33))]
);
}
#[test]
fn split_zero_minor_digit_currency_stays_whole_test() {
let parts = Money::from_major(10, &Currency::XAU).split(n(3));
assert_eq!(
*parts,
vec![
Money::from_major(4, &Currency::XAU),
Money::from_major(3, &Currency::XAU),
Money::from_major(3, &Currency::XAU),
]
);
}
#[test]
fn split_near_decimal_max_conserves_the_total_test() {
let a = Money::from_decimal(Decimal::MAX, &Currency::USD);
let total = a
.split(n(3))
.into_iter()
.reduce(|sum, part| sum + part)
.unwrap();
assert_eq!(total, a);
}
#[test]
fn split_conserves_the_total_test() {
let a = usd(dec!(100.03));
let total = a
.split(n(7))
.into_iter()
.reduce(|sum, part| sum + part)
.unwrap();
assert_eq!(total, a);
}
#[test]
fn allocate_proportionally_test() {
let parts = usd(dec!(10.00)).allocate(&vec1![n(7), n(3)]);
assert_eq!(*parts, vec![usd(dec!(7.00)), usd(dec!(3.00))]);
}
#[test]
fn allocate_gives_remainder_units_to_earlier_parts_test() {
let parts = usd(dec!(0.05)).allocate(&vec1![n(3), n(7)]);
assert_eq!(*parts, vec![usd(dec!(0.02)), usd(dec!(0.03))]);
}
#[test]
fn allocate_single_weight_is_identity_test() {
let a = usd(dec!(10.01));
assert_eq!(*a.allocate(&vec1![n(42)]), vec![a]);
}
#[test]
fn allocate_negative_is_symmetric_test() {
let parts = usd(dec!(-0.05)).allocate(&vec1![n(3), n(7)]);
assert_eq!(*parts, vec![usd(dec!(-0.02)), usd(dec!(-0.03))]);
}
#[test]
fn allocate_conserves_the_total_test() {
let a = usd(dec!(97.31));
let total = a
.allocate(&vec1![n(1), n(999), n(37), n(2)])
.into_iter()
.reduce(|sum, part| sum + part)
.unwrap();
assert_eq!(total, a);
}
#[test]
fn allocate_with_equal_weights_matches_split_test() {
let a = usd(dec!(10.00));
assert_eq!(a.allocate(&vec1![n(5), n(5), n(5)]), a.split(n(3)));
}
}