baseunits_rs/money/
money.rs

1use std::cmp::Ordering;
2use std::hash::{Hash, Hasher};
3use std::ops::{Add, Div, Mul, Neg, Sub};
4use std::str::FromStr;
5
6use iso_4217::CurrencyCode;
7use rust_decimal::Decimal;
8use rust_fp_categories::Empty;
9use rust_fp_categories::Monoid;
10use rust_fp_categories::Semigroup;
11use rust_decimal::prelude::{FromPrimitive, Zero};
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct Money {
15  pub amount: Decimal,
16  pub currency: CurrencyCode,
17}
18
19#[derive(Debug, PartialEq)]
20pub enum MoneyError {
21  NotSameCurrencyError,
22}
23
24impl Eq for Money {}
25
26impl Hash for Money {
27  fn hash<H>(&self, state: &mut H)
28  where
29    H: Hasher,
30  {
31    self.amount.hash(state);
32    state.write_u32(self.currency.num());
33  }
34}
35
36impl PartialOrd for Money {
37  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
38    if self.currency != other.currency {
39      None
40    } else if self.amount > other.amount {
41      Some(Ordering::Greater)
42    } else if self.amount < other.amount {
43      Some(Ordering::Less)
44    } else {
45      Some(Ordering::Equal)
46    }
47  }
48}
49
50impl Empty for Money {
51  fn empty() -> Self {
52    Money::zero(CurrencyCode::USD)
53  }
54
55  fn is_empty(&self) -> bool {
56    self.is_zero()
57  }
58}
59
60impl Semigroup for Money {
61  fn combine(self, other: Self) -> Self {
62    self + other
63  }
64}
65
66impl Monoid for Money {}
67
68impl Add for Money {
69  type Output = Money;
70
71  fn add(self, rhs: Self) -> Self::Output {
72    Money::add(self, rhs).unwrap_or_else(|err| panic!(format!("{:?}", err)))
73  }
74}
75
76impl Sub for Money {
77  type Output = Money;
78
79  fn sub(self, rhs: Self) -> Self::Output {
80    Money::subtract(self, rhs).unwrap_or_else(|err| panic!(format!("{:?}", err)))
81  }
82}
83
84impl Mul<Decimal> for Money {
85  type Output = Money;
86
87  fn mul(self, rhs: Decimal) -> Self::Output {
88    Money::times(self, rhs)
89  }
90}
91
92impl Div<Decimal> for Money {
93  type Output = Money;
94
95  fn div(self, rhs: Decimal) -> Self::Output {
96    Money::divided_by(self, rhs)
97  }
98}
99
100impl Neg for Money {
101  type Output = Money;
102
103  fn neg(self) -> Self::Output {
104    Money::negated(self)
105  }
106}
107
108impl From<(Decimal, CurrencyCode)> for Money {
109  fn from((amount, currency): (Decimal, CurrencyCode)) -> Self {
110    Money::new(amount, currency)
111  }
112}
113
114impl From<(&str, CurrencyCode)> for Money {
115  fn from((amount, currency): (&str, CurrencyCode)) -> Self {
116    let a = Decimal::from_str(amount).unwrap();
117    Money::new(a, currency)
118  }
119}
120
121macro_rules! from_numeric_impl {
122  ($($t:ty)*) => ($(
123    impl From<($t, CurrencyCode)> for Money {
124      fn from((amount, currency): ($t, CurrencyCode)) -> Self {
125        let mut a = Decimal::from(amount);
126        a.rescale(currency.digit().unwrap() as u32);
127        Money::new(a, currency)
128      }
129    }
130  )*)
131}
132
133from_numeric_impl! {i8 i16 i32 i64 u8 u16 u32 u64}
134
135impl Money {
136  pub fn new(amount: Decimal, currency: CurrencyCode) -> Self {
137    let mut a = amount;
138
139    a.rescale(currency.digit().unwrap() as u32);
140    Self {
141      amount: a,
142      currency,
143    }
144  }
145
146  pub fn dollars(amount: Decimal) -> Self {
147    Self::new(amount, CurrencyCode::USD)
148  }
149
150  pub fn dollars_i32(amount: i32) -> Self {
151    Self::dollars(Decimal::from_i32(amount).unwrap())
152  }
153
154  pub fn dollars_f32(amount: f32) -> Self {
155    Self::dollars(Decimal::from_f32(amount).unwrap())
156  }
157
158  pub fn zero(currency: CurrencyCode) -> Self {
159    Self::new(Decimal::zero(), currency)
160  }
161
162  pub fn abs(&self) -> Self {
163    Self {
164      amount: self.amount.abs(),
165      currency: self.currency,
166    }
167  }
168
169  pub fn is_positive(&self) -> bool {
170    self.amount > Decimal::zero()
171  }
172
173  pub fn is_negative(&self) -> bool {
174    self.amount < Decimal::zero()
175  }
176
177  pub fn is_zero(&self) -> bool {
178    self.amount.is_zero()
179  }
180
181  pub fn negated(self) -> Self {
182    Self {
183      amount: -self.amount,
184      currency: self.currency,
185    }
186  }
187
188  //noinspection RsExternalLinter
189  pub fn add(self, other: Self) -> Result<Self, MoneyError> {
190    if self.currency != other.currency {
191      Err(MoneyError::NotSameCurrencyError)
192    } else {
193      Ok(Self {
194        amount: self.amount + other.amount,
195        currency: self.currency,
196      })
197    }
198  }
199
200  pub fn subtract(self, other: Self) -> Result<Self, MoneyError> {
201    self.add(other.negated())
202  }
203
204  pub fn times(self, factor: Decimal) -> Self {
205    Self {
206      amount: self.amount * factor,
207      currency: self.currency,
208    }
209  }
210
211  pub fn divided_by(self, divisor: Decimal) -> Self {
212    Self {
213      amount: self.amount / divisor,
214      currency: self.currency,
215    }
216  }
217}
218
219#[cfg(test)]
220mod tests {
221  use iso_4217::CurrencyCode;
222  use rust_decimal::Decimal;
223  use crate::money::{Money};
224  use rust_decimal::prelude::{Zero, FromPrimitive};
225
226  #[test]
227  fn test_eq() {
228    let m1 = Money::from((1u32, CurrencyCode::USD));
229    let m2 = Money::from((1u32, CurrencyCode::USD));
230    assert_eq!(m1, m2);
231  }
232
233  #[test]
234  fn test_ne() {
235    let m1 = Money::from((1u32, CurrencyCode::USD));
236    let m2 = Money::from((2u32, CurrencyCode::USD));
237    assert_ne!(m1, m2);
238  }
239
240  #[test]
241  fn test_zero() {
242    let m1 = Money::zero(CurrencyCode::USD);
243    let m2 = Money::new(Decimal::zero(), CurrencyCode::USD);
244    assert_eq!(m1.abs(), m2);
245  }
246
247  #[test]
248  fn test_abs() {
249    let m1 = Money::new(Decimal::from_i32(-1).unwrap(), CurrencyCode::USD);
250    let m2 = Money::new(Decimal::from_i32(1).unwrap(), CurrencyCode::USD);
251    assert_eq!(m1.abs(), m2);
252  }
253
254  #[test]
255  fn test_add() {
256    let m1 = Money::from((1u32, CurrencyCode::USD));
257    let m2 = Money::from((2u32, CurrencyCode::USD));
258    let m3 = m1.clone();
259    let m4 = m2.clone();
260
261    let m5 = m1.add(m2).unwrap();
262    let m6 = m3 + m4;
263
264    assert_eq!(
265      m5,
266      Money::new(Decimal::from_i32(3).unwrap(), CurrencyCode::USD)
267    );
268    assert_eq!(
269      m6,
270      Money::new(Decimal::from_i32(3).unwrap(), CurrencyCode::USD)
271    );
272  }
273}