use crate::{FinMoneyCurrency, FinMoneyError, FinMoneyRoundingStrategy};
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use std::cmp::Ordering;
use std::fmt;
use std::ops::{Add, Mul, Neg, Sub};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FinMoney {
amount: Decimal,
currency: FinMoneyCurrency,
}
impl Default for FinMoney {
fn default() -> Self {
Self {
amount: Decimal::ZERO,
currency: FinMoneyCurrency::default(),
}
}
}
impl FinMoney {
#[inline]
fn assert_same_currency(&self, other: Self) -> Result<(), FinMoneyError> {
if !self.currency.is_same_currency(&other.currency) {
return Err(FinMoneyError::CurrencyMismatch {
expected: self.currency.get_code().to_string(),
actual: other.currency.get_code().to_string(),
});
}
Ok(())
}
#[inline]
fn round_result(&self, value: Decimal, strategy: FinMoneyRoundingStrategy) -> Decimal {
value.round_dp_with_strategy(
self.currency.get_precision().into(),
strategy.to_decimal_strategy(),
)
}
#[inline]
pub fn new(amount: Decimal, currency: FinMoneyCurrency) -> Self {
Self { amount, currency }
}
pub fn new_with_precision(
amount: Decimal,
currency: FinMoneyCurrency,
strategy: FinMoneyRoundingStrategy,
) -> Self {
let s = strategy.to_decimal_strategy();
let rounded_amount = amount.round_dp_with_strategy(currency.get_precision().into(), s);
Self {
amount: rounded_amount,
currency,
}
}
#[inline]
pub fn zero(currency: FinMoneyCurrency) -> Self {
Self {
amount: Decimal::ZERO,
currency,
}
}
#[inline]
pub fn get_amount(&self) -> Decimal {
self.amount
}
#[inline]
pub fn get_currency(&self) -> FinMoneyCurrency {
self.currency
}
#[inline]
pub fn get_currency_id(&self) -> i32 {
self.currency.get_id()
}
#[inline]
pub fn get_precision(&self) -> u8 {
self.currency.get_precision()
}
#[inline]
pub fn get_currency_code(&self) -> &str {
self.currency.get_code()
}
pub fn plus_money(&self, other: FinMoney) -> Result<FinMoney, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(FinMoney::new(self.amount + other.amount, self.currency))
}
#[inline]
pub fn plus_decimal(&self, d: Decimal) -> FinMoney {
FinMoney::new(self.amount + d, self.currency)
}
pub fn minus_money(&self, other: FinMoney) -> Result<FinMoney, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(FinMoney::new(self.amount - other.amount, self.currency))
}
#[inline]
pub fn minus_decimal(&self, d: Decimal) -> FinMoney {
FinMoney::new(self.amount - d, self.currency)
}
pub fn multiplied_by_money(&self, other: FinMoney) -> Result<FinMoney, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(FinMoney::new(self.amount * other.amount, self.currency))
}
#[inline]
pub fn multiplied_by_decimal(&self, d: Decimal) -> FinMoney {
FinMoney::new(self.amount * d, self.currency)
}
pub fn divided_by_money(
&self,
other: FinMoney,
round_strategy: FinMoneyRoundingStrategy,
) -> Result<FinMoney, FinMoneyError> {
self.assert_same_currency(other)?;
if other.amount.is_zero() {
return Err(FinMoneyError::DivisionByZero);
}
let raw = self.amount / other.amount;
let rounded = self.round_result(raw, round_strategy);
Ok(FinMoney::new(rounded, self.currency))
}
pub fn divided_by_decimal(
&self,
d: Decimal,
round_strategy: FinMoneyRoundingStrategy,
) -> Result<FinMoney, FinMoneyError> {
if d.is_zero() {
return Err(FinMoneyError::DivisionByZero);
}
let raw = self.amount / d;
let rounded = self.round_result(raw, round_strategy);
Ok(FinMoney::new(rounded, self.currency))
}
pub fn compare(&self, other: FinMoney) -> Result<Ordering, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(self.amount.cmp(&other.amount))
}
pub fn min(&self, other: FinMoney) -> Result<FinMoney, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(if self.amount <= other.amount {
*self
} else {
other
})
}
pub fn max(&self, other: FinMoney) -> Result<FinMoney, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(if self.amount >= other.amount {
*self
} else {
other
})
}
pub fn is_same_currency(&self, other: FinMoney) -> bool {
self.currency.is_same_currency(&other.currency)
}
pub fn is_equal_to(&self, other: FinMoney) -> bool {
self.currency.is_same_currency(&other.currency) && self.amount == other.amount
}
pub fn is_less_than(&self, other: FinMoney) -> Result<bool, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(self.amount < other.amount)
}
pub fn is_less_than_or_equal(&self, other: FinMoney) -> Result<bool, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(self.amount <= other.amount)
}
pub fn is_less_than_decimal(&self, decimal: Decimal) -> bool {
self.amount < decimal
}
pub fn is_less_than_or_equal_decimal(&self, decimal: Decimal) -> bool {
self.amount <= decimal
}
pub fn is_greater_than(&self, other: FinMoney) -> Result<bool, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(self.amount > other.amount)
}
pub fn is_greater_than_or_equal(&self, other: FinMoney) -> Result<bool, FinMoneyError> {
self.assert_same_currency(other)?;
Ok(self.amount >= other.amount)
}
pub fn is_greater_than_decimal(&self, decimal: Decimal) -> bool {
self.amount > decimal
}
pub fn is_greater_than_or_equal_decimal(&self, decimal: Decimal) -> bool {
self.amount >= decimal
}
pub fn rescale(&self, new_precision: u8) -> Result<FinMoney, FinMoneyError> {
let new_currency = self.currency.with_precision(new_precision)?;
let scaled = self.amount.round_dp(new_precision.into());
Ok(FinMoney::new(scaled, new_currency))
}
pub fn rounded(&self, strategy: FinMoneyRoundingStrategy) -> FinMoney {
let amount = self.round_result(self.amount, strategy);
FinMoney::new(amount, self.currency)
}
#[inline]
pub fn floor(&self) -> FinMoney {
FinMoney::new(self.amount.floor(), self.currency)
}
#[inline]
pub fn ceil(&self) -> FinMoney {
FinMoney::new(self.amount.ceil(), self.currency)
}
#[inline]
pub fn trunc(&self) -> FinMoney {
FinMoney::new(self.amount.trunc(), self.currency)
}
#[inline]
pub fn is_integer(&self) -> bool {
self.amount.fract().is_zero()
}
#[inline]
pub fn has_fraction(&self) -> bool {
!self.amount.fract().is_zero()
}
#[inline]
pub fn is_zero(&self) -> bool {
self.amount.is_zero()
}
#[inline]
pub fn is_positive(&self) -> bool {
self.amount.is_sign_positive() && !self.amount.is_zero()
}
#[inline]
pub fn is_negative(&self) -> bool {
self.amount.is_sign_negative() && !self.amount.is_zero()
}
#[inline]
pub fn is_positive_or_zero(&self) -> bool {
self.amount.is_sign_positive()
}
#[inline]
pub fn is_negative_or_zero(&self) -> bool {
self.amount.is_sign_negative() || self.amount.is_zero()
}
#[inline]
pub fn sqrt(&self) -> Result<FinMoney, FinMoneyError> {
match self.amount.sqrt() {
Some(result) => Ok(FinMoney::new(result, self.currency)),
None => Err(FinMoneyError::InvalidAmount(
"cannot take square root of negative number".to_string(),
)),
}
}
#[inline]
pub fn abs(&self) -> FinMoney {
FinMoney::new(self.amount.abs(), self.currency)
}
#[inline]
pub fn negated(&self) -> FinMoney {
FinMoney::new(-self.amount, self.currency)
}
#[inline]
pub fn normalize(&self) -> FinMoney {
FinMoney::new(self.amount.normalize(), self.currency)
}
pub fn percent_change_from(&self, initial: FinMoney) -> Result<Decimal, FinMoneyError> {
self.assert_same_currency(initial)?;
if initial.amount.is_zero() {
return Err(FinMoneyError::DivisionByZero);
}
Ok(((self.amount - initial.amount) * dec!(100)) / initial.amount)
}
pub fn negative_percent_change_from(
&self,
initial: FinMoney,
) -> Result<Decimal, FinMoneyError> {
self.assert_same_currency(initial)?;
if initial.amount.is_zero() {
return Err(FinMoneyError::DivisionByZero);
}
Ok(((initial.amount - self.amount) * dec!(100)) / initial.amount)
}
pub fn percent_change(
initial: FinMoney,
new_value: FinMoney,
) -> Result<Decimal, FinMoneyError> {
new_value.percent_change_from(initial)
}
pub fn negative_percent_change(
initial: FinMoney,
new_value: FinMoney,
) -> Result<Decimal, FinMoneyError> {
new_value.negative_percent_change_from(initial)
}
pub fn round_dp_with_strategy(&self, dp: u32, strategy: FinMoneyRoundingStrategy) -> FinMoney {
let s = strategy.to_decimal_strategy();
let rounded = self.amount.round_dp_with_strategy(dp, s);
FinMoney::new(rounded, self.currency)
}
pub fn round_dp(&self, dp: u32) -> FinMoney {
let rounded = self.amount.round_dp(dp);
FinMoney::new(rounded, self.currency)
}
}
impl FinMoney {
pub fn to_tick(
&self,
tick: Decimal,
strategy: FinMoneyRoundingStrategy,
) -> Result<FinMoney, FinMoneyError> {
if tick <= Decimal::ZERO {
return Err(FinMoneyError::InvalidTick);
}
let s = strategy.to_decimal_strategy();
if let Some(dp) = Self::tick_power10_dp(tick) {
let amt = self.amount.round_dp_with_strategy(dp, s);
return Ok(FinMoney::new(amt, self.currency));
}
let k = self.amount / tick;
let k_rounded = k.round_dp_with_strategy(0, s);
let amt = k_rounded * tick;
Ok(FinMoney::new(amt, self.currency))
}
pub fn to_tick_down(&self, tick: Decimal) -> Result<FinMoney, FinMoneyError> {
self.to_tick(tick, FinMoneyRoundingStrategy::ToNegativeInfinity)
}
pub fn to_tick_up(&self, tick: Decimal) -> Result<FinMoney, FinMoneyError> {
self.to_tick(tick, FinMoneyRoundingStrategy::ToPositiveInfinity)
}
pub fn to_tick_nearest(&self, tick: Decimal) -> Result<FinMoney, FinMoneyError> {
self.to_tick(tick, FinMoneyRoundingStrategy::MidpointNearestEven)
}
pub fn is_multiple_of_tick(&self, tick: Decimal) -> bool {
if tick.is_zero() {
return false;
}
if let Some(dp) = Self::tick_power10_dp(tick) {
let amt = self.amount.round_dp(dp);
return amt == self.amount;
}
let k = self.amount / tick;
k.fract().is_zero()
}
#[inline]
fn tick_power10_dp(tick: Decimal) -> Option<u32> {
let dp = tick.scale();
if tick == Decimal::new(1, dp) {
Some(dp)
} else {
None
}
}
}
impl Add for FinMoney {
type Output = Result<FinMoney, FinMoneyError>;
fn add(self, rhs: Self) -> Self::Output {
self.plus_money(rhs)
}
}
impl Sub for FinMoney {
type Output = Result<FinMoney, FinMoneyError>;
fn sub(self, rhs: Self) -> Self::Output {
self.minus_money(rhs)
}
}
impl Mul<Decimal> for FinMoney {
type Output = FinMoney;
fn mul(self, rhs: Decimal) -> Self::Output {
self.multiplied_by_decimal(rhs)
}
}
impl Mul<FinMoney> for Decimal {
type Output = FinMoney;
fn mul(self, rhs: FinMoney) -> Self::Output {
rhs.multiplied_by_decimal(self)
}
}
impl Neg for FinMoney {
type Output = FinMoney;
fn neg(self) -> Self::Output {
self.negated()
}
}
impl fmt::Display for FinMoney {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.amount, self.currency.get_code())
}
}