use std::{cmp::Ordering, num::NonZeroU32};
use miette::Diagnostic;
use thiserror::Error;
use crate::{Currency, Decimal, RoundingMode, Vec1};
#[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 { amount, currency }
}
#[must_use]
pub fn from_minor(minor: i64, currency: Currency) -> Self {
let amount = Decimal::new(minor, currency.minor_digits());
Self { amount, currency }
}
#[must_use]
pub fn from_decimal(amount: Decimal, currency: Currency) -> Self {
Self { amount, currency }
}
#[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(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(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(self, other: Money) -> Result<(), MoneyError> {
if self.currency == other.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!("cannot add: {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!("cannot subtract: {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!("cannot multiply: {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!("cannot divide: {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, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum MoneyError {
#[error("the currencies differ")]
#[diagnostic(
code(lucre::money::currency_mismatch),
help("convert one amount to the other's currency first, or hold both in a `MoneyBag`")
)]
CurrencyMismatch,
#[error("the result is too large for a decimal")]
#[diagnostic(
code(lucre::money::overflow),
help("a `Decimal` holds up to 28 significant digits")
)]
Overflow,
#[error("the divisor is zero")]
#[diagnostic(
code(lucre::money::division_by_zero),
help("check the divisor before dividing")
)]
DivisionByZero,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vec1;
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(expected = "cannot add: the result is too large for a decimal")]
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(expected = "cannot add: the currencies differ")]
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(expected = "cannot subtract: the currencies differ")]
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(expected = "cannot divide: the divisor is zero")]
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(expected = "cannot subtract: the result is too large for a decimal")]
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(expected = "cannot multiply: the result is too large for a decimal")]
fn mul_operator_overflow_panics_test() {
let _ = Money::from_decimal(Decimal::MAX, Currency::USD) * 2;
}
#[test]
#[should_panic(expected = "cannot divide: the result is too large for a decimal")]
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 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 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 = "the currencies differ")]
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 = "the currencies differ")]
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>>();
}
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)));
}
#[test]
fn debug_keeps_a_currency_on_one_line_when_pretty_printed_test() {
assert_eq!(
format!("{:#?}", Money::from_major(1, Currency::USD)),
"Money {\n amount: 1.00,\n currency: Currency(USD),\n}"
);
}
}