use crate::MoneyError;
use crate::currency::FormattableCurrency;
use crate::exchange::Exchange;
use crate::format::{Formatter, Params, Position};
use crate::locale::LocalFormat;
use std::cmp::Ordering;
use std::fmt;
use std::ops::Neg;
use std::str::FromStr;
use rust_decimal::Decimal;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub struct Money<'a, T: FormattableCurrency> {
amount: Decimal,
currency: &'a T,
}
impl<'a, T: FormattableCurrency> Neg for Money<'a, T> {
type Output = Money<'a, T>;
#[inline]
fn neg(self) -> Self::Output {
Money {
amount: -self.amount,
currency: self.currency,
}
}
}
impl<'a, T: FormattableCurrency> Money<'a, T> {
pub fn from_str(amount: &str, currency: &'a T) -> Result<Money<'a, T>, MoneyError> {
if amount.is_empty() {
return Err(MoneyError::InvalidAmount);
}
let format = LocalFormat::from_locale(currency.locale());
let amount_parts: Vec<&str> = amount.split(format.exponent_separator).collect();
let mut split_decimal: Vec<&str> = amount_parts[0].split(format.digit_separator).collect();
let mut parsed_decimal = split_decimal.concat();
for &num in format.digit_separator_pattern.iter() {
if split_decimal.len() <= 1 {
break;
}
let current = split_decimal.pop().unwrap();
if current.len() != num {
return Err(MoneyError::InvalidAmount);
}
}
if amount_parts.len() == 1 {
parsed_decimal += ".";
for _ in 0..currency.exponent() {
parsed_decimal += "0";
}
} else if amount_parts.len() == 2 {
i32::from_str(amount_parts[1])?;
parsed_decimal = parsed_decimal + "." + amount_parts[1];
} else {
return Err(MoneyError::InvalidAmount);
}
let decimal = Decimal::from_str(&parsed_decimal).map_err(|_| MoneyError::InvalidAmount)?;
Ok(Money::from_decimal(decimal, currency))
}
pub fn from_minor(amount: i64, currency: &'a T) -> Money<'a, T> {
let amount = Decimal::new(amount, currency.exponent());
Money { amount, currency }
}
pub fn from_major(amount: i64, currency: &'a T) -> Money<'a, T> {
let amount = Decimal::new(amount, 0);
Money { amount, currency }
}
pub fn from_decimal(amount: Decimal, currency: &'a T) -> Money<'a, T> {
Money { amount, currency }
}
#[inline]
pub fn amount(&self) -> &Decimal {
&self.amount
}
#[inline]
pub fn currency(&self) -> &'a T {
self.currency
}
#[inline]
pub fn is_zero(&self) -> bool {
self.amount == Decimal::ZERO
}
#[inline]
pub fn is_positive(&self) -> bool {
self.amount.is_sign_positive() && self.amount != Decimal::ZERO
}
#[inline]
pub fn is_negative(&self) -> bool {
self.amount.is_sign_negative() && self.amount != Decimal::ZERO
}
#[inline]
pub fn abs(&self) -> Money<'a, T> {
Money::from_decimal(self.amount.abs(), self.currency)
}
#[inline]
pub fn to_minor_units(&self) -> i64 {
use rust_decimal::prelude::ToPrimitive;
let scale = Decimal::from(10u64.pow(self.currency.exponent()));
(self.amount * scale).trunc().to_i64().unwrap_or(0)
}
pub fn to_f64_lossy(&self) -> f64 {
use rust_decimal::prelude::ToPrimitive;
self.amount.to_f64().unwrap_or(f64::NAN)
}
#[inline]
pub fn add(&self, other: Money<'a, T>) -> Result<Money<'a, T>, MoneyError> {
if self.currency != other.currency {
return Err(MoneyError::CurrencyMismatch {
expected: self.currency.code(),
actual: other.currency.code(),
});
}
Ok(Money::from_decimal(
self.amount + other.amount,
self.currency,
))
}
#[inline]
pub fn sub(&self, other: Money<'a, T>) -> Result<Money<'a, T>, MoneyError> {
if self.currency != other.currency {
return Err(MoneyError::CurrencyMismatch {
expected: self.currency.code(),
actual: other.currency.code(),
});
}
Ok(Money::from_decimal(
self.amount - other.amount,
self.currency,
))
}
#[inline]
pub fn mul<N: Into<Decimal>>(&self, n: N) -> Result<Money<'a, T>, MoneyError> {
self.amount
.checked_mul(n.into())
.map(|result| Money::from_decimal(result, self.currency))
.ok_or(MoneyError::Overflow)
}
#[inline]
pub fn div<N: Into<Decimal> + Copy + PartialEq + Default>(
&self,
n: N,
) -> Result<Money<'a, T>, MoneyError> {
if n == N::default() {
return Err(MoneyError::DivisionByZero);
}
Ok(Money::from_decimal(self.amount / n.into(), self.currency))
}
pub fn exchange_to(
&self,
target: &'a T,
exchange: &Exchange<'a, T>,
) -> Result<Money<'a, T>, MoneyError> {
let rate = exchange
.get_rate(self.currency, target)
.ok_or(MoneyError::InvalidCurrency)?;
rate.convert(self)
}
#[inline]
pub fn compare(&self, other: &Money<'a, T>) -> Result<Ordering, MoneyError> {
if self.currency != other.currency {
return Err(MoneyError::CurrencyMismatch {
expected: self.currency.code(),
actual: other.currency.code(),
});
}
Ok(self.amount.cmp(&other.amount))
}
#[inline]
pub fn gt(&self, other: &Money<'a, T>) -> Result<bool, MoneyError> {
Ok(self.compare(other)?.is_gt())
}
#[inline]
pub fn gte(&self, other: &Money<'a, T>) -> Result<bool, MoneyError> {
Ok(self.compare(other)?.is_ge())
}
#[inline]
pub fn lt(&self, other: &Money<'a, T>) -> Result<bool, MoneyError> {
Ok(self.compare(other)?.is_lt())
}
#[inline]
pub fn lte(&self, other: &Money<'a, T>) -> Result<bool, MoneyError> {
Ok(self.compare(other)?.is_le())
}
#[inline]
pub fn eq(&self, other: &Money<'a, T>) -> Result<bool, MoneyError> {
Ok(self.compare(other)?.is_eq())
}
pub fn split(&self, n: u32) -> Result<Vec<Money<'a, T>>, MoneyError> {
use rust_decimal::prelude::ToPrimitive;
if n == 0 {
return Err(MoneyError::InvalidRatio);
}
let minor_per_major = Decimal::from(10u64.pow(self.currency.exponent()));
let total_minor = (self.amount * minor_per_major).floor();
let major_per_minor = Decimal::new(1, self.currency.exponent());
let n_decimal = Decimal::from(n);
let base_share = (total_minor / n_decimal).floor();
let remainder = total_minor - (base_share * n_decimal);
let remainder_count = remainder.to_usize().unwrap_or(0);
let high_share =
Money::from_decimal((base_share + Decimal::ONE) * major_per_minor, self.currency);
let low_share = Money::from_decimal(base_share * major_per_minor, self.currency);
let mut result = Vec::with_capacity(n as usize);
for i in 0..n as usize {
if i < remainder_count {
result.push(high_share);
} else {
result.push(low_share);
}
}
Ok(result)
}
pub fn allocate(&self, shares: Vec<u32>) -> Result<Vec<Money<'a, T>>, MoneyError> {
if shares.is_empty() {
return Err(MoneyError::InvalidRatio);
}
let share_total: u64 = shares.iter().map(|&x| x as u64).sum();
if share_total == 0 {
return Err(MoneyError::InvalidRatio);
}
let minor_per_major = Decimal::from(10u64.pow(self.currency.exponent()));
let total_minor = (self.amount * minor_per_major).floor();
let share_total_decimal = Decimal::from(share_total);
let mut allocations_minor: Vec<Decimal> = Vec::with_capacity(shares.len());
let mut allocated = Decimal::ZERO;
for &share in &shares {
let share_value = (total_minor * Decimal::from(share) / share_total_decimal).floor();
allocations_minor.push(share_value);
allocated += share_value;
}
let mut remainder = total_minor - allocated;
let mut i: usize = 0;
while remainder > Decimal::ZERO {
allocations_minor[i] += Decimal::ONE;
remainder -= Decimal::ONE;
i += 1;
}
let major_per_minor = Decimal::new(1, self.currency.exponent());
Ok(allocations_minor
.into_iter()
.map(|minor| Money::from_decimal(minor * major_per_minor, self.currency))
.collect())
}
pub fn round(&self, digits: u32, strategy: Round) -> Money<'a, T> {
let mut money = *self;
money.amount = match strategy {
Round::HalfDown => money
.amount
.round_dp_with_strategy(digits, rust_decimal::RoundingStrategy::MidpointTowardZero),
Round::HalfUp => money.amount.round_dp_with_strategy(
digits,
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
),
Round::HalfEven => money.amount.round_dp_with_strategy(
digits,
rust_decimal::RoundingStrategy::MidpointNearestEven,
),
};
money
}
}
pub enum Round {
HalfUp,
HalfDown,
HalfEven,
}
const POSITIONS_SYMBOL_FIRST: &[Position] = &[Position::Sign, Position::Symbol, Position::Amount];
const POSITIONS_SYMBOL_LAST: &[Position] = &[Position::Sign, Position::Amount, Position::Symbol];
impl<'a, T: FormattableCurrency> fmt::Display for Money<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let currency = self.currency;
let format = LocalFormat::from_locale(currency.locale());
let positions = if currency.symbol_first() {
POSITIONS_SYMBOL_FIRST
} else {
POSITIONS_SYMBOL_LAST
};
let format_params = Params {
digit_separator: format.digit_separator,
exponent_separator: format.exponent_separator,
separator_pattern: format.digit_separator_pattern,
positions,
rounding: Some(currency.exponent()),
symbol: Some(currency.symbol()),
code: Some(currency.code()),
};
write!(f, "{}", Formatter::money(self, format_params))
}
}
#[cfg(feature = "serde")]
mod serde_support {
use super::*;
use crate::currency::Findable;
use rust_decimal::Decimal;
use serde::de::{self, Deserializer, MapAccess, Visitor};
use serde::ser::{SerializeStruct, Serializer};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt;
use std::marker::PhantomData;
impl<T: FormattableCurrency> Serialize for Money<'_, T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Money", 2)?;
state.serialize_field("amount", &self.amount)?;
state.serialize_field("currency", self.currency.code())?;
state.end()
}
}
impl<'de, T: Findable + 'static> Deserialize<'de> for Money<'static, T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Amount,
Currency,
}
struct MoneyVisitor<T>(PhantomData<T>);
impl<'de, T: Findable + 'static> Visitor<'de> for MoneyVisitor<T> {
type Value = Money<'static, T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct Money with amount and currency fields")
}
fn visit_map<V>(self, mut map: V) -> Result<Money<'static, T>, V::Error>
where
V: MapAccess<'de>,
{
let mut amount: Option<Decimal> = None;
let mut currency_code: Option<Cow<'de, str>> = None;
while let Some(key) = map.next_key()? {
match key {
Field::Amount => {
if amount.is_some() {
return Err(de::Error::duplicate_field("amount"));
}
amount = Some(map.next_value()?);
}
Field::Currency => {
if currency_code.is_some() {
return Err(de::Error::duplicate_field("currency"));
}
currency_code = Some(map.next_value()?);
}
}
}
let amount = amount.ok_or_else(|| de::Error::missing_field("amount"))?;
let currency_code =
currency_code.ok_or_else(|| de::Error::missing_field("currency"))?;
let currency = T::find(¤cy_code).ok_or_else(|| {
de::Error::custom(format!("unknown currency code: {}", currency_code))
})?;
Ok(Money::from_decimal(amount, currency))
}
}
const FIELDS: &[&str] = &["amount", "currency"];
deserializer.deserialize_struct("Money", FIELDS, MoneyVisitor(PhantomData))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::define_currency_set;
define_currency_set!(
test {
USD: {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 100,
name: "USD",
symbol: "$",
symbol_first: true,
},
GBP: {
code: "GBP",
exponent: 2,
locale: EnUs,
minor_units: 1,
name: "British Pound",
symbol: "£",
symbol_first: true,
},
EUR: {
code: "EUR",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Euro",
symbol: "€",
symbol_first: true,
},
INR: {
code: "INR",
exponent: 2,
locale: EnIn,
minor_units: 50,
name: "Indian Rupee",
symbol: "₹",
symbol_first: true,
},
BHD: {
code: "BHD",
exponent: 3,
locale: EnUs,
minor_units: 5,
name: "Bahraini Dinar",
symbol: "ب.د",
symbol_first: true,
},
AED: {
code: "AED",
exponent: 2,
locale: EnUs,
minor_units: 25,
name: "United Arab Emirates Dirham",
symbol: "د.إ",
symbol_first: false,
},
JPY: {
code: "JPY",
exponent: 0,
locale: EnUs,
minor_units: 1,
name: "Japanese Yen",
symbol: "Â¥",
symbol_first: true,
}
}
);
mod construction {
use super::*;
#[test]
fn major_minor_equivalence() {
let _usd = test::find("USD"); let major_usd = Money::from_major(10, test::USD);
let minor_usd = Money::from_minor(1000, test::USD);
assert_eq!(major_usd, minor_usd);
}
#[test]
fn from_minor_vs_from_major_usd() {
let from_minor = Money::from_minor(10000, test::USD);
let from_major = Money::from_major(100, test::USD);
assert_eq!(
format!("{}", from_minor),
format!("{}", from_major),
"from_minor and from_major should format identically"
);
assert_eq!("$100.00", format!("{}", from_major));
}
#[test]
fn from_minor_vs_from_major_eur() {
let from_minor = Money::from_minor(10000, test::EUR);
let from_major = Money::from_major(100, test::EUR);
assert_eq!(
format!("{}", from_minor),
format!("{}", from_major),
"from_minor and from_major should format identically"
);
assert_eq!("€100,00", format!("{}", from_major));
}
#[test]
fn from_minor_with_zero_exponent() {
let from_minor = Money::from_minor(100, test::JPY);
let from_major = Money::from_major(100, test::JPY);
assert_eq!(from_minor, from_major);
assert_eq!(format!("{}", from_minor), "Â¥100");
}
#[test]
fn from_str_parses_correctly() {
let expected_money = Money::from_minor(2999, test::GBP);
let money = Money::from_str("29.99", test::GBP).unwrap();
assert_eq!(money, expected_money);
}
#[test]
fn from_str_parses_64_bit_numbers() {
let expected_money = Money::from_major(i64::MAX, test::GBP);
let money = Money::from_str(&i64::MAX.to_string(), test::GBP).unwrap();
assert_eq!(money, expected_money);
}
#[test]
fn from_str_parses_signs() {
let expected_money = Money::from_minor(-300, test::GBP);
let money = Money::from_str("-3", test::GBP).unwrap();
assert_eq!(money, expected_money);
let expected_money = Money::from_minor(300, test::GBP);
let money = Money::from_str("+3", test::GBP).unwrap();
assert_eq!(money, expected_money);
}
#[test]
fn from_str_ignores_separators() {
let expected_money = Money::from_minor(100000000, test::GBP);
let money = Money::from_str("1,000,000", test::GBP).unwrap();
assert_eq!(money, expected_money);
}
#[test]
fn from_str_parse_errors() {
assert_eq!(
Money::from_str("1.0000,000", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1.0000.000", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1.0000!000", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str(",", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str(".", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str(",,.", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str(" ", test::USD).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str(" 100 ", test::USD).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("", test::USD).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1,00.00", test::GBP).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1.00,00", test::EUR).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1.00.000,00", test::EUR).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1.00.000.000,00", test::EUR).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1,00.00", test::INR).unwrap_err(),
MoneyError::InvalidAmount
);
assert_eq!(
Money::from_str("1.000.000.00", test::INR).unwrap_err(),
MoneyError::InvalidAmount
);
}
}
mod arithmetic {
use super::*;
#[test]
fn addition_and_subtraction() {
let sum = Money::from_major(1, test::USD)
.add(Money::from_major(1, test::USD))
.unwrap();
assert_eq!(Money::from_major(2, test::USD), sum);
let diff = Money::from_major(1, test::USD)
.sub(Money::from_major(1, test::USD))
.unwrap();
assert_eq!(Money::from_major(0, test::USD), diff);
}
#[test]
fn addition_returns_error_on_different_currencies() {
let result = Money::from_minor(100, test::USD).add(Money::from_minor(100, test::GBP));
assert!(result.is_err());
match result.unwrap_err() {
MoneyError::CurrencyMismatch { expected, actual } => {
assert_eq!(expected, "USD");
assert_eq!(actual, "GBP");
}
_ => panic!("Expected CurrencyMismatch error"),
}
}
#[test]
fn subtraction_returns_error_on_different_currencies() {
let result = Money::from_minor(100, test::USD).sub(Money::from_minor(100, test::GBP));
assert!(result.is_err());
match result.unwrap_err() {
MoneyError::CurrencyMismatch { expected, actual } => {
assert_eq!(expected, "USD");
assert_eq!(actual, "GBP");
}
_ => panic!("Expected CurrencyMismatch error"),
}
}
#[test]
fn multiplication() {
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(100, test::USD).mul(2).unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(-100, test::USD).mul(-2).unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(100, test::USD)
.mul(Decimal::new(2, 0))
.unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(-100, test::USD)
.mul(Decimal::new(-2, 0))
.unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(400, test::USD)
.mul(Decimal::new(5, 1))
.unwrap()
);
}
#[test]
fn division() {
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(400, test::USD).div(2).unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(-400, test::USD).div(-2).unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(-200, test::USD).div(-1).unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(400, test::USD)
.div(Decimal::new(2, 0))
.unwrap()
);
assert_eq!(
Money::from_minor(200, test::USD),
Money::from_minor(-400, test::USD)
.div(Decimal::new(-2, 0))
.unwrap()
);
assert_eq!(
Money::from_minor(400, test::USD),
Money::from_minor(-200, test::USD)
.div(Decimal::new(-5, 1))
.unwrap()
);
}
#[test]
fn division_by_zero_returns_error() {
let money = Money::from_minor(100, test::USD);
let result = money.div(0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MoneyError::DivisionByZero);
}
#[test]
fn negation() {
let money = Money::from_minor(100, test::USD);
assert_eq!(-money, Money::from_minor(-100, test::USD));
}
#[test]
fn copy_semantics() {
let money = Money::from_major(1, test::USD);
let _1st_derived_money = money.mul(3).unwrap();
let _2nd_derived_money = money.mul(3).unwrap();
}
#[test]
fn multiplication_overflow_returns_error() {
let money = Money::from_decimal(Decimal::MAX, test::USD);
let result = money.mul(2);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MoneyError::Overflow);
}
}
mod exchange {
use super::*;
use crate::ExchangeRate;
use rust_decimal_macros::dec;
#[test]
fn exchange_to_converts_currency() {
let mut exchange = Exchange::new();
let rate = ExchangeRate::new(test::USD, test::EUR, dec!(0.85)).unwrap();
exchange.set_rate(&rate);
let usd = Money::from_minor(1000, test::USD); let eur = usd.exchange_to(test::EUR, &exchange).unwrap();
assert_eq!(eur, Money::from_minor(850, test::EUR)); assert_eq!(eur.currency(), test::EUR);
}
#[test]
fn exchange_to_returns_error_when_rate_missing() {
let exchange = Exchange::<test::Currency>::new();
let usd = Money::from_minor(1000, test::USD);
let result = usd.exchange_to(test::EUR, &exchange);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MoneyError::InvalidCurrency);
}
#[test]
fn exchange_to_with_zero_amount() {
let mut exchange = Exchange::new();
let rate = ExchangeRate::new(test::USD, test::EUR, dec!(0.85)).unwrap();
exchange.set_rate(&rate);
let zero_usd = Money::from_minor(0, test::USD);
let zero_eur = zero_usd.exchange_to(test::EUR, &exchange).unwrap();
assert!(zero_eur.is_zero());
assert_eq!(zero_eur.currency(), test::EUR);
}
#[test]
fn exchange_to_preserves_sign() {
let mut exchange = Exchange::new();
let rate = ExchangeRate::new(test::USD, test::EUR, dec!(0.85)).unwrap();
exchange.set_rate(&rate);
let negative_usd = Money::from_minor(-1000, test::USD);
let negative_eur = negative_usd.exchange_to(test::EUR, &exchange).unwrap();
assert!(negative_eur.is_negative());
assert_eq!(negative_eur, Money::from_minor(-850, test::EUR));
}
}
mod comparison {
use super::*;
#[test]
fn ordering_via_compare() {
let small = Money::from_minor(100, test::USD);
let large = Money::from_minor(200, test::USD);
let equal = Money::from_minor(100, test::USD);
assert!(large.gt(&small).unwrap());
assert!(small.lt(&large).unwrap());
assert!(small.eq(&equal).unwrap());
}
#[test]
fn equality() {
assert!(Money::from_minor(100, test::USD) == Money::from_minor(100, test::USD));
assert!(Money::from_minor(100, test::USD) != Money::from_minor(200, test::USD));
assert!(Money::from_minor(100, test::USD) != Money::from_minor(100, test::GBP));
}
#[test]
fn sign_predicates() {
assert!(Money::from_minor(100, test::USD).is_positive());
assert!(!Money::from_minor(0, test::USD).is_positive());
assert!(!Money::from_minor(-100, test::USD).is_positive());
assert!(Money::from_minor(0, test::USD).is_zero());
assert!(!Money::from_minor(100, test::USD).is_zero());
assert!(!Money::from_minor(-100, test::USD).is_zero());
assert!(Money::from_minor(-100, test::USD).is_negative());
assert!(!Money::from_minor(100, test::USD).is_negative());
assert!(!Money::from_minor(0, test::USD).is_negative());
}
#[test]
fn is_zero_with_negative_zero() {
let neg_zero = Money::from_decimal(-Decimal::ZERO, test::USD);
assert!(neg_zero.is_zero());
assert!(!neg_zero.is_negative());
assert!(!neg_zero.is_positive());
}
#[test]
fn abs_returns_absolute_value() {
let negative = Money::from_minor(-500, test::USD);
assert_eq!(negative.abs(), Money::from_minor(500, test::USD));
let positive = Money::from_minor(500, test::USD);
assert_eq!(positive.abs(), Money::from_minor(500, test::USD));
let zero = Money::from_minor(0, test::USD);
assert_eq!(zero.abs(), Money::from_minor(0, test::USD));
}
#[test]
fn to_minor_units_converts_correctly() {
let usd = Money::from_minor(12345, test::USD);
assert_eq!(usd.to_minor_units(), 12345);
let usd_major = Money::from_major(100, test::USD);
assert_eq!(usd_major.to_minor_units(), 10000);
let jpy = Money::from_major(500, test::JPY);
assert_eq!(jpy.to_minor_units(), 500);
let bhd = Money::from_minor(1234, test::BHD);
assert_eq!(bhd.to_minor_units(), 1234);
let negative = Money::from_minor(-500, test::USD);
assert_eq!(negative.to_minor_units(), -500);
let zero = Money::from_minor(0, test::USD);
assert_eq!(zero.to_minor_units(), 0);
}
#[test]
fn to_f64_lossy_converts_correctly() {
let money = Money::from_minor(12345, test::USD);
assert!((money.to_f64_lossy() - 123.45).abs() < 0.0001);
let whole = Money::from_major(100, test::USD);
assert!((whole.to_f64_lossy() - 100.0).abs() < 0.0001);
let negative = Money::from_minor(-5000, test::USD);
assert!((negative.to_f64_lossy() - (-50.0)).abs() < 0.0001);
let zero = Money::from_minor(0, test::USD);
assert_eq!(zero.to_f64_lossy(), 0.0);
let jpy = Money::from_major(1000, test::JPY);
assert!((jpy.to_f64_lossy() - 1000.0).abs() < 0.0001);
}
}
mod compare_tests {
use super::*;
use std::cmp::Ordering;
#[test]
fn compare_same_currency() {
let a = Money::from_minor(100, test::USD);
let b = Money::from_minor(200, test::USD);
let c = Money::from_minor(100, test::USD);
assert_eq!(a.compare(&b).unwrap(), Ordering::Less);
assert_eq!(b.compare(&a).unwrap(), Ordering::Greater);
assert_eq!(a.compare(&c).unwrap(), Ordering::Equal);
}
#[test]
fn compare_different_currencies() {
let a = Money::from_minor(100, test::USD);
let b = Money::from_minor(100, test::GBP);
let result = a.compare(&b);
assert!(result.is_err());
match result.unwrap_err() {
MoneyError::CurrencyMismatch { expected, actual } => {
assert_eq!(expected, "USD");
assert_eq!(actual, "GBP");
}
_ => panic!("Expected CurrencyMismatch error"),
}
}
#[test]
fn comparison_helpers() {
let small = Money::from_minor(100, test::USD);
let large = Money::from_minor(200, test::USD);
let equal = Money::from_minor(100, test::USD);
assert!(large.gt(&small).unwrap());
assert!(!small.gt(&large).unwrap());
assert!(!small.gt(&equal).unwrap());
assert!(large.gte(&small).unwrap());
assert!(!small.gte(&large).unwrap());
assert!(small.gte(&equal).unwrap());
assert!(small.lt(&large).unwrap());
assert!(!large.lt(&small).unwrap());
assert!(!small.lt(&equal).unwrap());
assert!(small.lte(&large).unwrap());
assert!(!large.lte(&small).unwrap());
assert!(small.lte(&equal).unwrap());
assert!(small.eq(&equal).unwrap());
assert!(!small.eq(&large).unwrap());
}
#[test]
fn helpers_return_error_on_currency_mismatch() {
let usd = Money::from_minor(100, test::USD);
let gbp = Money::from_minor(100, test::GBP);
assert!(usd.gt(&gbp).is_err());
assert!(usd.gte(&gbp).is_err());
assert!(usd.lt(&gbp).is_err());
assert!(usd.lte(&gbp).is_err());
assert!(usd.eq(&gbp).is_err());
}
}
mod allocation {
use super::*;
#[test]
fn allocate_shares() {
let money = Money::from_minor(10000, test::USD);
let allocated = money.allocate(vec![70, 20, 10]).unwrap();
assert_eq!(allocated[0], Money::from_minor(7000, test::USD));
assert_eq!(allocated[1], Money::from_minor(2000, test::USD));
assert_eq!(allocated[2], Money::from_minor(1000, test::USD));
}
#[test]
fn allocate_indivisible_remainder() {
let money = Money::from_minor(1_100, test::USD);
let expected = vec![
Money::from_minor(367, test::USD),
Money::from_minor(367, test::USD),
Money::from_minor(366, test::USD),
];
assert_eq!(expected, money.allocate(vec![1, 1, 1]).unwrap());
assert_eq!(expected, money.split(3).unwrap());
}
#[test]
fn allocate_with_zero_shares() {
let money = Money::from_minor(1_000, test::USD);
let allocated = money.allocate(vec![1, 0, 0]).unwrap();
assert_eq!(allocated[0], Money::from_minor(1_000, test::USD));
assert_eq!(allocated[1], Money::from_minor(0, test::USD));
assert_eq!(allocated[2], Money::from_minor(0, test::USD));
}
#[test]
fn allocate_negative_amount() {
let money = Money::from_minor(-1100, test::USD);
let allocated = money.allocate(vec![1, 1, 1]).unwrap();
let sum: Decimal = allocated.iter().map(|m| *m.amount()).sum();
assert_eq!(sum, *money.amount());
for m in &allocated {
assert!(m.amount() <= &Decimal::ZERO);
}
}
#[test]
fn allocate_zero_amount() {
let money = Money::from_minor(0, test::USD);
let allocated = money.allocate(vec![1, 1, 1]).unwrap();
for m in &allocated {
assert!(m.is_zero());
}
}
#[test]
fn allocate_single_share() {
let money = Money::from_minor(1000, test::USD);
let allocated = money.allocate(vec![1]).unwrap();
assert_eq!(allocated.len(), 1);
assert_eq!(allocated[0], money);
}
#[test]
fn allocate_with_zero_exponent_currency() {
let money = Money::from_major(1000, test::JPY);
let allocated = money.allocate(vec![1, 1, 1]).unwrap();
let sum: Decimal = allocated.iter().map(|m| *m.amount()).sum();
assert_eq!(sum, *money.amount());
assert_eq!(allocated[0], Money::from_major(334, test::JPY));
assert_eq!(allocated[1], Money::from_major(333, test::JPY));
assert_eq!(allocated[2], Money::from_major(333, test::JPY));
}
#[test]
fn allocate_more_shares_than_minor_units() {
let money = Money::from_minor(100, test::USD); let shares: Vec<u32> = vec![1; 101]; let allocated = money.allocate(shares).unwrap();
assert_eq!(allocated.len(), 101);
for m in &allocated[..100] {
assert_eq!(*m.amount(), Decimal::new(1, 2));
}
assert!(allocated[100].is_zero());
}
#[test]
fn allocate_errors() {
let monies = Money::from_minor(100, test::USD).allocate(Vec::new());
assert_eq!(monies.unwrap_err(), MoneyError::InvalidRatio);
let monies = Money::from_minor(100, test::USD).allocate(vec![0, 0, 0]);
assert_eq!(monies.unwrap_err(), MoneyError::InvalidRatio);
let monies = Money::from_minor(100, test::USD).split(0);
assert_eq!(monies.unwrap_err(), MoneyError::InvalidRatio);
}
#[test]
fn split_sum_equals_original() {
let amounts = [1, 2, 3, 7, 10, 11, 100, 101, 999, 1000, 10001, 123456];
let splits = [2, 3, 5, 7, 11, 13];
for &amount in &amounts {
for &n in &splits {
let money = Money::from_minor(amount, test::USD);
let parts = money.split(n).unwrap();
let sum = parts
.iter()
.fold(Money::from_minor(0, test::USD), |acc, &m| {
acc.add(m).unwrap()
});
assert_eq!(
sum.to_minor_units(),
money.to_minor_units(),
"split({}) of {} minor units: sum {} != original {}",
n,
amount,
sum.to_minor_units(),
money.to_minor_units()
);
}
}
}
#[test]
fn allocate_sum_equals_original() {
let test_cases: Vec<(i64, Vec<u32>)> = vec![
(10000, vec![70, 20, 10]), (1100, vec![1, 1, 1]), (100, vec![1, 2, 3, 4]), (7, vec![1, 1, 1, 1, 1, 1, 1]), (1000000, vec![33, 33, 34]), (1, vec![1, 1, 1]), ];
for (amount, shares) in test_cases {
let money = Money::from_minor(amount, test::USD);
let parts = money.allocate(shares.clone()).unwrap();
let sum = parts
.iter()
.fold(Money::from_minor(0, test::USD), |acc, m| {
acc.add(*m).unwrap()
});
assert_eq!(
sum.to_minor_units(),
money.to_minor_units(),
"allocate({:?}) of {} minor units: sum {} != original {}",
shares,
amount,
sum.to_minor_units(),
money.to_minor_units()
);
}
}
#[test]
fn split_sum_equals_original_negative() {
let money = Money::from_minor(-1100, test::USD);
let parts = money.split(3).unwrap();
let sum = parts
.iter()
.fold(Money::from_minor(0, test::USD), |acc, &m| {
acc.add(m).unwrap()
});
assert_eq!(sum.to_minor_units(), money.to_minor_units());
}
#[test]
fn split_sum_equals_original_different_exponents() {
let currencies_and_amounts: Vec<(&test::Currency, i64)> = vec![
(test::USD, 12345), (test::JPY, 12345), (test::BHD, 12345), ];
for (currency, amount) in currencies_and_amounts {
let money = Money::from_minor(amount, currency);
let parts = money.split(7).unwrap();
let sum = parts
.iter()
.fold(Money::from_minor(0, currency), |acc, &m| {
acc.add(m).unwrap()
});
assert_eq!(
sum.to_minor_units(),
money.to_minor_units(),
"Currency {} with exponent {}: sum mismatch",
currency.code(),
currency.exponent()
);
}
}
}
mod rounding {
use super::*;
#[test]
fn precision_and_rounding() {
let expected_money = Money::from_minor(667, test::USD);
let money = Money::from_minor(2_000, test::USD).div(3).unwrap();
assert_eq!(money.round(2, Round::HalfEven), expected_money);
let expected_money = Money::from_minor(6_667, test::BHD);
let money = Money::from_minor(20_000, test::BHD).div(3).unwrap();
assert_eq!(money.round(3, Round::HalfEven), expected_money);
}
#[test]
fn half_up_at_boundary() {
let money = Money::from_str("2.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfUp);
assert_eq!(*rounded.amount(), Decimal::from(3));
let money = Money::from_str("2.40", test::USD).unwrap();
let rounded = money.round(0, Round::HalfUp);
assert_eq!(*rounded.amount(), Decimal::from(2));
}
#[test]
fn half_down_at_boundary() {
let money = Money::from_str("2.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfDown);
assert_eq!(*rounded.amount(), Decimal::from(2));
let money = Money::from_str("2.60", test::USD).unwrap();
let rounded = money.round(0, Round::HalfDown);
assert_eq!(*rounded.amount(), Decimal::from(3));
}
#[test]
fn half_even_rounds_to_even() {
let money = Money::from_str("2.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfEven);
assert_eq!(*rounded.amount(), Decimal::from(2));
let money = Money::from_str("3.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfEven);
assert_eq!(*rounded.amount(), Decimal::from(4));
}
#[test]
fn negative_amounts() {
let money = Money::from_str("-2.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfUp);
assert_eq!(*rounded.amount(), Decimal::from(-3));
let money = Money::from_str("-2.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfDown);
assert_eq!(*rounded.amount(), Decimal::from(-2));
let money = Money::from_str("-2.50", test::USD).unwrap();
let rounded = money.round(0, Round::HalfEven);
assert_eq!(*rounded.amount(), Decimal::from(-2));
}
}
mod formatting {
use super::*;
#[test]
fn separates_digits() {
let usd = Money::from_minor(0, test::USD);
assert_eq!(format!("{}", usd), "$0.00");
let usd = Money::from_minor(10_000_000, test::USD);
assert_eq!(format!("{}", usd), "$100,000.00");
let usd = Money::from_minor(-10_000_000, test::USD);
assert_eq!(format!("{}", usd), "-$100,000.00");
let usd = Money::from_minor(100_000_000_000, test::USD);
assert_eq!(format!("{}", usd), "$1,000,000,000.00");
let inr = Money::from_minor(10_000_000, test::INR);
assert_eq!(format!("{}", inr), "₹1,00,000.00");
let inr = Money::from_minor(-1_000_000_000, test::INR);
assert_eq!(format!("{}", inr), "-₹1,00,00,000.00");
}
#[test]
fn places_symbols_correctly() {
let money = Money::from_minor(0, test::USD);
assert_eq!(format!("{}", money), "$0.00");
let money = Money::from_minor(0, test::AED);
assert_eq!(format!("{}", money), "0.00د.إ");
}
#[test]
fn uses_correct_separators() {
let money = Money::from_minor(100_000, test::EUR);
assert_eq!(format!("{}", money), "€1.000,00");
}
#[test]
fn rounds_exponent() {
let money = Money::from_str("19.9999", test::USD).unwrap();
assert_eq!("$20.00", format!("{}", money));
let money = Money::from_str("29.111", test::USD).unwrap();
assert_eq!("$29.11", format!("{}", money));
let money = Money::from_str("39.1155", test::BHD).unwrap();
assert_eq!("ب.د39.116", format!("{}", money));
}
}
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use crate::Money;
use crate::define_currency_set;
define_currency_set!(
test {
USD: {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 100,
name: "US Dollar",
symbol: "$",
symbol_first: true,
},
EUR: {
code: "EUR",
exponent: 2,
locale: EnEu,
minor_units: 1,
name: "Euro",
symbol: "€",
symbol_first: true,
}
}
);
#[test]
fn serialize_money_to_json() {
let money = Money::from_minor(12345, test::USD);
let json = serde_json::to_string(&money).unwrap();
assert_eq!(json, r#"{"amount":"123.45","currency":"USD"}"#);
}
#[test]
fn serialize_negative_money() {
let money = Money::from_minor(-9999, test::EUR);
let json = serde_json::to_string(&money).unwrap();
assert_eq!(json, r#"{"amount":"-99.99","currency":"EUR"}"#);
}
#[test]
fn deserialize_money_from_json() {
let json = r#"{"amount":"123.45","currency":"USD"}"#;
let money: Money<test::Currency> = serde_json::from_str(json).unwrap();
assert_eq!(money, Money::from_minor(12345, test::USD));
}
#[test]
fn deserialize_money_reversed_field_order() {
let json = r#"{"currency":"EUR","amount":"50.00"}"#;
let money: Money<test::Currency> = serde_json::from_str(json).unwrap();
assert_eq!(money, Money::from_minor(5000, test::EUR));
}
#[test]
fn deserialize_negative_money() {
let json = r#"{"amount":"-99.99","currency":"EUR"}"#;
let money: Money<test::Currency> = serde_json::from_str(json).unwrap();
assert_eq!(money, Money::from_minor(-9999, test::EUR));
}
#[test]
fn roundtrip_serialization() {
let original = Money::from_minor(123456789, test::USD);
let json = serde_json::to_string(&original).unwrap();
let deserialized: Money<test::Currency> = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn deserialize_unknown_currency_fails() {
let json = r#"{"amount":"100.00","currency":"XYZ"}"#;
let result: Result<Money<test::Currency>, _> = serde_json::from_str(json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unknown currency"));
}
#[test]
fn deserialize_missing_amount_fails() {
let json = r#"{"currency":"USD"}"#;
let result: Result<Money<test::Currency>, _> = serde_json::from_str(json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("amount"));
}
#[test]
fn deserialize_missing_currency_fails() {
let json = r#"{"amount":"100.00"}"#;
let result: Result<Money<test::Currency>, _> = serde_json::from_str(json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("currency"));
}
#[test]
fn serialize_preserves_precision() {
let money = Money::from_decimal(
rust_decimal::Decimal::new(123456789012345678, 18),
test::USD,
);
let json = serde_json::to_string(&money).unwrap();
let deserialized: Money<test::Currency> = serde_json::from_str(&json).unwrap();
assert_eq!(money.amount(), deserialized.amount());
}
}
#[cfg(test)]
mod proptest_tests {
use super::*;
use crate::define_currency_set;
use proptest::prelude::*;
use rust_decimal::Decimal;
define_currency_set!(
test {
USD: {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 100,
name: "USD",
symbol: "$",
symbol_first: true,
},
JPY: {
code: "JPY",
exponent: 0,
locale: EnUs,
minor_units: 1,
name: "Japanese Yen",
symbol: "Â¥",
symbol_first: true,
},
BHD: {
code: "BHD",
exponent: 3,
locale: EnUs,
minor_units: 5,
name: "Bahraini Dinar",
symbol: "ب.د",
symbol_first: true,
}
}
);
fn minor_amount() -> impl Strategy<Value = i64> {
-1_000_000_000i64..1_000_000_000i64
}
fn non_zero_multiplier() -> impl Strategy<Value = i64> {
prop_oneof![-1_000_000i64..-1i64, 1i64..1_000_000i64,]
}
fn valid_shares() -> impl Strategy<Value = Vec<u32>> {
prop::collection::vec(0u32..100u32, 1..10)
.prop_filter("at least one non-zero share", |shares| {
shares.iter().any(|&s| s > 0)
})
}
mod arithmetic_properties {
use super::*;
proptest! {
#[test]
fn addition_is_commutative(a in minor_amount(), b in minor_amount()) {
let money_a = Money::from_minor(a, test::USD);
let money_b = Money::from_minor(b, test::USD);
prop_assert_eq!(money_a.add(money_b).unwrap(), money_b.add(money_a).unwrap());
}
#[test]
fn addition_is_associative(a in minor_amount(), b in minor_amount(), c in minor_amount()) {
let money_a = Money::from_minor(a, test::USD);
let money_b = Money::from_minor(b, test::USD);
let money_c = Money::from_minor(c, test::USD);
let left = money_a.add(money_b).unwrap().add(money_c).unwrap();
let right = money_a.add(money_b.add(money_c).unwrap()).unwrap();
prop_assert_eq!(left, right);
}
#[test]
fn zero_is_additive_identity(a in minor_amount()) {
let money = Money::from_minor(a, test::USD);
let zero = Money::from_minor(0, test::USD);
prop_assert_eq!(money.add(zero).unwrap(), money);
prop_assert_eq!(zero.add(money).unwrap(), money);
}
#[test]
fn subtraction_is_inverse_of_addition(a in minor_amount()) {
let money = Money::from_minor(a, test::USD);
let zero = Money::from_minor(0, test::USD);
prop_assert_eq!(money.sub(money).unwrap(), zero);
}
#[test]
fn negation_is_self_inverse(a in minor_amount()) {
let money = Money::from_minor(a, test::USD);
prop_assert_eq!(-(-money), money);
}
#[test]
fn multiplication_by_one_is_identity(a in minor_amount()) {
let money = Money::from_minor(a, test::USD);
prop_assert_eq!(money.mul(1i64).unwrap(), money);
}
#[test]
#[allow(clippy::erasing_op)]
fn multiplication_by_zero_gives_zero(a in minor_amount()) {
let money = Money::from_minor(a, test::USD);
let zero = Money::from_minor(0, test::USD);
prop_assert_eq!(money.mul(0i64).unwrap(), zero);
}
#[test]
fn division_by_one_is_identity(a in minor_amount()) {
let money = Money::from_minor(a, test::USD);
prop_assert_eq!(money.div(1i64).unwrap(), money);
}
#[test]
fn multiplication_then_division_roundtrip(a in minor_amount(), n in non_zero_multiplier()) {
let money = Money::from_minor(a, test::USD);
let multiplied = money.mul(n).unwrap();
let divided = multiplied.div(n).unwrap();
let diff = (*money.amount() - *divided.amount()).abs();
prop_assert!(diff < Decimal::new(1, 10), "difference too large: {}", diff);
}
#[test]
fn distributive_property(a in minor_amount(), n in -1000i64..1000, m in -1000i64..1000) {
let money = Money::from_minor(a, test::USD);
let left = money.mul(n + m).unwrap();
let right = money.mul(n).unwrap().add(money.mul(m).unwrap()).unwrap();
prop_assert_eq!(left, right);
}
}
}
mod allocation_properties {
use super::*;
proptest! {
#[test]
fn allocation_sum_equals_original(amount in minor_amount(), shares in valid_shares()) {
let money = Money::from_minor(amount, test::USD);
let allocated = money.allocate(shares).unwrap();
let sum: Decimal = allocated.iter().map(|m| *m.amount()).sum();
prop_assert_eq!(sum, *money.amount(), "sum of allocations must equal original");
}
#[test]
fn allocation_count_equals_shares_count(amount in minor_amount(), shares in valid_shares()) {
let money = Money::from_minor(amount, test::USD);
let allocated = money.allocate(shares.clone()).unwrap();
prop_assert_eq!(allocated.len(), shares.len());
}
#[test]
fn allocation_preserves_sign_for_positive(amount in 1i64..1_000_000_000, shares in valid_shares()) {
let money = Money::from_minor(amount, test::USD);
let allocated = money.allocate(shares).unwrap();
for m in allocated {
prop_assert!(!m.is_negative(), "positive money should not allocate to negative");
}
}
#[test]
fn zero_allocates_to_zeros(shares in valid_shares()) {
let money = Money::from_minor(0, test::USD);
let allocated = money.allocate(shares).unwrap();
for m in allocated {
prop_assert!(m.is_zero(), "zero money should allocate to zeros");
}
}
#[test]
fn split_is_consistent_with_allocate(amount in minor_amount(), n in 1u32..20) {
let money = Money::from_minor(amount, test::USD);
let shares: Vec<u32> = vec![1; n as usize];
let via_allocate = money.allocate(shares).unwrap();
let via_split = money.split(n).unwrap();
prop_assert_eq!(via_allocate, via_split);
}
#[test]
fn allocation_is_deterministic(amount in minor_amount(), shares in valid_shares()) {
let money = Money::from_minor(amount, test::USD);
let first = money.allocate(shares.clone()).unwrap();
let second = money.allocate(shares).unwrap();
prop_assert_eq!(first, second);
}
#[test]
fn allocation_works_for_zero_exponent_currency(amount in -1_000_000i64..1_000_000, shares in valid_shares()) {
let money = Money::from_major(amount, test::JPY);
let allocated = money.allocate(shares).unwrap();
let sum: Decimal = allocated.iter().map(|m| *m.amount()).sum();
prop_assert_eq!(sum, *money.amount());
}
#[test]
fn allocation_works_for_high_exponent_currency(amount in minor_amount(), shares in valid_shares()) {
let money = Money::from_minor(amount, test::BHD);
let allocated = money.allocate(shares).unwrap();
let sum: Decimal = allocated.iter().map(|m| *m.amount()).sum();
prop_assert_eq!(sum, *money.amount());
}
}
}
mod rounding_properties {
use super::*;
proptest! {
#[test]
fn rounding_is_idempotent(amount in minor_amount(), digits in 0u32..5) {
let money = Money::from_minor(amount, test::USD);
let once = money.round(digits, Round::HalfEven);
let twice = once.round(digits, Round::HalfEven);
prop_assert_eq!(once, twice);
}
#[test]
fn rounding_preserves_sign_except_zero(amount in minor_amount(), digits in 0u32..5) {
let money = Money::from_minor(amount, test::USD);
let rounded = money.round(digits, Round::HalfEven);
if money.is_positive() && !rounded.is_zero() {
prop_assert!(rounded.is_positive() || rounded.is_zero());
} else if money.is_negative() && !rounded.is_zero() {
prop_assert!(rounded.is_negative() || rounded.is_zero());
}
}
#[test]
fn half_up_rounds_away_from_zero_at_midpoint(amount in 1i64..1000) {
let money = Money::from_decimal(
Decimal::new(amount * 10 + 5, 1), test::USD
);
let rounded = money.round(0, Round::HalfUp);
prop_assert_eq!(*rounded.amount(), Decimal::from(amount + 1));
}
#[test]
fn half_down_rounds_toward_zero_at_midpoint(amount in 1i64..1000) {
let money = Money::from_decimal(
Decimal::new(amount * 10 + 5, 1), test::USD
);
let rounded = money.round(0, Round::HalfDown);
prop_assert_eq!(*rounded.amount(), Decimal::from(amount));
}
}
}
mod from_minor_major_properties {
use super::*;
proptest! {
#[test]
fn from_minor_to_major_roundtrip(amount in -1_000_000i64..1_000_000) {
let from_minor = Money::from_minor(amount * 100, test::USD);
let from_major = Money::from_major(amount, test::USD);
prop_assert_eq!(from_minor, from_major);
}
#[test]
fn from_minor_from_major_equivalence_jpy(amount in -1_000_000i64..1_000_000) {
let from_minor = Money::from_minor(amount, test::JPY);
let from_major = Money::from_major(amount, test::JPY);
prop_assert_eq!(from_minor, from_major);
}
#[test]
fn currency_is_preserved(amount in minor_amount()) {
let money = Money::from_minor(amount, test::USD);
prop_assert_eq!(money.currency(), test::USD);
}
}
}
}