1use core::cmp::Ordering;
7use core::fmt::Write;
8
9use thiserror::Error;
10
11use crate::{String, ToString, common::Money};
12
13const NANO_FACTOR: i32 = 1_000_000_000;
14
15#[derive(Debug, Error, PartialEq, Eq, Clone)]
17#[non_exhaustive]
18pub enum MoneyError {
19 #[error("Currency mismatch: Expected '{expected}', found '{found}'")]
20 CurrencyMismatch { expected: String, found: String },
21 #[error("Money arithmetic operation failed (overflow, underflow, or invalid operand)")]
22 OutOfRange,
23}
24
25fn normalize_money_fields_checked(
26 mut units: i64,
27 mut nanos: i32,
28) -> Result<(i64, i32), MoneyError> {
29 if nanos.abs() >= NANO_FACTOR {
30 let units_carry = i64::from(nanos / (NANO_FACTOR));
31 units = units
32 .checked_add(units_carry)
33 .ok_or(MoneyError::OutOfRange)?;
34 nanos %= NANO_FACTOR;
35 }
36
37 if units > 0 && nanos < 0 {
38 units = units
39 .checked_sub(1)
40 .ok_or(MoneyError::OutOfRange)?;
41 nanos = nanos
42 .checked_add(NANO_FACTOR)
43 .ok_or(MoneyError::OutOfRange)?;
44 } else if units < 0 && nanos > 0 {
45 units = units
46 .checked_add(1)
47 .ok_or(MoneyError::OutOfRange)?;
48 nanos = nanos
49 .checked_sub(NANO_FACTOR)
50 .ok_or(MoneyError::OutOfRange)?;
51 }
52
53 Ok((units, nanos))
54}
55
56impl PartialOrd for Money {
57 #[inline]
58 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
59 if self.currency_code != other.currency_code {
60 return None;
61 }
62
63 self.total_nanos()
64 .partial_cmp(&other.total_nanos())
65 }
66}
67
68fn fields_from_total_nanos(total: i128) -> Result<(i64, i32), MoneyError> {
69 let factor = i128::from(NANO_FACTOR);
70
71 let units_val = total / factor;
72 let units = i64::try_from(units_val).map_err(|_| MoneyError::OutOfRange)?;
73
74 let remainder_val = total % factor;
75 let nanos = i32::try_from(remainder_val).map_err(|_| MoneyError::OutOfRange)?;
76
77 Ok((units, nanos))
78}
79
80impl Money {
81 #[inline]
83 #[must_use]
84 pub const fn total_nanos(&self) -> i128 {
85 (self.units as i128) * (NANO_FACTOR as i128) + (self.nanos as i128)
86 }
87
88 pub fn from_total_nanos(currency: impl Into<String>, total: i128) -> Result<Self, MoneyError> {
90 let (units, nanos) = fields_from_total_nanos(total)?;
91
92 Ok(Self {
93 currency_code: currency.into(),
94 units,
95 nanos,
96 })
97 }
98
99 #[must_use]
101 pub fn to_formatted_string(&self, symbol: &str, decimal_places: u32) -> String {
102 let decimal_places = u32::min(9, decimal_places);
103
104 let mut current_units: i128 = i128::from(self.units);
105 let mut current_nanos: i128 = i128::from(self.nanos);
106
107 let ten_pow_9 = i128::from(NANO_FACTOR);
108 if current_nanos >= ten_pow_9 || current_nanos <= -ten_pow_9 {
109 current_units += current_nanos / ten_pow_9;
110 current_nanos %= ten_pow_9;
111 }
112
113 if current_units > 0 && current_nanos < 0 {
114 current_units -= 1;
115 current_nanos += ten_pow_9;
116 } else if current_units < 0 && current_nanos > 0 {
117 current_units += 1;
118 current_nanos -= ten_pow_9;
119 }
120
121 let mut rounded_nanos = 0;
122 let mut units_carry = 0;
123
124 if decimal_places > 0 {
125 let power_of_10_for_display = 10_i128.pow(decimal_places);
126 let rounding_power = 10_i128.pow(9 - decimal_places);
127
128 let abs_nanos = current_nanos.abs();
129
130 let remainder_for_rounding = abs_nanos % rounding_power;
131 rounded_nanos = abs_nanos / rounding_power;
132
133 if rounding_power > 1 && remainder_for_rounding >= rounding_power / 2 {
137 rounded_nanos += 1;
138 }
139
140 if rounded_nanos >= power_of_10_for_display {
142 units_carry = 1;
143 rounded_nanos = 0;
144 }
145 }
146
147 let is_negative = current_units < 0 || (current_units == 0 && current_nanos < 0);
148
149 let final_units_abs = current_units.abs() + units_carry;
150
151 let mut formatted_string = String::new();
152
153 if is_negative {
154 formatted_string.push('-');
155 }
156 formatted_string.push_str(symbol);
157 formatted_string.push_str(&final_units_abs.to_string());
158
159 if decimal_places > 0 {
160 formatted_string.push('.');
161 let _ = write!(
163 formatted_string,
164 "{:0width$}",
165 rounded_nanos,
166 width = decimal_places as usize
167 );
168 }
169
170 formatted_string
171 }
172
173 pub fn normalize(mut self) -> Result<Self, MoneyError> {
175 let (normalized_units, normalized_nanos) =
176 normalize_money_fields_checked(self.units, self.nanos)?;
177 self.units = normalized_units;
178 self.nanos = normalized_nanos;
179
180 Ok(self)
181 }
182
183 pub fn new(
185 currency_code: impl Into<String>,
186 units: i64,
187 nanos: i32,
188 ) -> Result<Self, MoneyError> {
189 let (normalized_units, normalized_nanos) = normalize_money_fields_checked(units, nanos)?;
190 Ok(Self {
191 currency_code: currency_code.into(),
192 units: normalized_units,
193 nanos: normalized_nanos,
194 })
195 }
196
197 pub fn to_rounded_imprecise_f64(&self, decimal_places: u32) -> Result<f64, MoneyError> {
206 if decimal_places > i32::MAX as u32 {
207 return Err(MoneyError::OutOfRange);
208 }
209
210 let full_amount = self.as_imprecise_f64();
211
212 let factor_exponent: i32 = decimal_places
213 .try_into()
214 .map_err(|_| MoneyError::OutOfRange)?;
215 let factor = 10.0f64.powi(factor_exponent);
216
217 if !factor.is_finite() {
218 return Err(MoneyError::OutOfRange);
219 }
220
221 let result = (full_amount * factor).round() / factor;
222
223 if !result.is_finite() {
224 return Err(MoneyError::OutOfRange);
225 }
226
227 Ok(result)
228 }
229
230 #[must_use]
234 pub fn as_imprecise_f64(&self) -> f64 {
235 self.units as f64 + (f64::from(self.nanos) / 1_000_000_000.0)
236 }
237
238 pub fn from_imprecise_f64(
245 currency_code: impl Into<String>,
246 amount: f64,
247 ) -> Result<Self, MoneyError> {
248 if !amount.is_finite() {
249 return Err(MoneyError::OutOfRange);
250 }
251
252 let truncated_amount = amount.trunc();
253
254 if truncated_amount > i64::MAX as f64 || truncated_amount < i64::MIN as f64 {
255 return Err(MoneyError::OutOfRange);
256 }
257
258 #[allow(clippy::cast_possible_truncation)]
260 let units = truncated_amount as i64;
261
262 let raw_nanos_f64 = amount.fract().abs() * f64::from(NANO_FACTOR);
263 #[allow(clippy::cast_possible_truncation)]
265 let nanos: i32 = raw_nanos_f64.round() as i32;
266
267 let final_nanos = if units < 0 && nanos > 0 {
268 -nanos
269 } else if units == 0 && amount < 0.0 && nanos > 0 {
270 -nanos
272 } else {
273 nanos
274 };
275
276 Self::new(currency_code, units, final_nanos)
277 }
278
279 pub fn try_add(&self, other: &Self) -> Result<Self, MoneyError> {
282 if self.currency_code != other.currency_code {
283 return Err(MoneyError::CurrencyMismatch {
284 expected: self.currency_code.clone(),
285 found: other.currency_code.clone(),
286 });
287 }
288
289 let total = self
290 .total_nanos()
291 .checked_add(other.total_nanos())
292 .ok_or(MoneyError::OutOfRange)?;
293 Self::from_total_nanos(self.currency_code.clone(), total)
294 }
295
296 pub fn try_add_assign(&mut self, other: &Self) -> Result<(), MoneyError> {
299 if self.currency_code != other.currency_code {
300 return Err(MoneyError::CurrencyMismatch {
301 expected: self.currency_code.clone(),
302 found: other.currency_code.clone(),
303 });
304 }
305
306 let total = self
307 .total_nanos()
308 .checked_add(other.total_nanos())
309 .ok_or(MoneyError::OutOfRange)?;
310 let (new_units, new_nanos) = fields_from_total_nanos(total)?;
311
312 self.units = new_units;
313 self.nanos = new_nanos;
314 Ok(())
315 }
316
317 pub fn try_sub(&self, other: &Self) -> Result<Self, MoneyError> {
320 if self.currency_code != other.currency_code {
321 return Err(MoneyError::CurrencyMismatch {
322 expected: self.currency_code.clone(),
323 found: other.currency_code.clone(),
324 });
325 }
326
327 let total = self
328 .total_nanos()
329 .checked_sub(other.total_nanos())
330 .ok_or(MoneyError::OutOfRange)?;
331 Self::from_total_nanos(self.currency_code.clone(), total)
332 }
333
334 pub fn try_sub_assign(&mut self, other: &Self) -> Result<(), MoneyError> {
337 if self.currency_code != other.currency_code {
338 return Err(MoneyError::CurrencyMismatch {
339 expected: self.currency_code.clone(),
340 found: other.currency_code.clone(),
341 });
342 }
343
344 let total = self
345 .total_nanos()
346 .checked_sub(other.total_nanos())
347 .ok_or(MoneyError::OutOfRange)?;
348 let (new_units, new_nanos) = fields_from_total_nanos(total)?;
349
350 self.units = new_units;
351 self.nanos = new_nanos;
352 Ok(())
353 }
354
355 pub fn try_mul_i64(&self, rhs: i64) -> Result<Self, MoneyError> {
358 let total = self
359 .total_nanos()
360 .checked_mul(i128::from(rhs))
361 .ok_or(MoneyError::OutOfRange)?;
362 Self::from_total_nanos(self.currency_code.clone(), total)
363 }
364
365 pub fn try_mul_f64(&self, rhs: f64) -> Result<Self, MoneyError> {
369 if !rhs.is_finite() {
370 return Err(MoneyError::OutOfRange);
371 }
372
373 let decimal_amount = self.as_imprecise_f64();
374 let result_decimal = decimal_amount * rhs;
375
376 if !result_decimal.is_finite() {
377 return Err(MoneyError::OutOfRange);
378 }
379
380 Self::from_imprecise_f64(self.currency_code.clone(), result_decimal)
382 }
383
384 pub fn try_div_i64(&self, rhs: i64) -> Result<Self, MoneyError> {
387 if rhs == 0 {
388 return Err(MoneyError::OutOfRange);
389 }
390
391 let total = self
392 .total_nanos()
393 .checked_div(i128::from(rhs))
394 .ok_or(MoneyError::OutOfRange)?;
395 Self::from_total_nanos(self.currency_code.clone(), total)
396 }
397
398 pub fn try_div_f64(&self, rhs: f64) -> Result<Self, MoneyError> {
402 if rhs == 0.0 {
403 return Err(MoneyError::OutOfRange);
404 }
405 if !rhs.is_finite() {
406 return Err(MoneyError::OutOfRange);
407 }
408
409 let decimal_amount = self.as_imprecise_f64();
410 let result_decimal = decimal_amount / rhs;
411
412 if !result_decimal.is_finite() {
413 return Err(MoneyError::OutOfRange);
414 }
415
416 Self::from_imprecise_f64(self.currency_code.clone(), result_decimal)
417 }
418
419 pub fn try_neg(&self) -> Result<Self, MoneyError> {
422 let neg_units = self
423 .units
424 .checked_neg()
425 .ok_or(MoneyError::OutOfRange)?;
426 let neg_nanos = self
427 .nanos
428 .checked_neg()
429 .ok_or(MoneyError::OutOfRange)?;
430
431 Self::new(self.currency_code.clone(), neg_units, neg_nanos)
432 }
433
434 #[must_use]
437 #[inline]
438 pub fn is_currency(&self, code: &str) -> bool {
439 self.currency_code == code
440 }
441
442 #[must_use]
444 #[inline]
445 pub fn is_usd(&self) -> bool {
446 self.is_currency("USD")
447 }
448
449 #[must_use]
451 #[inline]
452 pub fn is_eur(&self) -> bool {
453 self.is_currency("EUR")
454 }
455
456 #[must_use]
458 #[inline]
459 pub fn is_gbp(&self) -> bool {
460 self.is_currency("GBP")
461 }
462
463 #[must_use]
465 #[inline]
466 pub fn is_jpy(&self) -> bool {
467 self.is_currency("JPY")
468 }
469
470 #[must_use]
472 #[inline]
473 pub fn is_cad(&self) -> bool {
474 self.is_currency("CAD")
475 }
476
477 #[must_use]
479 #[inline]
480 pub fn is_aud(&self) -> bool {
481 self.is_currency("AUD")
482 }
483
484 #[must_use]
486 #[inline]
487 pub const fn is_positive(&self) -> bool {
488 self.units > 0 || (self.units == 0 && self.nanos > 0)
489 }
490
491 #[must_use]
493 #[inline]
494 pub const fn is_negative(&self) -> bool {
495 self.units < 0 || (self.units == 0 && self.nanos < 0)
496 }
497
498 #[must_use]
500 #[inline]
501 pub const fn is_zero(&self) -> bool {
502 self.units == 0 && self.nanos == 0
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 fn usd(u: i64, n: i32) -> Money {
511 Money::new("USD", u, n).unwrap()
512 }
513
514 fn eur(u: i64, n: i32) -> Money {
515 Money::new("EUR", u, n).unwrap()
516 }
517
518 #[test]
519 fn test_normalization_carry() {
520 let m = usd(1, 1_500_000_000);
523 assert_eq!(m.units, 2);
524 assert_eq!(m.nanos, 500_000_000);
525
526 let m = usd(-1, -1_500_000_000);
529 assert_eq!(m.units, -2);
530 assert_eq!(m.nanos, -500_000_000);
531
532 let m = usd(1, 1_000_000_000);
534 assert_eq!(m.units, 2);
535 assert_eq!(m.nanos, 0);
536 }
537
538 #[test]
539 fn test_normalization_sign_correction() {
540 let m = usd(1, -100);
543 assert_eq!(m.units, 0);
544 assert_eq!(m.nanos, 999_999_900);
545
546 let m = usd(-1, 100);
549 assert_eq!(m.units, 0);
550 assert_eq!(m.nanos, -999_999_900);
551
552 let m = usd(0, -500);
554 assert!(m.is_negative());
555 }
556
557 #[test]
560 fn test_add_sub() {
561 let m1 = usd(10, 500_000_000);
563 let m2 = usd(20, 500_000_000);
564 let sum = m1.try_add(&m2).unwrap();
565 assert_eq!(sum.units, 31);
566 assert_eq!(sum.nanos, 0);
567
568 let max = usd(i64::MAX, 0);
570 let one = usd(1, 0);
571 assert_eq!(max.try_add(&one), Err(MoneyError::OutOfRange));
572
573 let m1 = usd(1, 0);
575 let m2 = usd(2, 0);
576 let diff = m1.try_sub(&m2).unwrap();
577 assert_eq!(diff.units, -1);
578 assert_eq!(diff.nanos, 0);
579
580 let min = usd(i64::MIN, 0);
582 let one = usd(1, 0);
583 assert_eq!(min.try_sub(&one), Err(MoneyError::OutOfRange));
584
585 let u = usd(10, 0);
587 let e = eur(10, 0);
588 assert!(matches!(
589 u.try_add(&e),
590 Err(MoneyError::CurrencyMismatch { .. })
591 ));
592 }
593
594 #[test]
595 fn test_assign_ops() {
596 let mut m = usd(1, 500_000_000);
598 m.try_add_assign(&usd(0, 600_000_000)).unwrap();
599 assert_eq!(m.units, 2);
601 assert_eq!(m.nanos, 100_000_000);
602
603 m.try_sub_assign(&usd(3, 0)).unwrap();
604 assert_eq!(m.units, 0);
606 assert_eq!(m.nanos, -900_000_000);
607 }
608
609 #[test]
610 fn test_mul() {
611 let m = usd(10, 500_000_000); let res = m.try_mul_i64(2).unwrap();
614 assert_eq!(res.units, 21);
615
616 let res = m.try_mul_i64(-2).unwrap();
618 assert_eq!(res.units, -21);
619
620 let huge = usd((i64::MAX / 2) + 2, 0);
623 assert_eq!(huge.try_mul_i64(2), Err(MoneyError::OutOfRange));
624
625 let edge = usd(i64::MAX - 1, 600_000_000);
628 assert_eq!(edge.try_mul_i64(2), Err(MoneyError::OutOfRange));
631 }
632
633 #[test]
634 fn test_div() {
635 let m = usd(10, 0);
637 let res = m.try_div_i64(2).unwrap();
638 assert_eq!(res.units, 5);
639
640 let m = usd(1, 0);
643 let res = m.try_div_i64(3).unwrap();
644 assert_eq!(res.units, 0);
645 assert_eq!(res.nanos, 333_333_333);
646
647 assert_eq!(m.try_div_i64(0), Err(MoneyError::OutOfRange));
649 }
650
651 #[test]
654 fn test_f64_math_robustness() {
655 let m = usd(10, 0);
656
657 let res = m.try_mul_f64(1.5).unwrap();
659 assert_eq!(res.units, 15);
660
661 let res = m.try_div_f64(2.0).unwrap();
663 assert_eq!(res.units, 5);
664
665 assert_eq!(m.try_mul_f64(f64::INFINITY), Err(MoneyError::OutOfRange));
667 assert_eq!(m.try_div_f64(0.0), Err(MoneyError::OutOfRange)); let m = Money::from_imprecise_f64("USD", 10.55).unwrap();
672 assert_eq!(m.nanos, 550_000_000);
673 let f = m.as_imprecise_f64();
674 assert!((f - 10.55).abs() < 1e-9);
675 }
676
677 #[test]
678 fn test_f64_construction_edge_cases() {
679 assert_eq!(
681 Money::from_imprecise_f64("USD", f64::NAN),
682 Err(MoneyError::OutOfRange)
683 );
684
685 assert_eq!(
687 Money::from_imprecise_f64("USD", 1e20),
688 Err(MoneyError::OutOfRange)
689 );
690
691 let m = Money::from_imprecise_f64("USD", -0.005).unwrap();
693 assert_eq!(m.units, 0);
694 assert_eq!(m.nanos, -5_000_000);
695 }
696
697 #[test]
700 fn test_comparison() {
701 let m1 = usd(10, 0);
702 let m2 = usd(10, 1);
703 let m3 = usd(10, 0);
704
705 assert!(m1 < m2);
706 assert!(m1 == m3);
707
708 let e = eur(10, 0);
709 assert_eq!(m1.partial_cmp(&e), None);
711 }
712
713 #[test]
714 fn test_flags() {
715 let zero = usd(0, 0);
716 assert!(zero.is_zero());
717 assert!(!zero.is_positive());
718 assert!(!zero.is_negative());
719
720 let pos = usd(0, 1);
721 assert!(pos.is_positive());
722
723 let neg = usd(0, -1);
724 assert!(neg.is_negative());
725 }
726
727 #[test]
730 fn test_formatting_precision() {
731 assert_eq!(usd(1, 0).to_formatted_string("$", 2), "$1.00");
733
734 assert_eq!(usd(1, 5_000_000).to_formatted_string("$", 2), "$1.01");
737
738 assert_eq!(usd(1, 4_000_000).to_formatted_string("$", 2), "$1.00");
741
742 assert_eq!(
744 usd(1, 123_456_789).to_formatted_string("$", 9),
745 "$1.123456789"
746 );
747
748 assert_eq!(usd(1, 900_000_000).to_formatted_string("$", 0), "$1"); assert_eq!(usd(-5, -500_000_000).to_formatted_string("€", 2), "-€5.50");
753 }
754
755 #[test]
756 fn test_arithmetic_rollover_bugs() {
757 let m = usd(0, 500_000_000);
759 let res = m.try_mul_i64(10).unwrap(); assert_eq!(res.units, 5);
761 assert_eq!(res.nanos, 0);
762
763 let m1 = usd(0, 600_000_000);
765 let m2 = usd(0, 600_000_000);
766 let res = m1.try_add(&m2).unwrap(); assert_eq!(res.units, 1);
768 assert_eq!(res.nanos, 200_000_000);
769 }
770
771 #[test]
772 fn test_formatting() {
773 let m = usd(10, 500_000_000); assert_eq!(m.to_formatted_string("$", 2), "$10.50");
775
776 let m = usd(10, 555_000_000); assert_eq!(m.to_formatted_string("$", 2), "$10.56");
779
780 let m = usd(-5, -500_000_000); assert_eq!(m.to_formatted_string("$", 2), "-$5.50");
783 }
784
785 #[test]
786 fn test_f64_conversions() {
787 let m = Money::from_imprecise_f64("USD", 10.50).unwrap();
789 assert_eq!(m.units, 10);
790 assert_eq!(m.nanos, 500_000_000);
791
792 let m = usd(10, 555_000_000);
795 let f = m.to_rounded_imprecise_f64(2).unwrap();
796 assert!((f - 10.56).abs() < f64::EPSILON);
797 }
798}