use crate::currency::Currency;
use crate::errors::QlResult;
use crate::require;
use crate::types::Real;
use std::fmt;
use std::ops::{Div, Mul, Neg};
#[derive(Clone, Debug, PartialEq)]
pub struct Money {
currency: Currency,
value: Real,
}
impl Money {
pub fn new(currency: Currency, value: Real) -> Self {
Self { currency, value }
}
pub fn checked_new(currency: Currency, value: Real) -> QlResult<Self> {
require!(value.is_finite(), "money amount must be finite");
Ok(Self::new(currency, value))
}
pub fn currency(&self) -> &Currency {
&self.currency
}
pub fn value(&self) -> Real {
self.value
}
pub fn checked_add(&self, rhs: &Money) -> QlResult<Money> {
require!(
self.currency == rhs.currency,
"currency mismatch: {} vs {}",
self.currency.code(),
rhs.currency.code()
);
require!(
self.value.is_finite() && rhs.value.is_finite(),
"money amounts must be finite"
);
Money::checked_new(self.currency.clone(), self.value + rhs.value)
}
pub fn checked_mul(&self, rhs: Real) -> QlResult<Money> {
require!(
self.value.is_finite() && rhs.is_finite(),
"money and scalar must be finite"
);
Money::checked_new(self.currency.clone(), self.value * rhs)
}
pub fn checked_div(&self, rhs: Real) -> QlResult<Money> {
require!(
rhs.is_finite() && rhs != 0.0,
"divisor must be finite and nonzero"
);
require!(self.value.is_finite(), "money amount must be finite");
Money::checked_new(self.currency.clone(), self.value / rhs)
}
pub fn checked_ratio(&self, rhs: &Money) -> QlResult<Real> {
require!(self.currency == rhs.currency, "currency mismatch");
Ok(self.checked_div(rhs.value)?.value())
}
pub fn checked_sub(&self, rhs: &Money) -> QlResult<Money> {
self.checked_add(&Money::new(rhs.currency.clone(), -rhs.value))
}
}
impl Neg for Money {
type Output = Money;
fn neg(self) -> Money {
Money::new(self.currency, -self.value)
}
}
impl Mul<Real> for Money {
type Output = Money;
fn mul(self, rhs: Real) -> Money {
Money::new(self.currency, self.value * rhs)
}
}
impl Div<Real> for Money {
type Output = Money;
fn div(self, rhs: Real) -> Money {
Money::new(self.currency, self.value / rhs)
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.value, self.currency.code())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quantlib_no_conversion_arithmetic() {
let m1 = Money::new(Currency::eur(), 50000.0);
let m2 = Money::new(Currency::eur(), 100000.0);
let m3 = Money::new(Currency::eur(), 500000.0);
let result = m1
.checked_mul(3.0)
.unwrap()
.checked_add(&m2.checked_mul(2.5).unwrap())
.unwrap()
.checked_sub(&m3.checked_div(5.0).unwrap())
.unwrap()
.checked_add(&m1.checked_mul(m2.checked_ratio(&m3).unwrap()).unwrap())
.unwrap();
assert_eq!(result.value(), 310000.0);
}
#[test]
fn checked_operations_reject_invalid_and_overflowing_amounts() {
let amount = Money::new(Currency::eur(), Real::MAX);
assert!(amount.checked_add(&amount).is_err());
assert!(amount.checked_mul(2.0).is_err());
assert!(amount.checked_div(0.0).is_err());
assert!(amount.checked_mul(Real::NAN).is_err());
assert!(Money::checked_new(Currency::eur(), Real::INFINITY).is_err());
assert!(
Money::new(Currency::eur(), Real::NAN)
.checked_add(&amount)
.is_err()
);
assert!(
amount
.checked_ratio(&Money::new(Currency::usd(), 1.0))
.is_err()
);
}
#[test]
fn same_currency_amounts_add() {
let a = Money::new(Currency::eur(), 10.0);
let b = Money::new(Currency::eur(), 2.5);
let sum = a.checked_add(&b).unwrap();
assert_eq!(sum.value(), 12.5);
assert_eq!(sum.currency().code(), "EUR");
}
#[test]
fn mismatched_currencies_error() {
let a = Money::new(Currency::eur(), 1.0);
let b = Money::new(Currency::usd(), 1.0);
assert!(a.checked_add(&b).is_err());
}
}