Skip to main content

core_invoice/
numeric.rs

1use rust_decimal::Decimal;
2use std::fmt;
3use std::str::FromStr;
4
5/// Signed quantity (BT-129, BT-149). May be negative. Not money.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct Quantity(Decimal);
8
9impl Quantity {
10    pub const ZERO: Self = Self(Decimal::ZERO);
11    pub const ONE: Self = Self(Decimal::ONE);
12
13    pub fn new(value: Decimal) -> Self {
14        Self(value)
15    }
16
17    pub fn parse(s: &str) -> Result<Self, rust_decimal::Error> {
18        Ok(Self(Decimal::from_str(s.trim())?))
19    }
20
21    pub fn raw(self) -> Decimal {
22        self.0
23    }
24
25    pub fn is_negative(self) -> bool {
26        self.0.is_sign_negative() && !self.0.is_zero()
27    }
28}
29
30impl fmt::Display for Quantity {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "{}", self.0)
33    }
34}
35
36/// Per cent (`19` means 19%), not a fraction. `19` and `19.00` compare equal.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct Percentage(Decimal);
39
40impl Percentage {
41    pub const ZERO: Self = Self(Decimal::ZERO);
42
43    pub fn new(percent: Decimal) -> Self {
44        Self(percent)
45    }
46
47    pub fn from_fraction(fraction: Decimal) -> Option<Self> {
48        fraction
49            .checked_mul(Decimal::ONE_HUNDRED)
50            .map(|d| Self(d.normalize()))
51    }
52
53    pub fn as_percent(self) -> Decimal {
54        self.0
55    }
56
57    pub fn as_fraction(self) -> Decimal {
58        self.0 / Decimal::ONE_HUNDRED
59    }
60
61    pub fn is_zero(self) -> bool {
62        self.0.is_zero()
63    }
64
65    pub fn is_positive(self) -> bool {
66        self.0 > Decimal::ZERO
67    }
68
69    pub fn is_negative(self) -> bool {
70        self.0 < Decimal::ZERO
71    }
72}
73
74impl fmt::Display for Percentage {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "{}", self.0.normalize())
77    }
78}
79
80impl From<Decimal> for Percentage {
81    fn from(value: Decimal) -> Self {
82        Self::new(value)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn quantity_may_be_negative() {
92        let q = Quantity::new(Decimal::NEGATIVE_ONE);
93        assert!(q.is_negative());
94        assert_eq!(q.to_string(), "-1");
95    }
96
97    #[test]
98    fn ten_percent_is_not_point_one() {
99        let p = Percentage::new(Decimal::from(10));
100        assert_eq!(p.as_percent(), Decimal::from(10));
101        assert_eq!(p.as_fraction(), Decimal::new(10, 2));
102        assert_eq!(
103            Percentage::new(Decimal::from(19)),
104            Percentage::new(Decimal::new(1900, 2))
105        );
106    }
107}