use crate::Currency;
use crate::Decimal;
use crate::MoneyError;
use crate::fmt::format_with_separator;
use crate::fmt::{CODE_FORMAT, CODE_FORMAT_MINOR, SYMBOL_FORMAT, SYMBOL_FORMAT_MINOR, format};
use crate::macros::dec;
use crate::split_alloc_ops::Split;
use rust_decimal::RoundingStrategy as DecimalRoundingStrategy;
use rust_decimal::{MathematicalOps, prelude::FromPrimitive, prelude::ToPrimitive};
use std::fmt::Debug;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
#[cfg(feature = "locale")]
use crate::fmt::format_with_amount;
pub trait BaseMoney<C: Currency>: Sized + Clone {
fn new(amount: impl DecimalNumber) -> Result<Self, MoneyError>;
fn amount(&self) -> Decimal;
fn round(self) -> Self;
fn round_with(self, decimal_points: u32, strategy: RoundingStrategy) -> Self;
fn truncate(&self) -> Self;
fn truncate_with(&self, scale: u32) -> Self;
#[inline]
fn name(&self) -> &str {
C::NAME
}
#[inline]
fn symbol(&self) -> &str {
C::SYMBOL
}
#[inline]
fn code(&self) -> &str {
C::CODE
}
#[inline]
fn numeric_code(&self) -> i32 {
C::NUMERIC.into()
}
#[inline]
fn minor_unit(&self) -> u16 {
C::MINOR_UNIT
}
#[inline]
fn minor_amount(&self) -> Result<i128, MoneyError> {
self.amount()
.checked_mul(
dec!(10)
.checked_powu(self.minor_unit().into())
.ok_or(MoneyError::OverflowError)?,
)
.ok_or(MoneyError::OverflowError)?
.to_i128()
.ok_or(MoneyError::OverflowError)
}
#[inline]
fn thousand_separator(&self) -> &str {
C::THOUSAND_SEPARATOR
}
#[inline]
fn decimal_separator(&self) -> &str {
C::DECIMAL_SEPARATOR
}
#[inline]
fn is_zero(&self) -> bool {
self.amount().is_zero()
}
#[inline]
fn is_positive(&self) -> bool {
self.amount().is_sign_positive()
}
#[inline]
fn is_negative(&self) -> bool {
self.amount().is_sign_negative()
}
#[inline]
fn mantissa(&self) -> i128 {
self.amount().mantissa()
}
#[inline]
fn fraction(&self) -> Decimal {
self.amount().fract()
}
#[inline]
fn scale(&self) -> u32 {
self.amount().scale()
}
fn format_code(&self) -> String {
format(self.to_owned(), CODE_FORMAT)
}
fn format_symbol(&self) -> String {
format(self.to_owned(), SYMBOL_FORMAT)
}
fn format_code_minor(&self) -> String {
format(self.to_owned(), CODE_FORMAT_MINOR)
}
fn format_symbol_minor(&self) -> String {
format(self.to_owned(), SYMBOL_FORMAT_MINOR)
}
fn display(&self) -> String {
self.format_code()
}
}
pub trait BaseOps<C: Currency>:
Sized
+ BaseMoney<C>
+ Add<Output = Self>
+ Sub<Output = Self>
+ Mul<Output = Self>
+ Div<Output = Self>
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ Neg<Output = Self>
{
fn is_approx<M, T>(&self, m: M, tolerance: T) -> bool
where
M: BaseMoney<C> + BaseOps<C> + Amount<C>,
T: DecimalNumber,
{
self.checked_sub(m).is_some_and(|diff| {
tolerance
.get_decimal()
.is_some_and(|tol| tol >= diff.abs().amount())
})
}
fn abs(&self) -> Self;
fn checked_add<RHS>(&self, rhs: RHS) -> Option<Self>
where
RHS: Amount<C>;
fn checked_sub<RHS>(&self, rhs: RHS) -> Option<Self>
where
RHS: Amount<C>;
fn checked_mul<RHS>(&self, rhs: RHS) -> Option<Self>
where
RHS: DecimalNumber;
fn checked_div<RHS>(&self, rhs: RHS) -> Option<Self>
where
RHS: DecimalNumber;
fn checked_rem<RHS>(&self, rhs: RHS) -> Option<Self>
where
RHS: DecimalNumber,
{
Self::new(self.amount().checked_rem(rhs.get_decimal()?)?).ok()
}
fn split<P, R>(&self, p: P) -> Option<R>
where
R: Split<Self, C, P>,
{
R::split(self.clone(), p)
}
}
pub trait IterOps<C: Currency> {
type Item;
fn checked_sum(&self) -> Option<Self::Item>;
fn mean(&self) -> Option<Self::Item>;
fn median(&self) -> Option<Self::Item>;
fn mode(&self) -> Option<Vec<Self::Item>>;
}
pub trait Amount<C: Currency>: Sized {
fn get_decimal(&self) -> Option<Decimal>;
}
impl<C: Currency> Amount<C> for Decimal {
fn get_decimal(&self) -> Option<Decimal> {
Some(*self)
}
}
impl<C: Currency> Amount<C> for f64 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_f64(*self)
}
}
impl<C: Currency> Amount<C> for i32 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_i32(*self)
}
}
impl<C: Currency> Amount<C> for i64 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_i64(*self)
}
}
impl<C: Currency> Amount<C> for i128 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_i128(*self)
}
}
pub trait DecimalNumber: Sized {
fn get_decimal(&self) -> Option<Decimal>;
}
impl DecimalNumber for Decimal {
fn get_decimal(&self) -> Option<Decimal> {
Some(*self)
}
}
impl DecimalNumber for f64 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_f64(*self)
}
}
impl DecimalNumber for i32 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_i32(*self)
}
}
impl DecimalNumber for i64 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_i64(*self)
}
}
impl DecimalNumber for i128 {
fn get_decimal(&self) -> Option<Decimal> {
Decimal::from_i128(*self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RoundingStrategy {
#[default]
BankersRounding,
HalfUp,
HalfDown,
Ceil,
Floor,
}
impl From<RoundingStrategy> for DecimalRoundingStrategy {
fn from(value: RoundingStrategy) -> Self {
match value {
RoundingStrategy::BankersRounding => DecimalRoundingStrategy::MidpointNearestEven,
RoundingStrategy::HalfUp => DecimalRoundingStrategy::MidpointAwayFromZero,
RoundingStrategy::HalfDown => DecimalRoundingStrategy::MidpointTowardZero,
RoundingStrategy::Ceil => DecimalRoundingStrategy::AwayFromZero,
RoundingStrategy::Floor => DecimalRoundingStrategy::ToZero,
}
}
}
pub trait MoneyFormatter<C: Currency>: Sized + BaseMoney<C> {
fn format(&self, format_str: &str) -> String {
format(self.to_owned(), format_str)
}
fn format_with_separator(
&self,
format_str: &str,
thousand_separator: &str,
decimal_separator: &str,
) -> String {
format_with_separator(
self.to_owned(),
format_str,
thousand_separator,
decimal_separator,
)
}
#[cfg(feature = "locale")]
fn format_locale_amount(
&self,
locale_str: &str,
format_str: &str,
) -> Result<String, MoneyError> {
use icu_decimal::{DecimalFormatter, input::Decimal as LocaleDecimal};
use icu_locale::Locale;
let loc: Locale = locale_str.parse().map_err(|_| {
MoneyError::ParseLocale(
format!(
"failed parsing locale {} , invalid or not found",
locale_str
)
.into(),
)
})?;
let formatter = DecimalFormatter::try_new(loc.into(), Default::default())
.map_err(|_| MoneyError::ParseLocale("failed initiating decimal formatter".into()))?;
let is_negative = self.is_negative();
let curr_minor_unit = C::MINOR_UNIT.into();
let abs_amount = if self.scale() < curr_minor_unit {
let remaining_scale: usize = (curr_minor_unit - self.scale())
.try_into()
.map_err(|_| MoneyError::ParseLocale("invalid minor unit".into()))?;
let minor_amount = "0".repeat(remaining_scale);
let fract = if self.scale() == 0 {
format!(".{}", minor_amount)
} else {
minor_amount
};
let mut ret = self.amount().abs().to_string();
ret.push_str(&fract);
ret
} else {
self.amount().abs().to_string()
};
let decimal =
LocaleDecimal::try_from_str(&abs_amount).map_err(|_| MoneyError::OverflowError)?;
let formatted_decimal = formatter.format(&decimal).to_string();
let ret = format_with_amount::<C>(&formatted_decimal, is_negative, format_str);
Ok(ret)
}
}