dinero/models/
money.rs

1use std::fmt;
2use std::fmt::{Display, Formatter};
3use std::ops::{Add, Div, Mul, Neg, Sub};
4use std::rc::Rc;
5
6use num::rational::BigRational;
7use num::{self, ToPrimitive};
8use num::{BigInt, Signed, Zero};
9
10use crate::models::balance::Balance;
11use crate::models::currency::{CurrencySymbolPlacement, DigitGrouping, NegativeAmountDisplay};
12use crate::models::{Currency, HasName};
13use std::borrow::Borrow;
14use std::cmp::Ordering;
15
16/// Money representation: an amount and a currency
17///
18/// It is important that calculations are not done with floats but with Rational numbers so that
19/// everything adds up correctly
20///
21/// Money can be added, in which case it returns a balance, as it can have several currencies
22/// # Examples
23/// ```rust
24/// # use dinero::models::{Money, Balance, Currency, DigitGrouping};
25/// # use num::rational::BigRational;
26/// # use std::rc::Rc;
27/// use num::BigInt;
28/// #
29/// let usd = Rc::new(Currency::from("usd"));
30/// let mut eur = Rc::new(Currency::from("eur"));
31///
32/// let zero = Money::new();
33/// let m1 = Money::from((eur.clone(), BigRational::from(BigInt::from(100))));
34/// let m2 = Money::from((eur.clone(), BigRational::from(BigInt::from(200))));
35/// # let m3 = Money::from((eur.clone(), BigRational::from(BigInt::from(300))));
36/// let b1 = m1.clone() + m2.clone(); // 300 euros
37/// # assert_eq!(*b1.balance.get(&Some(eur.clone())).unwrap(), m3);
38///
39/// // Multi-currency works as well
40/// let d1 = Money::from((usd.clone(), BigRational::from(BigInt::from(50))));
41/// let b2 = d1.clone() + m1.clone(); // 100 euros and 50 usd
42/// assert_eq!(b2.balance.len(), 2);
43/// assert_eq!(*b2.balance.get(&Some(eur.clone())).unwrap(), m1);
44/// assert_eq!(*b2.balance.get(&Some(usd.clone())).unwrap(), d1);
45///
46/// let b3 = b1 - Balance::from(m2.clone()) + Balance::from(Money::new());
47/// assert_eq!(b3.to_money().unwrap(), m1);
48/// ```
49#[derive(Clone, Debug)]
50pub enum Money {
51    Zero,
52    Money {
53        amount: num::rational::BigRational,
54        currency: Rc<Currency>,
55    },
56}
57impl Default for Money {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62impl Money {
63    pub fn new() -> Self {
64        Money::Zero
65    }
66    pub fn is_zero(&self) -> bool {
67        match self {
68            Money::Zero => true,
69            Money::Money { amount, .. } => amount.is_zero(),
70        }
71    }
72    pub fn is_positive(&self) -> bool {
73        match self {
74            Money::Zero => true,
75            Money::Money { amount, .. } => amount.is_positive(),
76        }
77    }
78    pub fn is_negative(&self) -> bool {
79        match self {
80            Money::Zero => true,
81            Money::Money { amount, .. } => amount.is_negative(),
82        }
83    }
84    pub fn get_commodity(&self) -> Option<Rc<Currency>> {
85        match self {
86            Money::Zero => None,
87            Money::Money { currency, .. } => Some(currency.clone()),
88        }
89    }
90    pub fn get_amount(&self) -> BigRational {
91        match self {
92            Money::Zero => BigRational::new(BigInt::from(0), BigInt::from(1)),
93            Money::Money { amount, .. } => amount.clone(),
94        }
95    }
96    pub fn abs(&self) -> Money {
97        match self.is_negative() {
98            true => -self.clone(),
99            false => self.clone(),
100        }
101    }
102}
103impl Eq for Money {}
104
105impl PartialEq for Money {
106    fn eq(&self, other: &Self) -> bool {
107        match self {
108            Money::Zero => match other {
109                Money::Zero => true,
110                Money::Money { amount, .. } => amount.is_zero(),
111            },
112            Money::Money {
113                amount: a1,
114                currency: c1,
115            } => match other {
116                Money::Zero => a1.is_zero(),
117                Money::Money {
118                    amount: a2,
119                    currency: c2,
120                } => (a1 == a2) & (c1 == c2),
121            },
122        }
123    }
124}
125
126impl Ord for Money {
127    fn cmp(&self, other: &Self) -> Ordering {
128        match self.partial_cmp(other) {
129            Some(c) => c,
130            None => {
131                let self_commodity = self.get_commodity().unwrap();
132                let other_commodity = other.get_commodity().unwrap();
133                panic!(
134                    "Can't compare different currencies. {} and {}.",
135                    self_commodity, other_commodity
136                );
137            }
138        }
139    }
140}
141impl PartialOrd for Money {
142    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
143        let self_amount = self.get_amount();
144        let other_amount = other.get_amount();
145        match self.get_commodity() {
146            None => self_amount.partial_cmp(other_amount.borrow()),
147            Some(self_currency) => match other.get_commodity() {
148                None => self_amount.partial_cmp(other_amount.borrow()),
149                Some(other_currency) => {
150                    if self_currency == other_currency {
151                        self_amount.partial_cmp(other_amount.borrow())
152                    } else {
153                        None
154                    }
155                }
156            },
157        }
158    }
159}
160
161impl Mul<BigRational> for Money {
162    type Output = Money;
163
164    fn mul(self, rhs: BigRational) -> Self::Output {
165        match self {
166            Money::Zero => Money::new(),
167            Money::Money { amount, currency } => Money::from((currency, amount * rhs)),
168        }
169    }
170}
171
172impl Div<BigRational> for Money {
173    type Output = Money;
174
175    fn div(self, rhs: BigRational) -> Self::Output {
176        match self {
177            Money::Zero => Money::new(),
178            Money::Money { amount, currency } => Money::from((currency, amount / rhs)),
179        }
180    }
181}
182
183impl From<(Rc<Currency>, BigRational)> for Money {
184    fn from(cur_amount: (Rc<Currency>, BigRational)) -> Self {
185        let (currency, amount) = cur_amount;
186        Money::Money { amount, currency }
187    }
188}
189
190impl From<(Currency, BigRational)> for Money {
191    fn from(cur_amount: (Currency, BigRational)) -> Self {
192        let (currency, amount) = cur_amount;
193        Money::Money {
194            amount,
195            currency: Rc::new(currency),
196        }
197    }
198}
199
200impl Add for Money {
201    type Output = Balance;
202
203    fn add(self, rhs: Self) -> Self::Output {
204        let b1 = Balance::from(self);
205        let b2 = Balance::from(rhs);
206        b1 + b2
207    }
208}
209impl Sub for Money {
210    type Output = Balance;
211
212    fn sub(self, rhs: Self) -> Self::Output {
213        let b1 = Balance::from(self);
214        let b2 = Balance::from(rhs);
215        b1 - b2
216    }
217}
218
219impl<'a> Neg for Money {
220    type Output = Money;
221
222    fn neg(self) -> Self::Output {
223        match self {
224            Money::Zero => Money::Zero,
225            Money::Money { currency, amount } => Money::Money {
226                currency,
227                amount: -amount,
228            },
229        }
230    }
231}
232
233impl Display for Money {
234    // [This is what Microsoft says about currency formatting](https://docs.microsoft.com/en-us/globalization/locale/currency-formatting)
235
236    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
237        match self {
238            Money::Zero => write!(f, "0"),
239            Money::Money { amount, currency } => {
240                // Suppose: -1.234.567,000358 EUR
241                // Get the format
242                let format = currency.display_format.borrow();
243                // Read number of decimals from format
244                let decimals = match format.max_decimals {
245                    Some(d) => d,
246                    None => format.precision,
247                };
248
249                let full_str = format!("{:.*}", decimals, amount.to_f64().unwrap());
250
251                // num = trunc + fract
252                let integer_part: String = full_str
253                    .split('.')
254                    .next()
255                    .unwrap()
256                    .split('-')
257                    .last()
258                    .unwrap()
259                    .into(); // -1.234.567
260
261                let decimal_part = amount.fract().to_f64().unwrap();
262
263                // decimal_str holds the decimal part without the dot (decimal separator)
264                let mut decimal_str = if decimals == 0 {
265                    String::new()
266                } else {
267                    format!("{:.*}", decimals, &decimal_part)
268                        .split('.')
269                        .last()
270                        .unwrap()
271                        .into()
272                };
273                // now add the dot
274                if decimals > 0 {
275                    decimal_str = format!("{}{}", format.get_decimal_separator_str(), decimal_str);
276                }
277
278                let integer_str = {
279                    match format.get_digit_grouping() {
280                        DigitGrouping::None => integer_part, // Do nothing
281                        grouping => {
282                            let mut group_size = 3;
283                            let mut counter = 0;
284                            let mut reversed = vec![];
285                            match format.get_thousands_separator_str() {
286                                Some(character) => {
287                                    let thousands_separator = character;
288                                    for c in integer_part.chars().rev() {
289                                        if c == '-' {
290                                            continue;
291                                        }
292
293                                        if counter == group_size {
294                                            reversed.push(thousands_separator);
295                                            if grouping == DigitGrouping::Indian {
296                                                group_size = 2;
297                                            }
298                                            counter = 0;
299                                        }
300                                        reversed.push(c);
301                                        counter += 1;
302                                    }
303                                    reversed.iter().rev().collect()
304                                }
305                                None => integer_part,
306                            }
307                        }
308                    }
309                };
310
311                let amount_str = format!("{}{}", integer_str, decimal_str);
312                match format.symbol_placement {
313                    CurrencySymbolPlacement::BeforeAmount => {
314                        if amount.is_negative() {
315                            match format.negative_amount_display {
316                                NegativeAmountDisplay::BeforeSymbolAndNumber => {
317                                    write!(f, "-{}{}", currency.get_name(), amount_str)
318                                }
319                                NegativeAmountDisplay::BeforeNumberBehindCurrency => {
320                                    write!(f, "{}-{}", currency.get_name(), amount_str)
321                                }
322                                NegativeAmountDisplay::AfterNumber => {
323                                    write!(f, "{}{}-", currency.get_name(), amount_str)
324                                }
325                                NegativeAmountDisplay::Parentheses => {
326                                    write!(f, "({}{})", currency.get_name(), amount_str)
327                                }
328                            }
329                        } else {
330                            write!(f, "{}{}", currency.get_name(), amount_str)
331                        }
332                    }
333                    CurrencySymbolPlacement::AfterAmount => {
334                        if amount.is_negative() {
335                            match format.negative_amount_display {
336                                NegativeAmountDisplay::BeforeSymbolAndNumber
337                                | NegativeAmountDisplay::BeforeNumberBehindCurrency => {
338                                    write!(f, "-{} {}", amount_str, currency.get_name())
339                                }
340                                NegativeAmountDisplay::AfterNumber => {
341                                    write!(f, "{}- {}", amount_str, currency.get_name())
342                                }
343                                NegativeAmountDisplay::Parentheses => {
344                                    write!(f, "({} {})", amount_str, currency.get_name())
345                                }
346                            }
347                        } else {
348                            write!(f, "{} {}", amount_str, currency.get_name())
349                        }
350                    }
351                }
352            }
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use std::rc::Rc;
360
361    use num::BigRational;
362
363    use crate::models::{Currency, CurrencyDisplayFormat};
364
365    use super::Money;
366
367    #[test]
368    fn rounding() {
369        let one_decimal_format = CurrencyDisplayFormat::from("-1234.5 EUR");
370        let no_decimal_format = CurrencyDisplayFormat::from("-1234 EUR");
371        let eur = Rc::new(Currency::from("EUR"));
372
373        // Money amount
374        let m1 = Money::from((eur.clone(), BigRational::from_float(-17.77).unwrap()));
375
376        eur.set_format(&one_decimal_format);
377        assert_eq!(format!("{}", &m1), "-17.8 EUR");
378
379        eur.set_format(&no_decimal_format);
380        assert_eq!(format!("{}", &m1), "-18 EUR");
381    }
382}