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#[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 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
237 match self {
238 Money::Zero => write!(f, "0"),
239 Money::Money { amount, currency } => {
240 let format = currency.display_format.borrow();
243 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 let integer_part: String = full_str
253 .split('.')
254 .next()
255 .unwrap()
256 .split('-')
257 .last()
258 .unwrap()
259 .into(); let decimal_part = amount.fract().to_f64().unwrap();
262
263 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 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, 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 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}