use regex::Regex;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use std::fmt;
use std::fmt::Display;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)] pub struct MoneyAmount(pub Decimal);
#[allow(dead_code)] impl MoneyAmount {
pub fn scale(&self) -> u32 {
self.0.scale()
}
pub fn mantissa(&self) -> u128 {
self.0.mantissa().unsigned_abs()
}
}
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)] 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 super::*;
use std::sync::LazyLock;
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"));
}
#[allow(dead_code)] impl MoneyAmount {
pub fn parse(input: &str) -> Result<Self, MoneyAmountParseError> {
let cleaned = Regex::new(r"[^\d\.\-]+")
.unwrap()
.replace_all(input, "")
.to_string();
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(MoneyAmount(parsed))
}
}
impl FromStr for MoneyAmount {
type Err = MoneyAmountParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
MoneyAmount::parse(s)
}
}
impl TryFrom<&str> for MoneyAmount {
type Error = MoneyAmountParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
MoneyAmount::from_str(value)
}
}
impl From<u128> for MoneyAmount {
fn from(value: u128) -> Self {
MoneyAmount(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(MoneyAmount(decimal))
}
}
impl Display for MoneyAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.normalize())
}
}