use std::fmt;
use std::fmt::Display;
use std::str::FromStr;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
fn strip_non_numeric(input: &str) -> String {
input
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MoneyAmount(pub Decimal);
impl MoneyAmount {
#[must_use]
pub const fn scale(&self) -> u32 {
self.0.scale()
}
#[must_use]
pub const fn mantissa(&self) -> u128 {
self.0.mantissa().unsigned_abs()
}
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[non_exhaustive]
pub enum MoneyAmountParseError {
#[error("Invalid number format")]
InvalidFormat,
#[error(
"Amount must be between {} and {}",
constants::MIN_STR,
constants::MAX_STR
)]
OutOfRange,
#[error("Negative value is not allowed")]
Negative,
#[error("Too big of a precision: {money} vs {token} on token")]
WrongPrecision {
money: u32,
token: u32,
},
}
mod constants {
use std::sync::LazyLock;
use super::{Decimal, FromStr};
pub const MIN_STR: &str = "0.000000001";
pub const MAX_STR: &str = "999999999";
pub static MIN: LazyLock<Decimal> =
LazyLock::new(|| Decimal::from_str(MIN_STR).expect("valid decimal"));
pub static MAX: LazyLock<Decimal> =
LazyLock::new(|| Decimal::from_str(MAX_STR).expect("valid decimal"));
}
impl MoneyAmount {
pub fn parse(input: &str) -> Result<Self, MoneyAmountParseError> {
let cleaned = strip_non_numeric(input);
let parsed =
Decimal::from_str(&cleaned).map_err(|_| MoneyAmountParseError::InvalidFormat)?;
if parsed.is_sign_negative() {
return Err(MoneyAmountParseError::Negative);
}
if parsed < *constants::MIN || parsed > *constants::MAX {
return Err(MoneyAmountParseError::OutOfRange);
}
Ok(Self(parsed))
}
}
impl FromStr for MoneyAmount {
type Err = MoneyAmountParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl TryFrom<&str> for MoneyAmount {
type Error = MoneyAmountParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
impl From<u128> for MoneyAmount {
fn from(value: u128) -> Self {
Self(Decimal::from(value))
}
}
impl TryFrom<f64> for MoneyAmount {
type Error = MoneyAmountParseError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
let decimal = Decimal::from_f64(value).ok_or(MoneyAmountParseError::OutOfRange)?;
if decimal.is_sign_negative() {
return Err(MoneyAmountParseError::Negative);
}
if decimal < *constants::MIN || decimal > *constants::MAX {
return Err(MoneyAmountParseError::OutOfRange);
}
Ok(Self(decimal))
}
}
impl Display for MoneyAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.normalize())
}
}