use crate::error::AmountError;
use rust_decimal::Decimal;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InvoiceAmount(Decimal);
pub type Amount = InvoiceAmount;
impl InvoiceAmount {
pub const ZERO: Self = Self(Decimal::ZERO);
pub fn from_minor(cents: i64) -> Self {
Self(Decimal::new(cents, 2))
}
pub fn try_new(value: Decimal) -> Result<Self, AmountError> {
if value.scale() > 2 {
return Err(AmountError::TooManyDecimals);
}
Ok(Self(value))
}
pub fn parse(s: &str) -> Result<Self, AmountError> {
let d = Decimal::from_str(s.trim()).map_err(|_| AmountError::TooManyDecimals)?;
Self::try_new(d)
}
pub fn raw(self) -> Decimal {
self.0
}
pub fn is_zero(self) -> bool {
self.0.is_zero()
}
pub fn checked_add(self, other: Self) -> Option<Self> {
self.0
.checked_add(other.0)
.and_then(|d| Self::try_new(d).ok())
}
pub fn checked_sub(self, other: Self) -> Option<Self> {
self.0
.checked_sub(other.0)
.and_then(|d| Self::try_new(d).ok())
}
pub fn abs(self) -> Self {
Self(self.0.abs())
}
pub fn checked_sum(amounts: impl IntoIterator<Item = Self>) -> Option<Self> {
let mut acc = Self::ZERO;
for a in amounts {
acc = acc.checked_add(a)?;
}
Some(acc)
}
pub fn from_decimal_rounded(value: Decimal) -> Result<Self, AmountError> {
use rust_decimal::RoundingStrategy;
let rounded = value.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero);
Self::try_new(rounded)
}
}
impl fmt::Display for InvoiceAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.round_dp(2))
}
}
impl FromStr for InvoiceAmount {
type Err = AmountError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UnitPriceAmount(Decimal);
impl UnitPriceAmount {
pub const ZERO: Self = Self(Decimal::ZERO);
pub fn new(value: Decimal) -> Self {
Self(value)
}
pub fn parse(s: &str) -> Result<Self, rust_decimal::Error> {
Ok(Self(Decimal::from_str(s.trim())?))
}
pub fn raw(self) -> Decimal {
self.0
}
}
impl fmt::Display for UnitPriceAmount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refuses_third_decimal() {
assert!(InvoiceAmount::parse("0.005").is_err());
assert!(InvoiceAmount::try_new(Decimal::new(5, 3)).is_err());
}
#[test]
fn accepts_two_or_fewer() {
assert!(InvoiceAmount::parse("100.00").is_ok());
assert!(InvoiceAmount::parse("100").is_ok());
assert!(InvoiceAmount::parse("100.1").is_ok());
assert_eq!(
InvoiceAmount::from_minor(10000).raw(),
Decimal::new(10000, 2)
);
}
#[test]
fn unit_price_keeps_four_decimals() {
let p = UnitPriceAmount::parse("10000.1234").unwrap();
assert_eq!(p.to_string(), "10000.1234");
}
#[test]
fn checked_add_no_saturate() {
let a = InvoiceAmount::parse("1.00").unwrap();
let b = InvoiceAmount::parse("2.50").unwrap();
assert_eq!(a.checked_add(b).unwrap().to_string(), "3.50");
}
}