monee/
money.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::currency::*;
5use crate::Error;
6
7#[derive(PartialEq, Eq, Clone)]
8pub struct Money {
9    amount: i64,
10    currency: &'static Currency,
11    is_negative: bool,
12}
13
14#[macro_export]
15macro_rules! money {
16    ($x:expr, $y:expr) => {
17        Money::from_string($x.to_string(), $y.to_string()).unwrap();
18    };
19}
20
21impl Money {
22    pub fn from_string(s: String, currency: String) -> Result<Money, Error> {
23        let currency = Currency::from_string(currency).unwrap_or(Currency::find("USD")?);
24        let mut amount: i64 = 0;
25        let mut is_negative: bool = false;
26
27        if s != "" {
28            let mut decimal_found: bool = false;
29            let mut decimal_fill: u8 = 0;
30            let mut decimal_places: u8 = 0;
31
32            for char in s.chars() {
33                if char == '-' {
34                    is_negative = true;
35                    continue;
36                }
37                if char == '.' {
38                    decimal_found = true;
39                    continue;
40                }
41
42                let num = char.to_digit(10).ok_or(Error::InvalidAmount)? as i64;
43
44                if decimal_found {
45                    decimal_fill += 1;
46                    decimal_places += 1;
47
48                    if decimal_places == 3 {
49                        if num > 4 {
50                            // Add num
51                            amount = amount.checked_add(1).ok_or(Error::InvalidAmount)?;
52                        }
53                        decimal_places -= 1;
54                        break;
55                    }
56                }
57
58                // Multiply by 10 to pad right with one place
59                amount = amount.checked_mul(10).ok_or(Error::InvalidAmount)?;
60
61                // Add num
62                amount = amount.checked_add(num).ok_or(Error::InvalidAmount)?;
63            }
64
65            if !decimal_found {
66                decimal_fill = 2;
67            }
68
69            if decimal_places == 2 {
70                decimal_fill = 0;
71            }
72
73            if decimal_fill > 0 {
74                loop {
75                    amount *= 10;
76                    decimal_fill -= 1;
77
78                    if decimal_fill == 0 {
79                        break;
80                    }
81                }
82            }
83        }
84
85        Ok(Money {
86            amount,
87            currency,
88            is_negative,
89        })
90    }
91}
92
93impl FromStr for Money {
94    type Err = Error;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        Money::from_string(s.to_string(), "".to_string())
98    }
99}
100
101impl fmt::Display for Money {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        let mut s = format!("{:03}", self.amount);
104
105        if self.amount == 0 {
106            s = format!("{}", self.amount)
107        } else {
108            s.insert(s.len() - 2, '.');
109
110            let mut remainder: usize = s.len() - 3;
111            let mut position: usize = s.len() - 3;
112
113            loop {
114                if remainder <= 3 {
115                    break;
116                }
117
118                position -= 3;
119
120                s.insert(position, ',');
121
122                remainder = position;
123            }
124
125            if self.is_negative {
126                s.insert(0, '-');
127            }
128
129            if self.currency.symbol != "" {
130                let fill = f.fill();
131                if let Some(width) = f.width() {
132                    for _ in 0..width {
133                        s.insert(0, fill)
134                    }
135                }
136                s = format!("{}{}", self.currency.symbol, s)
137            }
138        }
139
140        write!(f, "{}", s.to_string())
141    }
142}
143
144impl fmt::Debug for Money {
145    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146        fmt::Display::fmt(self, f)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_create_from_string() {
156        let money: Money = "".parse().unwrap();
157        assert_eq!("0", money.to_string());
158
159        let money: Money = "0".parse().unwrap();
160        assert_eq!("0", money.to_string());
161
162        let money: Money = "0.0".parse().unwrap();
163        assert_eq!("0", money.to_string());
164
165        let money: Money = "0.00".parse().unwrap();
166        assert_eq!("0", money.to_string());
167
168        let money: Money = "111".parse().unwrap();
169        assert_eq!("$111.00", money.to_string());
170
171        let money: Money = "111.0".parse().unwrap();
172        assert_eq!("$111.00", money.to_string());
173
174        let money: Money = "111.1".parse().unwrap();
175        assert_eq!("$111.10", money.to_string());
176
177        let money: Money = "111.01".parse().unwrap();
178        assert_eq!("$111.01", money.to_string());
179
180        let money: Money = "112.00".parse().unwrap();
181        assert_eq!("$112.00", money.to_string());
182
183        let money = money!("19.9999", "USD");
184        assert_eq!("$20.00", format!("{}", money));
185
186        let money: Money = money!("20.00", "USD");
187        assert_eq!("$20.00", money.to_string())
188    }
189
190    #[test]
191    fn test_failing_creating_from_string() {
192        let money = "111f.05f".parse::<Money>();
193        assert_eq!(Error::InvalidAmount, money.unwrap_err());
194    }
195
196    #[test]
197    fn money_fmt_separates_digits() {
198        let usd = money!(0, "USD"); // Zero Dollars
199        let expected_usd_fmt = "0";
200        assert_eq!(format!("{}", usd), expected_usd_fmt);
201    }
202
203    #[test]
204    fn money_format_rounds_exponent() {
205        // // 19.999 rounds to 20 for USD
206        let money = money!("19.9999", "USD");
207        assert_eq!("$20.00", format!("{}", money));
208
209        // // 29.111 rounds to 29.11 for USD
210        let money = money!("29.111", "USD");
211        assert_eq!("$29.11", format!("{}", money));
212
213        let money: Money = "11123.0154".parse().unwrap();
214        assert_eq!("$11,123.02", money.to_string());
215
216        let money: Money = "1112345.0154".parse().unwrap();
217        assert_eq!("$1,112,345.02", money.to_string());
218    }
219
220    #[test]
221    fn money_format_padding() {
222        let money = money!("20.00", "USD");
223        assert_eq!("$ 20.00", format!("{: >1}", money));
224    }
225
226    #[test]
227    fn money_from_float() {
228        let money = money!(20, "USD");
229        assert_eq!("$ 20.00", format!("{: >1}", money));
230
231        let money = money!(20.10, "USD");
232        assert_eq!("$ 20.10", format!("{: >1}", money));
233
234        let money = money!(20.105, "USD");
235        assert_eq!("$ 20.11", format!("{: >1}", money));
236    }
237
238    #[test]
239    fn money_allow_negative() {
240        let money = money!(-20, "USD");
241        assert_eq!("$ -20.00", format!("{: >1}", money));
242
243        let money: Money = "-20".parse().unwrap();
244        assert_eq!("$-20.00", money.to_string());
245    }
246}