use rust_decimal::Decimal;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Quantity(Decimal);
impl Quantity {
pub const ZERO: Self = Self(Decimal::ZERO);
pub const ONE: Self = Self(Decimal::ONE);
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
}
pub fn is_negative(self) -> bool {
self.0.is_sign_negative() && !self.0.is_zero()
}
}
impl fmt::Display for Quantity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Percentage(Decimal);
impl Percentage {
pub const ZERO: Self = Self(Decimal::ZERO);
pub fn new(percent: Decimal) -> Self {
Self(percent)
}
pub fn from_fraction(fraction: Decimal) -> Option<Self> {
fraction
.checked_mul(Decimal::ONE_HUNDRED)
.map(|d| Self(d.normalize()))
}
pub fn as_percent(self) -> Decimal {
self.0
}
pub fn as_fraction(self) -> Decimal {
self.0 / Decimal::ONE_HUNDRED
}
pub fn is_zero(self) -> bool {
self.0.is_zero()
}
pub fn is_positive(self) -> bool {
self.0 > Decimal::ZERO
}
pub fn is_negative(self) -> bool {
self.0 < Decimal::ZERO
}
}
impl fmt::Display for Percentage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.normalize())
}
}
impl From<Decimal> for Percentage {
fn from(value: Decimal) -> Self {
Self::new(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quantity_may_be_negative() {
let q = Quantity::new(Decimal::NEGATIVE_ONE);
assert!(q.is_negative());
assert_eq!(q.to_string(), "-1");
}
#[test]
fn ten_percent_is_not_point_one() {
let p = Percentage::new(Decimal::from(10));
assert_eq!(p.as_percent(), Decimal::from(10));
assert_eq!(p.as_fraction(), Decimal::new(10, 2));
assert_eq!(
Percentage::new(Decimal::from(19)),
Percentage::new(Decimal::new(1900, 2))
);
}
}