use std::{
fmt::Display,
ops::{Add, Div, Mul, Neg, Rem, Sub},
};
use rust_decimal::{prelude::ToPrimitive, Decimal};
use thiserror::Error;
#[cfg(feature = "serde")]
use serde::{ser::SerializeStruct, Serialize};
pub use rust_decimal::RoundingStrategy;
pub mod currency_map;
pub mod iso_currencies;
#[cfg(feature = "formatting")]
pub mod formatting;
pub trait Currency {
fn code(&self) -> &'static str;
fn minor_units(&self) -> u32;
fn numeric_code(&self) -> u32;
}
impl std::fmt::Debug for &dyn Currency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Currency")
.field("code", &self.code())
.finish()
}
}
impl PartialEq for &dyn Currency {
fn eq(&self, other: &Self) -> bool {
self.code() == other.code()
}
}
pub trait MinorUnits {
fn minor_units(&self) -> u32;
}
impl<C> MinorUnits for C
where
C: Currency,
{
fn minor_units(&self) -> u32 {
self.minor_units()
}
}
impl MinorUnits for &dyn Currency {
fn minor_units(&self) -> u32 {
(*self).minor_units()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Money<C> {
amount: Decimal,
currency: C,
}
impl<C> Money<C>
where
C: Copy, {
pub fn new<N: Into<Decimal>>(amount: N, currency: C) -> Self {
Self {
amount: amount.into(),
currency,
}
}
pub fn amount(&self) -> Decimal {
self.amount
}
pub fn is_zero(&self) -> bool {
self.amount.is_zero()
}
pub fn is_positive(&self) -> bool {
self.amount.is_sign_positive()
}
pub fn is_negative(&self) -> bool {
self.amount.is_sign_negative()
}
pub fn round_to_precision(&self, decimal_places: u32, strategy: RoundingStrategy) -> Self {
Self {
amount: self.amount.round_dp_with_strategy(decimal_places, strategy),
currency: self.currency,
}
}
}
impl<C> Money<C>
where
C: MinorUnits + Copy,
{
pub fn from_minor_units(minor_units: i64, currency: C) -> Self {
Self {
amount: Decimal::new(minor_units, currency.minor_units()),
currency,
}
}
pub fn round(&self, strategy: RoundingStrategy) -> Self {
Self {
amount: self
.amount
.round_dp_with_strategy(self.currency.minor_units(), strategy),
currency: self.currency,
}
}
pub fn to_minor_units(&self, rounding_strategy: RoundingStrategy) -> Option<i64> {
let num_minor_units = self.currency.minor_units();
let multiplier = Decimal::from(10_u64.pow(num_minor_units));
self.amount
.round_dp_with_strategy(num_minor_units, rounding_strategy)
.mul(multiplier)
.to_i64()
}
}
impl<C> Money<C>
where
C: Currency + Copy,
{
pub fn currency(&self) -> C {
self.currency
}
}
impl Money<&dyn Currency> {
pub fn currency(&self) -> &dyn Currency {
self.currency
}
}
impl<C> PartialEq for Money<C>
where
C: Currency + PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.amount == other.amount
}
}
impl<C> PartialEq<Money<&dyn Currency>> for Money<C>
where
C: Currency + PartialEq,
{
fn eq(&self, other: &Money<&dyn Currency>) -> bool {
self.amount == other.amount && self.currency.code() == other.currency.code()
}
}
impl PartialEq for Money<&dyn Currency> {
fn eq(&self, other: &Self) -> bool {
self.amount == other.amount && self.currency.code() == other.currency.code()
}
}
impl<C> PartialEq<Money<C>> for Money<&dyn Currency>
where
C: Currency,
{
fn eq(&self, other: &Money<C>) -> bool {
self.amount == other.amount && self.currency.code() == other.currency.code()
}
}
#[derive(Debug, Error, PartialEq, Clone)]
pub enum MoneyMathError {
#[error("the money instances have incompatible currencies ({0}, {1})")]
IncompatibleCurrencies(&'static str, &'static str),
}
macro_rules! impl_binary_op {
($trait:ident, $method:ident) => {
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for Money instances with a static currency."]
impl<C> $trait for Money<C>
where
C: Currency,
{
type Output = Self;
fn $method(self, rhs: Self) -> Self::Output {
Self {
amount: self.amount.$method(rhs.amount),
currency: self.currency,
}
}
}
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for two Money instances with dynamically-typed currencies."]
#[doc = " The Output is a Result instead of a Money since the operation"]
#[doc = " can fail if the currencies are incompatible."]
impl $trait for Money<&dyn Currency> {
type Output = Result<Self, MoneyMathError>;
fn $method(self, rhs: Self) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(Self {
amount: self.amount.$method(rhs.amount),
currency: self.currency,
})
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for a Money instance with a dynamically-typed Currency"]
#[doc = " and a Money instance with a statically-typed Currency. The output"]
#[doc = " is a Result since the operation can fail if the currencies are incompatible."]
impl<C> $trait<Money<C>> for Money<&dyn Currency>
where
C: Currency,
{
type Output = Result<Self, MoneyMathError>;
fn $method(self, rhs: Money<C>) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(Self {
amount: self.amount.$method(rhs.amount),
currency: self.currency,
})
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for a Money instance with a statically-typed Currency"]
#[doc = " and a Money instance with a dynamically-typed Currency. The output"]
#[doc = " is a Result since the operation can fail if the currencies are incompatible."]
impl<C> $trait<Money<&dyn Currency>> for Money<C>
where
C: Currency,
{
type Output = Result<Self, MoneyMathError>;
fn $method(self, rhs: Money<&dyn Currency>) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(Self {
amount: self.amount.$method(rhs.amount),
currency: self.currency,
})
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
};
}
impl_binary_op!(Add, add);
impl_binary_op!(Sub, sub);
macro_rules! impl_binary_numeric_op {
($trait:ident, $method:ident) => {
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for Money instances with a static currency."]
#[doc = " The right-hand-side of the operation can be"]
#[doc = " anything that can be converted into a Decimal."]
impl<C, N> $trait<N> for Money<C>
where
C: Currency,
N: Into<Decimal>,
{
type Output = Self;
fn $method(self, rhs: N) -> Self::Output {
Self {
amount: self.amount.$method(rhs.into()),
currency: self.currency,
}
}
}
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for Money instances with a dynamic currency."]
#[doc = " The right-hand-side of the operation can be"]
#[doc = " anything that can be converted into a Decimal."]
impl<N> $trait<N> for Money<&dyn Currency>
where
N: Into<Decimal>,
{
type Output = Self;
fn $method(self, rhs: N) -> Self::Output {
Self {
amount: self.amount.$method(rhs.into()),
currency: self.currency,
}
}
}
};
}
impl_binary_numeric_op!(Mul, mul);
impl_binary_numeric_op!(Div, div);
impl_binary_numeric_op!(Rem, rem);
macro_rules! impl_unary_op {
($trait:ident, $method:ident) => {
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for Money instances with a static currency."]
impl<C> $trait for Money<C>
where
C: Currency,
{
type Output = Self;
fn $method(self) -> Self::Output {
Self {
amount: self.amount.$method(),
currency: self.currency,
}
}
}
#[doc = "Supports "]
#[doc = stringify!($trait)]
#[doc = " for Money instances with a dynamic currency."]
impl $trait for Money<&dyn Currency> {
type Output = Self;
fn $method(self) -> Self::Output {
Self {
amount: self.amount.$method(),
currency: self.currency,
}
}
}
};
}
impl_unary_op!(Neg, neg);
impl<C> Div for Money<C>
where
C: Currency,
{
type Output = Decimal;
fn div(self, rhs: Self) -> Self::Output {
self.amount.div(rhs.amount)
}
}
impl Div for Money<&dyn Currency> {
type Output = Result<Decimal, MoneyMathError>;
fn div(self, rhs: Self) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(self.amount.div(rhs.amount))
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
impl<C> Div<Money<C>> for Money<&dyn Currency>
where
C: Currency,
{
type Output = Result<Decimal, MoneyMathError>;
fn div(self, rhs: Money<C>) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(self.amount.div(rhs.amount))
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
impl<C> Div<Money<&dyn Currency>> for Money<C>
where
C: Currency,
{
type Output = Result<Decimal, MoneyMathError>;
fn div(self, rhs: Money<&dyn Currency>) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(self.amount.div(rhs.amount))
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
impl<C> PartialOrd for Money<C>
where
C: Currency + PartialOrd,
{
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.amount.partial_cmp(&other.amount)
}
}
impl PartialOrd for Money<&dyn Currency> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.currency.code() == other.currency.code() {
self.amount.partial_cmp(&other.amount)
} else {
None
}
}
}
#[cfg(feature = "serde")]
impl<C> Serialize for Money<C>
where
C: Currency,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut struct_serializer = serializer.serialize_struct("money", 2)?;
struct_serializer.serialize_field("amount", &self.amount.to_string())?;
struct_serializer.serialize_field("currency", &self.currency.code())?;
struct_serializer.end()
}
}
#[cfg(feature = "serde")]
impl Serialize for Money<&dyn Currency> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut struct_serializer = serializer.serialize_struct("money", 2)?;
struct_serializer.serialize_field("amount", &self.amount.to_string())?;
struct_serializer.serialize_field("currency", &self.currency.code())?;
struct_serializer.end()
}
}
impl<C> Display for Money<C>
where
C: Currency,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.amount, self.currency.code())
}
}
impl Display for Money<&dyn Currency> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.amount, self.currency.code())
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use super::*;
use crate::iso_currencies::{JPY, USD};
use currency_map::CurrencyMap;
use rust_decimal::Decimal;
const CURRENCIES: LazyLock<CurrencyMap> =
LazyLock::new(|| CurrencyMap::from_collection([&USD as &dyn Currency, &JPY]));
#[test]
fn new_static() {
let m1 = Money::new(Decimal::ONE, USD);
assert_eq!(m1.amount(), Decimal::ONE);
assert_eq!(m1.currency, USD);
let m2 = Money::new(Decimal::TWO, JPY);
assert_eq!(m2.amount(), Decimal::TWO);
assert_eq!(m2.currency, JPY);
let m3 = Money::new(1, USD);
assert_eq!(m3.amount(), Decimal::ONE);
assert_eq!(m3.currency, USD);
}
#[test]
fn equality_static() {
assert_eq!(Money::new(Decimal::ONE, USD), Money::new(Decimal::ONE, USD));
assert_eq!(Money::new(Decimal::ONE, JPY), Money::new(Decimal::ONE, JPY));
assert_ne!(Money::new(Decimal::ONE, USD), Money::new(Decimal::TWO, USD));
assert_ne!(Money::new(Decimal::ONE, JPY), Money::new(Decimal::TWO, JPY));
}
#[test]
fn new_dynamic() {
let m1 = Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap());
assert_eq!(m1.amount(), Decimal::ONE);
assert_eq!(m1.currency(), &USD);
let m2 = Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap());
assert_eq!(m2.amount(), Decimal::ONE);
assert_eq!(m2.currency(), &JPY);
let m3 = Money::new(1, CURRENCIES.get("USD").unwrap());
assert_eq!(m3.amount(), Decimal::ONE);
assert_eq!(m3.currency(), &USD);
}
#[test]
fn from_minor_units() {
let m1 = Money::from_minor_units(100, USD);
assert_eq!(m1.amount(), Decimal::ONE);
assert_eq!(m1.currency(), USD);
let m2 = Money::from_minor_units(100, JPY);
assert_eq!(m2.amount(), Decimal::ONE_HUNDRED);
assert_eq!(m2.currency(), JPY);
let currency_jpy = CURRENCIES.get("JPY").unwrap();
let m3 = Money::from_minor_units(100, currency_jpy);
assert_eq!(m3.amount(), Decimal::ONE_HUNDRED);
assert_eq!(m3.currency(), currency_jpy);
}
#[test]
fn is_zero() {
assert!(Money::new(Decimal::ZERO, USD).is_zero());
assert!(Money::new(Decimal::ZERO, CURRENCIES.get("USD").unwrap()).is_zero());
}
#[test]
fn equality_dynamic() {
assert_eq!(
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
);
assert_eq!(
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
Money::new(Decimal::TWO, CURRENCIES.get("USD").unwrap()),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
Money::new(Decimal::TWO, CURRENCIES.get("JPY").unwrap()),
);
}
#[test]
fn equality_mixed() {
assert_eq!(
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
Money::new(Decimal::ONE, USD),
);
assert_eq!(
Money::new(Decimal::ONE, USD),
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
);
assert_eq!(
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
Money::new(Decimal::ONE, JPY),
);
assert_eq!(
Money::new(Decimal::ONE, JPY),
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
Money::new(Decimal::ONE, JPY),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap()),
Money::new(Decimal::ONE, USD),
);
assert_ne!(
Money::new(Decimal::ONE, CURRENCIES.get("USD").unwrap()),
Money::new(Decimal::TWO, USD),
);
}
#[test]
fn add_static() {
assert_eq!(
Money::new(Decimal::ONE, USD) + Money::new(Decimal::ONE, USD),
Money::new(Decimal::TWO, USD),
);
assert_eq!(
Money::new(Decimal::ONE, JPY) + Money::new(Decimal::ONE, JPY),
Money::new(Decimal::TWO, JPY),
);
assert_eq!(
Money::new(Decimal::ONE, USD)
+ Money::new(Decimal::ONE, USD)
+ Money::new(Decimal::ONE, USD),
Money::new(Decimal::new(3, 0), USD),
);
}
#[test]
fn add_dynamic() {
let currency_usd = CURRENCIES.get("USD").unwrap();
let currency_jpy = CURRENCIES.get("JPY").unwrap();
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_usd),
Ok(Money::new(Decimal::TWO, currency_usd)),
);
assert_eq!(
Money::new(Decimal::ONE, currency_jpy) + Money::new(Decimal::ONE, currency_jpy),
Ok(Money::new(Decimal::TWO, currency_jpy)),
);
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_jpy),
Err(MoneyMathError::IncompatibleCurrencies(
currency_usd.code(),
currency_jpy.code(),
)),
);
assert_eq!(
Money::new(Decimal::ONE, currency_jpy) + Money::new(Decimal::ONE, currency_usd),
Err(MoneyMathError::IncompatibleCurrencies(
currency_jpy.code(),
currency_usd.code(),
)),
);
assert_eq!(
(Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_usd))
.and_then(|m| m + Money::new(Decimal::ONE, currency_usd)),
Ok(Money::new(Decimal::new(3, 0), currency_usd)),
);
assert_eq!(
(Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_jpy))
.and_then(|m| m + Money::new(Decimal::ONE, currency_usd)),
Err(MoneyMathError::IncompatibleCurrencies(
currency_usd.code(),
currency_jpy.code()
)),
);
}
#[test]
fn add_mixed() {
let currency_usd = CURRENCIES.get("USD").unwrap();
let currency_jpy = CURRENCIES.get("JPY").unwrap();
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, USD),
Ok(Money::new(Decimal::TWO, currency_usd)),
);
assert_eq!(
Money::new(Decimal::ONE, currency_jpy) + Money::new(Decimal::ONE, JPY),
Ok(Money::new(Decimal::TWO, currency_jpy)),
);
assert_eq!(
Money::new(Decimal::ONE, JPY) + Money::new(Decimal::ONE, currency_jpy),
Ok(Money::new(Decimal::TWO, JPY)),
);
assert_eq!(
Money::new(Decimal::ONE, USD) + Money::new(Decimal::ONE, currency_usd),
Ok(Money::new(Decimal::TWO, USD)),
);
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, JPY),
Err(MoneyMathError::IncompatibleCurrencies(
currency_usd.code(),
JPY.code()
)),
);
assert_eq!(
Money::new(Decimal::ONE, USD) + Money::new(Decimal::ONE, currency_jpy),
Err(MoneyMathError::IncompatibleCurrencies(
USD.code(),
currency_jpy.code()
)),
);
}
#[test]
fn subtract() {
assert_eq!(
Money::new(Decimal::TWO, USD) - Money::new(Decimal::ONE, USD),
Money::new(Decimal::ONE, USD)
);
assert_eq!(
Money::new(Decimal::ONE, USD) - Money::new(Decimal::TWO, USD),
Money::new(Decimal::NEGATIVE_ONE, USD)
);
let currency_usd = CURRENCIES.get("USD").unwrap();
let currency_jpy = CURRENCIES.get("JPY").unwrap();
assert_eq!(
Money::new(Decimal::TWO, currency_usd) - Money::new(Decimal::ONE, currency_usd),
Ok(Money::new(Decimal::ONE, currency_usd))
);
assert_eq!(
Money::new(Decimal::ONE, currency_usd) - Money::new(Decimal::TWO, currency_usd),
Ok(Money::new(Decimal::NEGATIVE_ONE, currency_usd))
);
assert_eq!(
Money::new(Decimal::TWO, currency_jpy) - Money::new(Decimal::ONE, currency_usd),
Err(MoneyMathError::IncompatibleCurrencies("JPY", "USD"))
);
assert_eq!(
Money::new(Decimal::TWO, currency_usd) - Money::new(Decimal::ONE, USD),
Ok(Money::new(Decimal::ONE, currency_usd))
);
assert_eq!(
Money::new(Decimal::ONE, USD) - Money::new(Decimal::TWO, currency_usd),
Ok(Money::new(Decimal::NEGATIVE_ONE, USD))
);
assert_eq!(
Money::new(Decimal::TWO, JPY) - Money::new(Decimal::ONE, currency_usd),
Err(MoneyMathError::IncompatibleCurrencies("JPY", "USD"))
);
assert_eq!(
Money::new(Decimal::TWO, currency_jpy) - Money::new(Decimal::ONE, USD),
Err(MoneyMathError::IncompatibleCurrencies("JPY", "USD"))
);
}
#[test]
fn multiply() {
assert_eq!(Money::new(10, USD) * 10, Money::new(100, USD));
assert_eq!(Money::new(10, USD) * Decimal::TEN, Money::new(100, USD));
let currency_usd = CURRENCIES.get("USD").unwrap();
assert_eq!(
Money::new(10, currency_usd) * 10,
Money::new(100, currency_usd)
);
assert_eq!(
Money::new(10, currency_usd) * Decimal::TEN,
Money::new(100, currency_usd)
);
}
#[test]
fn divide() {
assert_eq!(
Money::new(Decimal::TEN, USD) / 2,
Money::new(Decimal::new(5, 0), USD)
);
assert_eq!(
Money::new(Decimal::TEN, USD) / Decimal::TWO,
Money::new(Decimal::new(5, 0), USD)
);
assert_eq!(
Money::new(Decimal::TWO, USD) / Decimal::TEN,
Money::new(Decimal::new(2, 1), USD)
);
let currency_usd = CURRENCIES.get("USD").unwrap();
assert_eq!(
Money::new(Decimal::TEN, currency_usd) / 2,
Money::new(Decimal::new(5, 0), currency_usd)
);
assert_eq!(
Money::new(Decimal::TEN, currency_usd) / Decimal::TWO,
Money::new(Decimal::new(5, 0), currency_usd)
);
assert_eq!(
Money::new(Decimal::TWO, currency_usd) / Decimal::TEN,
Money::new(Decimal::new(2, 1), currency_usd)
);
}
#[test]
fn divide_other_money() {
assert_eq!(Money::new(3, USD) / Money::new(2, USD), Decimal::new(15, 1));
let currency_usd = CURRENCIES.get("USD").unwrap();
assert_eq!(
Money::new(3, currency_usd) / Money::new(2, currency_usd),
Ok(Decimal::new(15, 1))
);
let currency_jpy = CURRENCIES.get("JPY").unwrap();
assert_eq!(
Money::new(3, currency_usd) / Money::new(2, currency_jpy),
Err(MoneyMathError::IncompatibleCurrencies("USD", "JPY"))
);
assert_eq!(
Money::new(3, currency_usd) / Money::new(2, USD),
Ok(Decimal::new(15, 1))
);
assert_eq!(
Money::new(3, currency_usd) / Money::new(2, JPY),
Err(MoneyMathError::IncompatibleCurrencies("USD", "JPY"))
);
}
#[test]
fn rem() {
assert_eq!(
Money::new(Decimal::TEN, USD) % 10,
Money::new(Decimal::ZERO, USD)
);
assert_eq!(
Money::new(Decimal::TEN, USD) % Decimal::TEN,
Money::new(Decimal::ZERO, USD)
);
let currency_usd = CURRENCIES.get("USD").unwrap();
assert_eq!(
Money::new(Decimal::TEN, currency_usd) % 10,
Money::new(Decimal::ZERO, currency_usd)
);
assert_eq!(
Money::new(Decimal::TEN, currency_usd) % Decimal::TEN,
Money::new(Decimal::ZERO, currency_usd)
);
}
#[test]
fn negate() {
assert_eq!(
-Money::new(Decimal::ONE, USD),
Money::new(Decimal::NEGATIVE_ONE, USD)
);
let currency_usd = CURRENCIES.get("USD").unwrap();
assert_eq!(
-Money::new(Decimal::ONE, currency_usd),
Money::new(Decimal::NEGATIVE_ONE, currency_usd)
);
}
#[test]
fn is_positive_negative() {
assert!(Money::new(Decimal::ONE, USD).is_positive());
assert!((-Money::new(Decimal::ONE, USD)).is_negative());
let currency_usd = CURRENCIES.get("USD").unwrap();
assert!(Money::new(Decimal::ONE, currency_usd).is_positive());
assert!((-Money::new(Decimal::ONE, currency_usd)).is_negative());
assert!(Money::new(Decimal::ZERO, USD).is_positive());
assert!(!Money::new(Decimal::ZERO, USD).is_negative());
}
#[test]
fn round() {
assert_eq!(
Money::new(Decimal::new(1555, 3), USD).round(RoundingStrategy::MidpointNearestEven),
Money::new(Decimal::new(156, 2), USD)
);
assert_eq!(
Money::new(Decimal::new(1555, 3), USD).round(RoundingStrategy::MidpointTowardZero),
Money::new(Decimal::new(155, 2), USD)
);
}
#[test]
fn round_to_precision() {
assert_eq!(
Money::new(Decimal::new(15, 1), USD)
.round_to_precision(0, RoundingStrategy::MidpointNearestEven),
Money::new(Decimal::TWO, USD)
);
assert_eq!(
Money::new(Decimal::new(15, 1), USD)
.round_to_precision(0, RoundingStrategy::MidpointTowardZero),
Money::new(Decimal::ONE, USD)
);
}
#[test]
fn partial_ord() {
assert!(Money::new(Decimal::ONE, USD) < Money::new(Decimal::TWO, USD));
assert!(Money::new(Decimal::TWO, USD) > Money::new(Decimal::ONE, USD));
let currency_usd = CURRENCIES.get("USD").unwrap();
let currency_jpy = CURRENCIES.get("JPY").unwrap();
assert!(Money::new(Decimal::ONE, currency_usd) < Money::new(Decimal::TWO, currency_usd));
assert!(Money::new(Decimal::TWO, currency_usd) > Money::new(Decimal::ONE, currency_usd));
assert_eq!(
Money::new(Decimal::ONE, currency_usd)
.partial_cmp(&Money::new(Decimal::TWO, currency_jpy)),
None
);
assert!(!(Money::new(Decimal::ONE, currency_usd) < Money::new(Decimal::TWO, currency_jpy)));
assert!(!(Money::new(Decimal::TWO, currency_usd) > Money::new(Decimal::ONE, currency_jpy)));
}
#[test]
fn to_string() {
assert_eq!(
Money::new(Decimal::ONE_THOUSAND, USD).to_string(),
"1000 USD"
);
assert_eq!(
Money::new(Decimal::ONE_THOUSAND, &USD as &dyn Currency).to_string(),
"1000 USD"
);
}
#[test]
#[cfg(feature = "serde")]
fn serialize() {
let expected = "{\"amount\":\"1\",\"currency\":\"USD\"}".to_string();
let json = serde_json::to_string(&Money::new(Decimal::ONE, USD)).unwrap();
assert_eq!(json, expected);
let json = serde_json::to_string(&Money::new(Decimal::ONE, &USD as &dyn Currency)).unwrap();
assert_eq!(json, expected);
}
#[test]
fn to_minor_units() {
let m = Money::new(Decimal::new(1045, 2), USD);
assert_eq!(
Some(1045),
m.to_minor_units(RoundingStrategy::MidpointNearestEven)
);
let m = Money::new(Decimal::new(1045, 0), JPY);
assert_eq!(
Some(1045),
m.to_minor_units(RoundingStrategy::MidpointNearestEven)
);
}
#[test]
fn to_minor_units_rounding() {
let m = Money::new(Decimal::new(104567, 4), USD);
assert_eq!(
Some(1046),
m.to_minor_units(RoundingStrategy::MidpointNearestEven)
);
let m = Money::new(Decimal::new(104567, 4), JPY);
assert_eq!(
Some(10),
m.to_minor_units(RoundingStrategy::MidpointNearestEven)
);
}
}