baseunits_rs/money/
money.rs1use 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 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}