use crate::currency::Currency;
use crate::error::Error;
use crate::fractional_money::FractionalMoney;
use rust_decimal::Decimal;
use std::fmt::{Display, Formatter};
use std::ops::{Add, Div, Mul, Sub};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Money {
amount: Decimal,
currency: Currency,
}
impl Money {
pub fn new(value: Decimal, currency: Currency) -> Result<Self, Error> {
let normed_amt = validate_and_normalize(value, currency)?;
Ok(Money {
currency,
amount: normed_amt,
})
}
pub fn amount(&self) -> Decimal {
self.amount
}
pub fn currency(&self) -> Currency {
self.currency
}
pub fn try_add(&self, rhs: &Self) -> Result<Self, Error> {
if self.currency != rhs.currency {
return Err(Error::MismatchedCurrency);
}
Ok(Self {
currency: self.currency,
amount: self.amount + rhs.amount,
})
}
pub fn try_subtract(&self, rhs: &Self) -> Result<Self, Error> {
if self.currency != rhs.currency {
return Err(Error::MismatchedCurrency);
}
Ok(Self {
currency: self.currency,
amount: self.amount - rhs.amount,
})
}
}
impl Display for Money {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {:?}", self.amount, self.currency)
}
}
impl Add for Money {
type Output = Money;
fn add(self, rhs: Self) -> Self::Output {
self.try_add(&rhs).unwrap()
}
}
impl Sub for Money {
type Output = Money;
fn sub(self, rhs: Self) -> Self::Output {
self.try_subtract(&rhs).unwrap()
}
}
impl Mul<Decimal> for Money {
type Output = FractionalMoney;
fn mul(self, rhs: Decimal) -> Self::Output {
let frac: FractionalMoney = self.into();
frac * rhs
}
}
impl Div<Decimal> for Money {
type Output = FractionalMoney;
fn div(self, rhs: Decimal) -> Self::Output {
let frac: FractionalMoney = self.into();
frac / rhs
}
}
fn validate_and_normalize(amt: Decimal, currency: Currency) -> Result<Decimal, Error> {
match currency {
Currency::USD | Currency::CAD => {
let scale = amt.scale();
if scale != 0 && scale != 2 {
return Err(Error::InvalidMoneyValue(format!(
"expected 0 or 2 decimal places for {currency:?}, but '{amt}' has {scale}"
)));
}
let mut value = amt;
value.rescale(2);
Ok(value)
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
use crate::{cad, usd};
use anyhow::Result;
use expecting::*;
use rust_decimal_macros::dec;
#[test]
fn new__usd__2_decimals() -> Result<()> {
let a = expect_ok!(Money::new(dec!(0.00), Currency::USD));
expect_eq!(a.to_string(), "0.00 USD");
expect_eq!(a.amount().to_string(), "0.00");
let a = expect_ok!(Money::new(dec!(1.00), Currency::USD));
expect_eq!(a.to_string(), "1.00 USD");
expect_eq!(a.amount().to_string(), "1.00");
let a = expect_ok!(Money::new(dec!(-1.00), Currency::USD));
expect_eq!(a.to_string(), "-1.00 USD");
expect_eq!(a.amount().to_string(), "-1.00");
let a = expect_ok!(Money::new(dec!(13.37), Currency::USD));
expect_eq!(a.to_string(), "13.37 USD");
expect_eq!(a.amount().to_string(), "13.37");
Ok(())
}
#[test]
fn new__usd__0_decimals() -> Result<()> {
let a = expect_ok!(Money::new(dec!(0), Currency::USD));
expect_eq!(a.to_string(), "0.00 USD");
expect_eq!(a.amount().to_string(), "0.00");
let a = expect_ok!(Money::new(dec!(1), Currency::USD));
expect_eq!(a.to_string(), "1.00 USD");
expect_eq!(a.amount().to_string(), "1.00");
let a = expect_ok!(Money::new(dec!(-1), Currency::USD));
expect_eq!(a.to_string(), "-1.00 USD");
expect_eq!(a.amount().to_string(), "-1.00");
Ok(())
}
#[test]
fn new__usd__1_decimals__fails() -> Result<()> {
expect_err!(Money::new(dec!(0.0), Currency::USD));
expect_err!(Money::new(dec!(0.1), Currency::USD));
expect_err!(Money::new(dec!(1.1), Currency::USD));
expect_err!(Money::new(dec!(-1.1), Currency::USD));
Ok(())
}
#[test]
fn new__usd__gt_2_decimals__fails() -> Result<()> {
expect_err!(Money::new(dec!(0.000), Currency::USD));
expect_err!(Money::new(dec!(0.101), Currency::USD));
expect_err!(Money::new(dec!(1.111), Currency::USD));
expect_err!(Money::new(dec!(-1.110), Currency::USD));
expect_err!(Money::new(dec!(-1.1102345), Currency::USD));
Ok(())
}
#[test]
fn new__cad__2_decimals() -> Result<()> {
let a = expect_ok!(Money::new(dec!(0.00), Currency::CAD));
expect_eq!(a.to_string(), "0.00 CAD");
expect_eq!(a.amount().to_string(), "0.00");
let a = expect_ok!(Money::new(dec!(1.00), Currency::CAD));
expect_eq!(a.to_string(), "1.00 CAD");
expect_eq!(a.amount().to_string(), "1.00");
let a = expect_ok!(Money::new(dec!(-1.00), Currency::CAD));
expect_eq!(a.to_string(), "-1.00 CAD");
expect_eq!(a.amount().to_string(), "-1.00");
let a = expect_ok!(Money::new(dec!(13.37), Currency::CAD));
expect_eq!(a.to_string(), "13.37 CAD");
expect_eq!(a.amount().to_string(), "13.37");
Ok(())
}
#[test]
fn new__cad__0_decimals() -> Result<()> {
let a = expect_ok!(Money::new(dec!(0), Currency::CAD));
expect_eq!(a.to_string(), "0.00 CAD");
expect_eq!(a.amount().to_string(), "0.00");
let a = expect_ok!(Money::new(dec!(1), Currency::CAD));
expect_eq!(a.to_string(), "1.00 CAD");
expect_eq!(a.amount().to_string(), "1.00");
let a = expect_ok!(Money::new(dec!(-1), Currency::CAD));
expect_eq!(a.to_string(), "-1.00 CAD");
expect_eq!(a.amount().to_string(), "-1.00");
Ok(())
}
#[test]
fn new__cad__1_decimals__fails() -> Result<()> {
expect_err!(Money::new(dec!(0.0), Currency::CAD));
expect_err!(Money::new(dec!(0.1), Currency::CAD));
expect_err!(Money::new(dec!(1.1), Currency::CAD));
expect_err!(Money::new(dec!(-1.1), Currency::CAD));
Ok(())
}
#[test]
fn new__cad__gt_2_decimals__fails() -> Result<()> {
expect_err!(Money::new(dec!(0.000), Currency::CAD));
expect_err!(Money::new(dec!(0.101), Currency::CAD));
expect_err!(Money::new(dec!(1.111), Currency::CAD));
expect_err!(Money::new(dec!(-1.110), Currency::CAD));
expect_err!(Money::new(dec!(-1.1102345), Currency::CAD));
Ok(())
}
#[test]
fn add__matching_currency() -> Result<()> {
expect_eq!(usd!(1) + usd!(2.99), usd!(3.99));
expect_eq!(usd!(1) + usd!(-2.99), usd!(-1.99));
expect_eq!(cad!(1) + cad!(-1), cad!(0));
Ok(())
}
#[test]
#[should_panic]
fn add__mismatched_currencies__panics() {
let _ = usd!(1) + cad!(2.99);
}
#[test]
fn try_add__mismatched_currency__returns_err() -> Result<()> {
expect_err!(usd!(1).try_add(&cad!(1)));
Ok(())
}
#[test]
fn subtract__matching_currency() -> Result<()> {
expect_eq!(usd!(1) - usd!(2.99), usd!(-1.99));
expect_eq!(usd!(1) - usd!(-2.99), usd!(3.99));
expect_eq!(cad!(1) - cad!(1), cad!(0));
Ok(())
}
#[test]
#[should_panic]
fn subtract__mismatched_currencies__panics() {
let _ = usd!(1) - cad!(0.50);
}
#[test]
fn try_subtract__mismatched_currency__returns_err() -> Result<()> {
expect_err!(usd!(1).try_subtract(&cad!(1)));
Ok(())
}
#[test]
fn multiply() -> Result<()> {
let product = usd!(2.23) * dec!(2);
expect_eq!(product.round(), usd!(4.46));
Ok(())
}
#[test]
fn divide() -> Result<()> {
let quotient = usd!(2.23) / dec!(2);
expect_eq!(quotient.round(), usd!(1.12));
Ok(())
}
}