1use std::{
48 cmp::Ordering,
49 fmt::{Debug, Display},
50 hash::{Hash, Hasher},
51 ops::{Add, Div, Mul, Neg, Sub},
52 str::FromStr,
53};
54
55use nautilus_core::{
56 correctness::{
57 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
58 check_in_range_inclusive_f64,
59 },
60 string::formatting::Separable,
61};
62use rust_decimal::Decimal;
63use serde::{Deserialize, Deserializer, Serialize};
64
65#[cfg(not(any(feature = "defi", feature = "high-precision")))]
66use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
67#[cfg(any(feature = "defi", feature = "high-precision"))]
68use super::fixed::{f64_to_fixed_i128, fixed_i128_to_f64};
69#[cfg(feature = "defi")]
70use crate::types::fixed::MAX_FLOAT_PRECISION;
71use crate::types::{
72 Currency,
73 fixed::{
74 FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision, mantissa_exponent_to_fixed_i128,
75 raw_scales_match,
76 },
77};
78
79#[cfg(feature = "high-precision")]
84pub type MoneyRaw = i128;
85
86#[cfg(not(feature = "high-precision"))]
87pub type MoneyRaw = i64;
88
89#[unsafe(no_mangle)]
100#[allow(unsafe_code)]
101pub static MONEY_RAW_MAX: MoneyRaw = (MONEY_MAX * FIXED_SCALAR) as MoneyRaw;
102
103#[unsafe(no_mangle)]
112#[allow(unsafe_code)]
113pub static MONEY_RAW_MIN: MoneyRaw = (MONEY_MIN * FIXED_SCALAR) as MoneyRaw;
114
115#[cfg(feature = "high-precision")]
120pub const MONEY_MAX: f64 = 17_014_118_346_046.0;
122
123#[cfg(not(feature = "high-precision"))]
124pub const MONEY_MAX: f64 = 9_223_372_036.0;
126
127#[cfg(feature = "high-precision")]
132pub const MONEY_MIN: f64 = -17_014_118_346_046.0;
134
135#[cfg(not(feature = "high-precision"))]
136pub const MONEY_MIN: f64 = -9_223_372_036.0;
138
139#[repr(C)]
146#[derive(Clone, Copy, Eq)]
147#[cfg_attr(
148 feature = "python",
149 pyo3::pyclass(
150 module = "nautilus_trader.core.nautilus_pyo3.model",
151 frozen,
152 from_py_object
153 )
154)]
155#[cfg_attr(
156 feature = "python",
157 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
158)]
159pub struct Money {
160 pub raw: MoneyRaw,
162 pub currency: Currency,
164}
165
166impl Money {
167 pub fn new_checked(amount: f64, currency: Currency) -> CorrectnessResult<Self> {
177 check_in_range_inclusive_f64(amount, MONEY_MIN, MONEY_MAX, "amount")?;
181
182 #[cfg(feature = "defi")]
183 if currency.precision > MAX_FLOAT_PRECISION {
184 return Err(CorrectnessError::PredicateViolation {
186 message: format!(
187 "`currency.precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Money::from_wei()` for wei values instead"
188 ),
189 });
190 }
191
192 #[cfg(feature = "high-precision")]
193 let raw = f64_to_fixed_i128(amount, currency.precision);
194
195 #[cfg(not(feature = "high-precision"))]
196 let raw = f64_to_fixed_i64(amount, currency.precision);
197
198 Ok(Self { raw, currency })
199 }
200
201 #[must_use]
207 pub fn new(amount: f64, currency: Currency) -> Self {
208 Self::new_checked(amount, currency).expect_display(FAILED)
209 }
210
211 #[must_use]
217 pub fn from_raw(raw: MoneyRaw, currency: Currency) -> Self {
218 Self::from_raw_checked(raw, currency).expect_display(FAILED)
219 }
220
221 pub fn from_raw_checked(raw: MoneyRaw, currency: Currency) -> CorrectnessResult<Self> {
230 if raw < MONEY_RAW_MIN || raw > MONEY_RAW_MAX {
231 return Err(CorrectnessError::PredicateViolation {
232 message: format!(
233 "`raw` value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
234 ),
235 });
236 }
237
238 check_fixed_precision(currency.precision)?;
239
240 Ok(Self { raw, currency })
250 }
251
252 #[must_use]
261 pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, currency: Currency) -> Self {
262 check_fixed_precision(currency.precision).expect_display(FAILED);
263
264 if mantissa == 0 {
265 return Self { raw: 0, currency };
266 }
267
268 let raw_i128 =
269 mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, currency.precision)
270 .expect("Overflow in Money::from_mantissa_exponent");
271
272 #[allow(
273 clippy::useless_conversion,
274 reason = "i128 to MoneyRaw is real when not high-precision"
275 )]
276 let raw: MoneyRaw = raw_i128
277 .try_into()
278 .expect("Raw value exceeds MoneyRaw range in Money::from_mantissa_exponent");
279 assert!(
280 raw >= MONEY_RAW_MIN && raw <= MONEY_RAW_MAX,
281 "`raw` value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
282 );
283
284 Self { raw, currency }
285 }
286
287 #[must_use]
293 pub fn zero(currency: Currency) -> Self {
294 Self::new(0.0, currency)
295 }
296
297 #[must_use]
300 pub fn normalized(&self) -> Self {
301 #[cfg(feature = "high-precision")]
302 let raw = super::fixed::correct_raw_i128(self.raw, self.currency.precision);
303
304 #[cfg(not(feature = "high-precision"))]
305 let raw = super::fixed::correct_raw_i64(self.raw, self.currency.precision);
306
307 Self {
308 raw,
309 currency: self.currency,
310 }
311 }
312
313 #[must_use]
315 pub fn is_zero(&self) -> bool {
316 self.raw == 0
317 }
318
319 #[must_use]
321 pub fn is_positive(&self) -> bool {
322 self.raw > 0
323 }
324
325 #[must_use]
335 pub fn checked_add(self, rhs: Self) -> Option<Self> {
336 assert_eq!(
337 self.currency, rhs.currency,
338 "Currency mismatch: cannot add {} to {}",
339 rhs.currency.code, self.currency.code
340 );
341
342 if !raw_scales_match(self.currency.precision, rhs.currency.precision) {
343 return None;
344 }
345 let raw = self.raw.checked_add(rhs.raw)?;
346 if raw < MONEY_RAW_MIN || raw > MONEY_RAW_MAX {
347 return None;
348 }
349 Some(Self {
350 raw,
351 currency: self.currency,
352 })
353 }
354
355 #[must_use]
365 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
366 assert_eq!(
367 self.currency, rhs.currency,
368 "Currency mismatch: cannot subtract {} from {}",
369 rhs.currency.code, self.currency.code
370 );
371
372 if !raw_scales_match(self.currency.precision, rhs.currency.precision) {
373 return None;
374 }
375 let raw = self.raw.checked_sub(rhs.raw)?;
376 if raw < MONEY_RAW_MIN || raw > MONEY_RAW_MAX {
377 return None;
378 }
379 Some(Self {
380 raw,
381 currency: self.currency,
382 })
383 }
384
385 #[cfg(feature = "high-precision")]
386 #[must_use]
392 pub fn as_f64(&self) -> f64 {
393 #[cfg(feature = "defi")]
394 assert!(
395 self.currency.precision <= MAX_FLOAT_PRECISION,
396 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
397 );
398
399 fixed_i128_to_f64(self.raw)
400 }
401
402 #[cfg(not(feature = "high-precision"))]
403 #[must_use]
409 pub fn as_f64(&self) -> f64 {
410 #[cfg(feature = "defi")]
411 if self.currency.precision > MAX_FLOAT_PRECISION {
412 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
413 }
414
415 fixed_i64_to_f64(self.raw)
416 }
417
418 #[must_use]
420 pub fn as_decimal(&self) -> Decimal {
421 let precision = self.currency.precision;
423 let precision_diff = FIXED_PRECISION.saturating_sub(precision);
424
425 let rescaled_raw = self.raw / MoneyRaw::pow(10, u32::from(precision_diff));
428
429 #[allow(
430 clippy::useless_conversion,
431 reason = "i128::from is real when MoneyRaw is i64"
432 )]
433 Decimal::from_i128_with_scale(i128::from(rescaled_raw), u32::from(precision))
434 }
435
436 #[must_use]
438 pub fn to_formatted_string(&self) -> String {
439 let amount_str = format!("{:.*}", self.currency.precision as usize, self.as_f64())
440 .separate_with_underscores();
441 format!("{} {}", amount_str, self.currency.code)
442 }
443
444 pub fn from_decimal(decimal: Decimal, currency: Currency) -> CorrectnessResult<Self> {
455 let exponent = -(decimal.scale() as i8);
456 let raw_i128 =
457 mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, currency.precision)?;
458
459 #[allow(
460 clippy::useless_conversion,
461 reason = "i128 to MoneyRaw is real when not high-precision"
462 )]
463 let raw: MoneyRaw =
464 raw_i128
465 .try_into()
466 .map_err(|_| CorrectnessError::PredicateViolation {
467 message: format!(
468 "Decimal value exceeds MoneyRaw range [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}]"
469 ),
470 })?;
471
472 if !(raw >= MONEY_RAW_MIN && raw <= MONEY_RAW_MAX) {
473 return Err(CorrectnessError::PredicateViolation {
474 message: format!(
475 "Raw value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
476 ),
477 });
478 }
479
480 Ok(Self { raw, currency })
481 }
482}
483
484impl FromStr for Money {
485 type Err = String;
486
487 fn from_str(value: &str) -> Result<Self, Self::Err> {
488 let parts: Vec<&str> = value.split_whitespace().collect();
489
490 if parts.len() != 2 {
492 return Err(format!(
493 "Error invalid input format '{value}'. Expected '<amount> <currency>'"
494 ));
495 }
496
497 let clean_amount = parts[0].replace('_', "");
498
499 let decimal = if clean_amount.contains('e') || clean_amount.contains('E') {
500 Decimal::from_scientific(&clean_amount)
501 .map_err(|e| format!("Error parsing amount '{}' as Decimal: {e}", parts[0]))?
502 } else {
503 Decimal::from_str(&clean_amount)
504 .map_err(|e| format!("Error parsing amount '{}' as Decimal: {e}", parts[0]))?
505 };
506
507 let currency = Currency::from_str(parts[1]).map_err(|e: anyhow::Error| e.to_string())?;
508 Self::from_decimal(decimal, currency).map_err(|e| e.to_string())
509 }
510}
511
512impl<T: AsRef<str>> From<T> for Money {
513 fn from(value: T) -> Self {
514 Self::from_str(value.as_ref()).expect(FAILED)
515 }
516}
517
518impl From<Money> for f64 {
519 fn from(money: Money) -> Self {
520 money.as_f64()
521 }
522}
523
524impl From<&Money> for f64 {
525 fn from(money: &Money) -> Self {
526 money.as_f64()
527 }
528}
529
530impl Hash for Money {
531 fn hash<H: Hasher>(&self, state: &mut H) {
532 self.raw.hash(state);
533 self.currency.hash(state);
534 }
535}
536
537impl PartialEq for Money {
538 fn eq(&self, other: &Self) -> bool {
539 self.raw == other.raw && self.currency == other.currency
540 }
541}
542
543impl PartialOrd for Money {
544 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
545 Some(self.cmp(other))
546 }
547
548 fn lt(&self, other: &Self) -> bool {
549 assert_eq!(self.currency, other.currency);
550 self.raw.lt(&other.raw)
551 }
552
553 fn le(&self, other: &Self) -> bool {
554 assert_eq!(self.currency, other.currency);
555 self.raw.le(&other.raw)
556 }
557
558 fn gt(&self, other: &Self) -> bool {
559 assert_eq!(self.currency, other.currency);
560 self.raw.gt(&other.raw)
561 }
562
563 fn ge(&self, other: &Self) -> bool {
564 assert_eq!(self.currency, other.currency);
565 self.raw.ge(&other.raw)
566 }
567}
568
569impl Ord for Money {
570 fn cmp(&self, other: &Self) -> Ordering {
571 assert_eq!(self.currency, other.currency);
572 self.raw.cmp(&other.raw)
573 }
574}
575
576impl Neg for Money {
577 type Output = Self;
578 fn neg(self) -> Self::Output {
579 Self {
580 raw: -self.raw,
581 currency: self.currency,
582 }
583 }
584}
585
586impl Add for Money {
587 type Output = Self;
588 fn add(self, rhs: Self) -> Self::Output {
589 assert_eq!(
590 self.currency, rhs.currency,
591 "Currency mismatch: cannot add {} to {}",
592 rhs.currency.code, self.currency.code
593 );
594 Self {
595 raw: self
596 .raw
597 .checked_add(rhs.raw)
598 .expect("Overflow occurred when adding `Money`"),
599 currency: self.currency,
600 }
601 }
602}
603
604impl Sub for Money {
605 type Output = Self;
606 fn sub(self, rhs: Self) -> Self::Output {
607 assert_eq!(
608 self.currency, rhs.currency,
609 "Currency mismatch: cannot subtract {} from {}",
610 rhs.currency.code, self.currency.code
611 );
612 Self {
613 raw: self
614 .raw
615 .checked_sub(rhs.raw)
616 .expect("Underflow occurred when subtracting `Money`"),
617 currency: self.currency,
618 }
619 }
620}
621
622impl Add<Decimal> for Money {
623 type Output = Decimal;
624 fn add(self, rhs: Decimal) -> Self::Output {
625 self.as_decimal() + rhs
626 }
627}
628
629impl Sub<Decimal> for Money {
630 type Output = Decimal;
631 fn sub(self, rhs: Decimal) -> Self::Output {
632 self.as_decimal() - rhs
633 }
634}
635
636impl Mul<Decimal> for Money {
637 type Output = Decimal;
638 fn mul(self, rhs: Decimal) -> Self::Output {
639 self.as_decimal() * rhs
640 }
641}
642
643impl Div<Decimal> for Money {
644 type Output = Decimal;
645 fn div(self, rhs: Decimal) -> Self::Output {
646 self.as_decimal() / rhs
647 }
648}
649
650impl Add<f64> for Money {
651 type Output = f64;
652 fn add(self, rhs: f64) -> Self::Output {
653 self.as_f64() + rhs
654 }
655}
656
657impl Sub<f64> for Money {
658 type Output = f64;
659 fn sub(self, rhs: f64) -> Self::Output {
660 self.as_f64() - rhs
661 }
662}
663
664impl Mul<f64> for Money {
665 type Output = f64;
666 fn mul(self, rhs: f64) -> Self::Output {
667 self.as_f64() * rhs
668 }
669}
670
671impl Div<f64> for Money {
672 type Output = f64;
673 fn div(self, rhs: f64) -> Self::Output {
674 self.as_f64() / rhs
675 }
676}
677
678impl Debug for Money {
679 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680 if self.currency.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
681 write!(f, "{}({}, {})", stringify!(Money), self.raw, self.currency)
682 } else {
683 write!(
684 f,
685 "{}({:.*}, {})",
686 stringify!(Money),
687 self.currency.precision as usize,
688 self.as_f64(),
689 self.currency
690 )
691 }
692 }
693}
694
695impl Display for Money {
696 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697 if self.currency.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
698 write!(f, "{} {}", self.raw, self.currency)
699 } else {
700 write!(f, "{} {}", self.as_decimal(), self.currency)
701 }
702 }
703}
704
705impl Serialize for Money {
706 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
707 where
708 S: serde::Serializer,
709 {
710 serializer.serialize_str(&self.to_string())
711 }
712}
713
714impl<'de> Deserialize<'de> for Money {
715 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
716 where
717 D: Deserializer<'de>,
718 {
719 let money_str: String = Deserialize::deserialize(deserializer)?;
720 Ok(Self::from(money_str.as_str()))
721 }
722}
723
724#[inline(always)]
730pub fn check_positive_money(value: Money, param: &str) -> CorrectnessResult<()> {
731 if value.raw <= 0 {
732 return Err(CorrectnessError::NotPositive {
733 param: param.to_string(),
734 value: value.to_string(),
735 type_name: "`Money`",
736 });
737 }
738 Ok(())
739}
740
741#[cfg(test)]
742mod tests {
743 use nautilus_core::{approx_eq, correctness::CorrectnessError};
744 use rstest::rstest;
745 use rust_decimal_macros::dec;
746
747 use super::*;
748
749 #[rstest]
750 fn test_debug() {
751 let money = Money::new(1010.12, Currency::USD());
752 let result = format!("{money:?}");
753 let expected = "Money(1010.12, USD)";
754 assert_eq!(result, expected);
755 }
756
757 #[rstest]
758 fn test_display() {
759 let money = Money::new(1010.12, Currency::USD());
760 let result = format!("{money}");
761 let expected = "1010.12 USD";
762 assert_eq!(result, expected);
763 }
764
765 #[rstest]
766 #[case(1010.12, 2, "USD", "Money(1010.12, USD)", "1010.12 USD")] #[case(123.456_789, 8, "BTC", "Money(123.45678900, BTC)", "123.45678900 BTC")] fn test_formatting_normal_precision(
769 #[case] value: f64,
770 #[case] precision: u8,
771 #[case] currency_code: &str,
772 #[case] expected_debug: &str,
773 #[case] expected_display: &str,
774 ) {
775 use crate::enums::CurrencyType;
776 let currency = Currency::new(
777 currency_code,
778 precision,
779 0,
780 currency_code,
781 CurrencyType::Fiat,
782 );
783 let money = Money::new(value, currency);
784
785 assert_eq!(format!("{money:?}"), expected_debug);
786 assert_eq!(format!("{money}"), expected_display);
787 }
788
789 #[rstest]
790 #[cfg(feature = "defi")]
791 #[case(
792 1_000_000_000_000_000_000_i128,
793 18,
794 "wei",
795 "Money(1000000000000000000, wei)",
796 "1000000000000000000 wei"
797 )] #[case(
799 2_500_000_000_000_000_000_i128,
800 18,
801 "ETH",
802 "Money(2500000000000000000, ETH)",
803 "2500000000000000000 ETH"
804 )] fn test_formatting_high_precision(
806 #[case] raw_value: i128,
807 #[case] precision: u8,
808 #[case] currency_code: &str,
809 #[case] expected_debug: &str,
810 #[case] expected_display: &str,
811 ) {
812 use crate::enums::CurrencyType;
813 let currency = Currency::new(
814 currency_code,
815 precision,
816 0,
817 currency_code,
818 CurrencyType::Crypto,
819 );
820 let money = Money::from_raw(raw_value, currency);
821
822 assert_eq!(format!("{money:?}"), expected_debug);
823 assert_eq!(format!("{money}"), expected_display);
824 }
825
826 #[rstest]
827 fn test_zero_constructor() {
828 let usd = Currency::USD();
829 let money = Money::zero(usd);
830 assert_eq!(money.raw, 0);
831 assert_eq!(money.currency, usd);
832 }
833
834 #[rstest]
835 #[should_panic(expected = "Currency mismatch")]
836 fn test_money_different_currency_addition() {
837 let usd = Money::new(1000.0, Currency::USD());
838 let btc = Money::new(1.0, Currency::BTC());
839 let _ = usd + btc; }
841
842 #[rstest] fn test_with_maximum_value() {
844 let money = Money::new_checked(MONEY_MAX, Currency::USD());
845 assert!(money.is_ok());
846 }
847
848 #[rstest] fn test_with_minimum_value() {
850 let money = Money::new_checked(MONEY_MIN, Currency::USD());
851 assert!(money.is_ok());
852 }
853
854 #[rstest]
855 fn test_new_checked_returns_typed_error_with_stable_display() {
856 let error = Money::new_checked(MONEY_MAX + 1.0, Currency::USD()).unwrap_err();
857
858 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
859 assert_eq!(
860 error.to_string(),
861 format!(
862 "invalid f64 for 'amount' not in range [{MONEY_MIN}, {MONEY_MAX}], was {}",
863 MONEY_MAX + 1.0
864 )
865 );
866 }
867
868 #[rstest]
869 fn test_money_is_zero() {
870 let zero_usd = Money::new(0.0, Currency::USD());
871 assert!(zero_usd.is_zero());
872 assert_eq!(zero_usd, Money::from("0.0 USD"));
873
874 let non_zero_usd = Money::new(100.0, Currency::USD());
875 assert!(!non_zero_usd.is_zero());
876 }
877
878 #[rstest]
879 fn test_money_is_positive() {
880 let usd = Currency::USD();
881 assert!(Money::new(100.0, usd).is_positive());
882 assert!(!Money::new(0.0, usd).is_positive());
883 assert!(!Money::new(-100.0, usd).is_positive());
884 }
885
886 #[rstest]
887 fn test_money_comparisons() {
888 let usd = Currency::USD();
889 let m1 = Money::new(100.0, usd);
890 let m2 = Money::new(200.0, usd);
891
892 assert!(m1 < m2);
893 assert!(m2 > m1);
894 assert!(m1 <= m2);
895 assert!(m2 >= m1);
896
897 let m3 = Money::new(100.0, usd);
899 assert!(m1 == m3);
900 }
901
902 #[rstest]
903 fn test_add() {
904 let a = 1000.0;
905 let b = 500.0;
906 let money1 = Money::new(a, Currency::USD());
907 let money2 = Money::new(b, Currency::USD());
908 let money3 = money1 + money2;
909 assert_eq!(money3.raw, Money::new(a + b, Currency::USD()).raw);
910 }
911
912 #[rstest]
913 fn test_sub() {
914 let usd = Currency::USD();
915 let money1 = Money::new(1000.0, usd);
916 let money2 = Money::new(250.0, usd);
917 let result = money1 - money2;
918 assert!(approx_eq!(f64, result.as_f64(), 750.0, epsilon = 1e-9));
919 assert_eq!(result.currency, usd);
920 }
921
922 #[rstest]
923 fn test_money_checked_add_within_bounds() {
924 let usd = Currency::USD();
925 let a = Money::new(100.0, usd);
926 let b = Money::new(50.0, usd);
927 assert_eq!(a.checked_add(b), Some(Money::new(150.0, usd)));
928 }
929
930 #[rstest]
931 fn test_money_checked_add_above_max_returns_none() {
932 let usd = Currency::USD();
933 let near_max = Money::from_raw(MONEY_RAW_MAX, usd);
934 let one = Money::new(1.0, usd);
935 assert_eq!(near_max.checked_add(one), None);
936 }
937
938 #[rstest]
939 fn test_money_checked_sub_within_bounds() {
940 let usd = Currency::USD();
941 let a = Money::new(100.0, usd);
942 let b = Money::new(40.0, usd);
943 assert_eq!(a.checked_sub(b), Some(Money::new(60.0, usd)));
944 }
945
946 #[rstest]
947 fn test_money_checked_sub_below_min_returns_none() {
948 let usd = Currency::USD();
949 let near_min = Money::from_raw(MONEY_RAW_MIN, usd);
950 let one = Money::new(1.0, usd);
951 assert_eq!(near_min.checked_sub(one), None);
952 }
953
954 #[rstest]
955 #[should_panic(expected = "Currency mismatch")]
956 fn test_money_checked_add_currency_mismatch_panics() {
957 let usd = Money::new(100.0, Currency::USD());
958 let aud = Money::new(50.0, Currency::AUD());
959 let _ = usd.checked_add(aud);
960 }
961
962 #[rstest]
963 #[should_panic(expected = "Currency mismatch")]
964 fn test_money_checked_sub_currency_mismatch_panics() {
965 let usd = Money::new(100.0, Currency::USD());
966 let aud = Money::new(50.0, Currency::AUD());
967 let _ = usd.checked_sub(aud);
968 }
969
970 #[rstest]
971 fn test_money_checked_add_at_exact_max_returns_some() {
972 let usd = Currency::USD();
973 let near_max = Money::from_raw(MONEY_RAW_MAX - 1, usd);
974 let one_unit = Money::from_raw(1, usd);
975 assert_eq!(
976 near_max.checked_add(one_unit),
977 Some(Money::from_raw(MONEY_RAW_MAX, usd)),
978 );
979 }
980
981 #[rstest]
982 fn test_money_checked_sub_at_exact_min_returns_some() {
983 let usd = Currency::USD();
984 let near_min = Money::from_raw(MONEY_RAW_MIN + 1, usd);
985 let one_unit = Money::from_raw(1, usd);
986 assert_eq!(
987 near_min.checked_sub(one_unit),
988 Some(Money::from_raw(MONEY_RAW_MIN, usd)),
989 );
990 }
991
992 #[rstest]
993 fn test_money_negation() {
994 let money = Money::new(100.0, Currency::USD());
995 let result = -money;
996 assert_eq!(result, Money::from("-100.0 USD"));
997 assert_eq!(result.currency, Currency::USD().clone());
998 }
999
1000 #[rstest]
1001 fn test_money_addition_decimal() {
1002 let money = Money::new(100.0, Currency::USD());
1003 let result = money + dec!(50.25);
1004 assert_eq!(result, dec!(150.25));
1005 }
1006
1007 #[rstest]
1008 fn test_money_subtraction_decimal() {
1009 let money = Money::new(100.0, Currency::USD());
1010 let result = money - dec!(30.50);
1011 assert_eq!(result, dec!(69.50));
1012 }
1013
1014 #[rstest]
1015 fn test_money_multiplication_decimal() {
1016 let money = Money::new(100.0, Currency::USD());
1017 let result = money * dec!(1.5);
1018 assert_eq!(result, dec!(150.00));
1019 }
1020
1021 #[rstest]
1022 fn test_money_division_decimal() {
1023 let money = Money::new(100.0, Currency::USD());
1024 let result = money / dec!(4);
1025 assert_eq!(result, dec!(25.00));
1026 }
1027
1028 #[rstest]
1029 fn test_money_addition_f64() {
1030 let money = Money::new(100.0, Currency::USD());
1031 let result = money + 50.25;
1032 assert!(approx_eq!(f64, result, 150.25, epsilon = 1e-9));
1033 }
1034
1035 #[rstest]
1036 fn test_money_subtraction_f64() {
1037 let money = Money::new(100.0, Currency::USD());
1038 let result = money - 30.50;
1039 assert!(approx_eq!(f64, result, 69.50, epsilon = 1e-9));
1040 }
1041
1042 #[rstest]
1043 fn test_money_multiplication_f64() {
1044 let money = Money::new(100.0, Currency::USD());
1045 let result = money * 1.5;
1046 assert!(approx_eq!(f64, result, 150.0, epsilon = 1e-9));
1047 }
1048
1049 #[rstest]
1050 fn test_money_division_f64() {
1051 let money = Money::new(100.0, Currency::USD());
1052 let result = money / 4.0;
1053 assert!(approx_eq!(f64, result, 25.0, epsilon = 1e-9));
1054 }
1055
1056 #[rstest]
1057 fn test_money_new_usd() {
1058 let money = Money::new(1000.0, Currency::USD());
1059 assert_eq!(money.currency.code.as_str(), "USD");
1060 assert_eq!(money.currency.precision, 2);
1061 assert_eq!(money.to_string(), "1000.00 USD");
1062 assert_eq!(money.to_formatted_string(), "1_000.00 USD");
1063 assert_eq!(money.as_decimal(), dec!(1000.00));
1064 assert!(approx_eq!(f64, money.as_f64(), 1000.0, epsilon = 0.001));
1065 }
1066
1067 #[rstest]
1068 fn test_money_new_btc() {
1069 let money = Money::new(10.3, Currency::BTC());
1070 assert_eq!(money.currency.code.as_str(), "BTC");
1071 assert_eq!(money.currency.precision, 8);
1072 assert_eq!(money.to_string(), "10.30000000 BTC");
1073 assert_eq!(money.to_formatted_string(), "10.30000000 BTC");
1074 }
1075
1076 #[rstest]
1077 #[case("0USD")] #[case("0x00 USD")] #[case("0 US")] #[case("0 USD USD")] #[should_panic(expected = "Condition failed")]
1082 fn test_from_str_invalid_input(#[case] input: &str) {
1083 let _ = Money::from(input);
1084 }
1085
1086 #[rstest]
1087 #[case("0 USD", Currency::USD(), dec!(0.00))]
1088 #[case("1.1 AUD", Currency::AUD(), dec!(1.10))]
1089 #[case("1.12345678 BTC", Currency::BTC(), dec!(1.12345678))]
1090 #[case("10_000.10 USD", Currency::USD(), dec!(10000.10))]
1091 fn test_from_str_valid_input(
1092 #[case] input: &str,
1093 #[case] expected_currency: Currency,
1094 #[case] expected_dec: Decimal,
1095 ) {
1096 let money = Money::from(input);
1097 assert_eq!(money.currency, expected_currency);
1098 assert_eq!(money.as_decimal(), expected_dec);
1099 }
1100
1101 #[rstest]
1102 fn test_money_from_str_negative() {
1103 let money = Money::from("-123.45 USD");
1104 assert!(approx_eq!(f64, money.as_f64(), -123.45, epsilon = 1e-9));
1105 assert_eq!(money.currency, Currency::USD());
1106 }
1107
1108 #[rstest]
1109 #[case("1e7 USD", 10_000_000.0)]
1110 #[case("2.5e3 EUR", 2_500.0)]
1111 #[case("1.234e-2 GBP", 0.01)] #[case("5E-3 JPY", 0.0)] fn test_from_str_scientific_notation(#[case] input: &str, #[case] expected_value: f64) {
1114 let money = Money::from_str(input).unwrap();
1115 assert!(approx_eq!(
1116 f64,
1117 money.as_f64(),
1118 expected_value,
1119 epsilon = 1e-10
1120 ));
1121 }
1122
1123 #[rstest]
1124 #[case("1_234.56 USD", 1234.56)]
1125 #[case("1_000_000 EUR", 1_000_000.0)]
1126 #[case("99_999.99 GBP", 99_999.99)]
1127 fn test_from_str_with_underscores(#[case] input: &str, #[case] expected_value: f64) {
1128 let money = Money::from_str(input).unwrap();
1129 assert!(approx_eq!(
1130 f64,
1131 money.as_f64(),
1132 expected_value,
1133 epsilon = 1e-10
1134 ));
1135 }
1136
1137 #[rstest]
1138 fn test_from_decimal_precision_preservation() {
1139 use rust_decimal::Decimal;
1140
1141 let decimal = Decimal::from_str("123.45").unwrap();
1142 let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
1143 assert_eq!(money.currency.precision, 2);
1144 assert!(approx_eq!(f64, money.as_f64(), 123.45, epsilon = 1e-10));
1145
1146 let expected_raw = 12345 * 10_i64.pow(u32::from(FIXED_PRECISION - 2));
1148 assert_eq!(money.raw, MoneyRaw::from(expected_raw));
1149 }
1150
1151 #[rstest]
1152 fn test_from_decimal_rounding() {
1153 use rust_decimal::Decimal;
1154
1155 let decimal = Decimal::from_str("1.005").unwrap();
1157 let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
1158 assert_eq!(money.as_f64(), 1.0); let decimal = Decimal::from_str("1.015").unwrap();
1161 let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
1162 assert_eq!(money.as_f64(), 1.02); }
1164
1165 #[rstest]
1166 fn test_money_hash() {
1167 use std::{
1168 collections::hash_map::DefaultHasher,
1169 hash::{Hash, Hasher},
1170 };
1171
1172 let m1 = Money::new(100.0, Currency::USD());
1173 let m2 = Money::new(100.0, Currency::USD());
1174 let m3 = Money::new(100.0, Currency::AUD());
1175
1176 let mut s1 = DefaultHasher::new();
1177 let mut s2 = DefaultHasher::new();
1178 let mut s3 = DefaultHasher::new();
1179
1180 m1.hash(&mut s1);
1181 m2.hash(&mut s2);
1182 m3.hash(&mut s3);
1183
1184 assert_eq!(
1185 s1.finish(),
1186 s2.finish(),
1187 "Same amount + same currency => same hash"
1188 );
1189 assert_ne!(
1190 s1.finish(),
1191 s3.finish(),
1192 "Same amount + different currency => different hash"
1193 );
1194 }
1195
1196 #[rstest]
1197 fn test_money_serialization_deserialization() {
1198 let money = Money::new(123.45, Currency::USD());
1199 let serialized = serde_json::to_string(&money);
1200 let deserialized: Money = serde_json::from_str(&serialized.unwrap()).unwrap();
1201 assert_eq!(money, deserialized);
1202 }
1203
1204 #[rstest]
1205 #[should_panic(expected = "`raw` value")]
1206 fn test_money_from_raw_out_of_range_panics() {
1207 let usd = Currency::USD();
1208 let raw = MONEY_RAW_MAX.saturating_add(1);
1209 let _ = Money::from_raw(raw, usd);
1210 }
1211
1212 #[rstest]
1213 fn test_money_from_raw_checked_valid() {
1214 let usd = Currency::USD();
1215 let money = Money::from_raw_checked(123_450_000_000, usd).unwrap();
1216 assert_eq!(money.currency, usd);
1217 }
1218
1219 #[rstest]
1220 fn test_money_from_raw_checked_above_max_returns_error() {
1221 let usd = Currency::USD();
1222 let raw = MONEY_RAW_MAX.saturating_add(1);
1223 let error = Money::from_raw_checked(raw, usd).unwrap_err();
1224 assert!(matches!(error, CorrectnessError::PredicateViolation { .. }));
1225 }
1226
1227 #[rstest]
1228 fn test_money_from_raw_checked_below_min_returns_error() {
1229 let usd = Currency::USD();
1230 let raw = MONEY_RAW_MIN.saturating_sub(1);
1231 let error = Money::from_raw_checked(raw, usd).unwrap_err();
1232 assert!(matches!(error, CorrectnessError::PredicateViolation { .. }));
1233 }
1234
1235 #[rstest]
1236 fn test_from_decimal_rejects_out_of_range() {
1237 let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1238 let result = Money::from_decimal(huge, Currency::USD());
1239 assert!(result.is_err());
1240 }
1241
1242 #[rstest]
1243 fn test_from_decimal_out_of_range_returns_typed_error_with_stable_display() {
1244 let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1245 let error = Money::from_decimal(huge, Currency::USD()).unwrap_err();
1246 match error {
1247 CorrectnessError::PredicateViolation { ref message } => {
1248 assert!(
1249 message.contains("MoneyRaw range") || message.contains("Money"),
1250 "unexpected message: {message:?}",
1251 );
1252 }
1253 _ => panic!("expected PredicateViolation, was {error:?}"),
1254 }
1255 }
1256
1257 #[rstest]
1258 fn test_from_mantissa_exponent_exact_precision() {
1259 let money = Money::from_mantissa_exponent(12345, -2, Currency::USD());
1260 assert_eq!(money.as_f64(), 123.45);
1261 }
1262
1263 #[rstest]
1264 fn test_from_mantissa_exponent_excess_rounds_down() {
1265 let money = Money::from_mantissa_exponent(12345, -3, Currency::USD());
1267 assert_eq!(money.as_f64(), 12.34);
1268 }
1269
1270 #[rstest]
1271 fn test_from_mantissa_exponent_excess_rounds_up() {
1272 let money = Money::from_mantissa_exponent(12355, -3, Currency::USD());
1274 assert_eq!(money.as_f64(), 12.36);
1275 }
1276
1277 #[rstest]
1278 fn test_from_mantissa_exponent_positive_exponent() {
1279 let money = Money::from_mantissa_exponent(5, 2, Currency::USD());
1280 assert_eq!(money.as_f64(), 500.0);
1281 }
1282
1283 #[rstest]
1284 #[should_panic(expected = "Overflow")]
1285 fn test_from_mantissa_exponent_overflow_panics() {
1286 let _ = Money::from_mantissa_exponent(i64::MAX, 9, Currency::USD());
1287 }
1288
1289 #[rstest]
1290 #[should_panic(expected = "exceeds i128 range")]
1291 fn test_from_mantissa_exponent_large_exponent_panics() {
1292 let _ = Money::from_mantissa_exponent(1, 119, Currency::USD());
1293 }
1294
1295 #[rstest]
1296 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1297 let money = Money::from_mantissa_exponent(0, 119, Currency::USD());
1298 assert_eq!(money.as_f64(), 0.0);
1299 }
1300
1301 #[rstest]
1302 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1303 let money = Money::from_mantissa_exponent(12345, -120, Currency::USD());
1305 assert_eq!(money.as_f64(), 0.0);
1306 }
1307
1308 #[rstest]
1309 #[case(42.0, true, "positive value")]
1310 #[case(0.0, false, "zero value")]
1311 #[case( -13.5, false, "negative value")]
1312 #[allow(clippy::used_underscore_binding)]
1313 fn test_check_positive_money(
1314 #[case] amount: f64,
1315 #[case] should_succeed: bool,
1316 #[case] _case_name: &str,
1317 ) {
1318 let money = Money::new(amount, Currency::USD());
1319
1320 let res = check_positive_money(money, "money");
1321
1322 if should_succeed {
1323 assert!(res.is_ok(), "expected Ok(..) for {amount}");
1324 } else {
1325 assert!(res.is_err(), "expected Err(..) for {amount}");
1326 let msg = res.unwrap_err().to_string();
1327 assert!(
1328 msg.contains("not positive"),
1329 "error message should mention positivity; got: {msg:?}"
1330 );
1331 }
1332 }
1333
1334 #[rstest]
1335 fn test_check_positive_money_returns_typed_error_with_stable_display() {
1336 let error = check_positive_money(Money::new(0.0, Currency::USD()), "money").unwrap_err();
1337
1338 assert_eq!(
1339 error,
1340 CorrectnessError::NotPositive {
1341 param: "money".to_string(),
1342 value: "0.00 USD".to_string(),
1343 type_name: "`Money`",
1344 }
1345 );
1346 assert_eq!(
1347 error.to_string(),
1348 "invalid `Money` for 'money' not positive, was 0.00 USD"
1349 );
1350 }
1351}
1352
1353#[cfg(test)]
1354mod property_tests {
1355 use proptest::prelude::*;
1356 use rstest::rstest;
1357
1358 use super::*;
1359
1360 fn currency_strategy() -> impl Strategy<Value = Currency> {
1361 prop_oneof![
1362 Just(Currency::USD()),
1363 Just(Currency::EUR()),
1364 Just(Currency::GBP()),
1365 Just(Currency::JPY()),
1366 Just(Currency::AUD()),
1367 Just(Currency::CAD()),
1368 Just(Currency::CHF()),
1369 Just(Currency::BTC()),
1370 Just(Currency::ETH()),
1371 Just(Currency::USDT()),
1372 ]
1373 }
1374
1375 fn money_amount_strategy() -> impl Strategy<Value = f64> {
1376 prop_oneof![
1377 -1000.0..1000.0,
1378 -100_000.0..100_000.0,
1379 -1_000_000.0..1_000_000.0,
1380 Just(0.0),
1381 Just(MONEY_MIN / 2.0),
1382 Just(MONEY_MAX / 2.0),
1383 Just(MONEY_MIN + 1.0),
1384 Just(MONEY_MAX - 1.0),
1385 Just(MONEY_MIN),
1386 Just(MONEY_MAX),
1387 ]
1388 }
1389
1390 fn money_strategy() -> impl Strategy<Value = Money> {
1391 (money_amount_strategy(), currency_strategy())
1392 .prop_filter_map("constructible money", |(amount, currency)| {
1393 Money::new_checked(amount, currency).ok()
1394 })
1395 }
1396
1397 proptest! {
1398 #[rstest]
1399 fn prop_money_construction_roundtrip(
1400 amount in money_amount_strategy(),
1401 currency in currency_strategy()
1402 ) {
1403 if let Ok(money) = Money::new_checked(amount, currency) {
1404 let roundtrip = money.as_f64();
1405 let precision_epsilon = if currency.precision == 0 {
1406 1.0
1407 } else {
1408 let currency_epsilon = 10.0_f64.powi(-i32::from(currency.precision));
1409 let magnitude_epsilon = amount.abs() * 1e-10;
1410 currency_epsilon.max(magnitude_epsilon)
1411 };
1412 prop_assert!((roundtrip - amount).abs() <= precision_epsilon,
1413 "Roundtrip failed: {} -> {} -> {} (precision: {}, epsilon: {})",
1414 amount, money.raw, roundtrip, currency.precision, precision_epsilon);
1415 prop_assert_eq!(money.currency, currency);
1416 }
1417 }
1418
1419 #[rstest]
1420 fn prop_money_addition_commutative(
1421 money1 in money_strategy(),
1422 money2 in money_strategy(),
1423 ) {
1424 if money1.currency == money2.currency
1425 && let (Some(_), Some(_)) = (
1426 money1.raw.checked_add(money2.raw),
1427 money2.raw.checked_add(money1.raw)
1428 )
1429 {
1430 let sum1 = money1 + money2;
1431 let sum2 = money2 + money1;
1432 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1433 prop_assert_eq!(sum1.currency, money1.currency);
1434 }
1435 }
1436
1437 #[rstest]
1438 fn prop_money_addition_associative(
1439 money1 in money_strategy(),
1440 money2 in money_strategy(),
1441 money3 in money_strategy(),
1442 ) {
1443 if money1.currency == money2.currency
1444 && money2.currency == money3.currency
1445 && let (Some(sum1), Some(sum2)) = (
1446 money1.raw.checked_add(money2.raw),
1447 money2.raw.checked_add(money3.raw)
1448 )
1449 && let (Some(left), Some(right)) = (
1450 sum1.checked_add(money3.raw),
1451 money1.raw.checked_add(sum2)
1452 )
1453 && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&left)
1454 && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&right)
1455 {
1456 let left_result = Money::from_raw(left, money1.currency);
1457 let right_result = Money::from_raw(right, money1.currency);
1458 prop_assert_eq!(left_result, right_result, "Addition should be associative");
1459 }
1460 }
1461
1462 #[rstest]
1463 fn prop_money_subtraction_inverse(
1464 money1 in money_strategy(),
1465 money2 in money_strategy(),
1466 ) {
1467 if money1.currency == money2.currency
1468 && let Some(sum_raw) = money1.raw.checked_add(money2.raw)
1469 && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&sum_raw)
1470 {
1471 let sum = Money::from_raw(sum_raw, money1.currency);
1472 let diff = sum - money2;
1473 prop_assert_eq!(diff, money1, "Subtraction should be inverse of addition");
1474 }
1475 }
1476
1477 #[rstest]
1480 fn prop_money_checked_add_matches_spec(
1481 raw1 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
1482 raw2 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
1483 currency in currency_strategy(),
1484 ) {
1485 let m1 = Money::from_raw(raw1, currency);
1486 let m2 = Money::from_raw(raw2, currency);
1487 let expected = m1.raw
1488 .checked_add(m2.raw)
1489 .filter(|r| (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(r))
1490 .map(|raw| Money { raw, currency });
1491 prop_assert_eq!(m1.checked_add(m2), expected);
1492 }
1493
1494 #[rstest]
1497 fn prop_money_checked_sub_matches_spec(
1498 raw1 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
1499 raw2 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
1500 currency in currency_strategy(),
1501 ) {
1502 let m1 = Money::from_raw(raw1, currency);
1503 let m2 = Money::from_raw(raw2, currency);
1504 let expected = m1.raw
1505 .checked_sub(m2.raw)
1506 .filter(|r| (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(r))
1507 .map(|raw| Money { raw, currency });
1508 prop_assert_eq!(m1.checked_sub(m2), expected);
1509 }
1510
1511 #[rstest]
1512 fn prop_money_zero_identity(money in money_strategy()) {
1513 let zero = Money::zero(money.currency);
1514 prop_assert_eq!(money + zero, money, "Zero should be additive identity");
1515 prop_assert_eq!(zero + money, money, "Zero should be additive identity (commutative)");
1516 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1517 }
1518
1519 #[rstest]
1520 fn prop_money_negation_inverse(money in money_strategy()) {
1521 let negated = -money;
1522 let double_neg = -negated;
1523 prop_assert_eq!(money, double_neg, "Double negation should equal original");
1524 prop_assert_eq!(negated.currency, money.currency, "Negation preserves currency");
1525
1526 if let Some(sum_raw) = money.raw.checked_add(negated.raw)
1527 && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&sum_raw) {
1528 let sum = Money::from_raw(sum_raw, money.currency);
1529 prop_assert!(sum.is_zero(), "Money + (-Money) should equal zero");
1530 }
1531 }
1532
1533 #[rstest]
1534 fn prop_money_comparison_consistency(
1535 money1 in money_strategy(),
1536 money2 in money_strategy(),
1537 ) {
1538 if money1.currency == money2.currency {
1539 let eq = money1 == money2;
1540 let lt = money1 < money2;
1541 let gt = money1 > money2;
1542 let le = money1 <= money2;
1543 let ge = money1 >= money2;
1544
1545 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1546 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1547
1548 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1549 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1550 prop_assert_eq!(lt, money2 > money1, "< should be symmetric with >");
1551 prop_assert_eq!(le, money2 >= money1, "<= should be symmetric with >=");
1552 }
1553 }
1554
1555 #[rstest]
1556 fn prop_money_decimal_conversion(money in money_strategy()) {
1557 let decimal = money.as_decimal();
1558
1559 prop_assert_eq!(decimal.scale(), u32::from(money.currency.precision));
1561
1562 #[cfg(feature = "defi")]
1563 {
1564 let decimal_f64: f64 = decimal.try_into().unwrap_or(0.0);
1565 prop_assert!(decimal_f64.is_finite(), "Decimal should convert to finite f64");
1566 }
1567 #[cfg(not(feature = "defi"))]
1568 {
1569 let decimal_f64: f64 = decimal.try_into().unwrap_or(0.0);
1570 let original_f64 = money.as_f64();
1571
1572 let base_epsilon = 10.0_f64.powi(-(money.currency.precision as i32));
1573 let precision_epsilon = if cfg!(feature = "high-precision") {
1574 base_epsilon.max(1e-10)
1575 } else {
1576 base_epsilon
1577 };
1578 let diff = (decimal_f64 - original_f64).abs();
1579 prop_assert!(diff <= precision_epsilon,
1580 "Decimal conversion should preserve value within currency precision: {} vs {} (diff: {}, epsilon: {})",
1581 original_f64, decimal_f64, diff, precision_epsilon);
1582 }
1583 }
1584
1585 #[rstest]
1586 fn prop_money_arithmetic_with_f64(
1587 money in money_strategy(),
1588 factor in -1000.0..1000.0_f64,
1589 ) {
1590 if factor != 0.0 {
1591 let original_f64 = money.as_f64();
1592
1593 let mul_result = money * factor;
1594 let expected_mul = original_f64 * factor;
1595 prop_assert!((mul_result - expected_mul).abs() < 0.01,
1596 "Multiplication with f64 should be accurate");
1597
1598 let div_result = money / factor;
1599 let expected_div = original_f64 / factor;
1600 if expected_div.is_finite() {
1601 prop_assert!((div_result - expected_div).abs() < 0.01,
1602 "Division with f64 should be accurate");
1603 }
1604
1605 let add_result = money + factor;
1606 let expected_add = original_f64 + factor;
1607 prop_assert!((add_result - expected_add).abs() < 0.01,
1608 "Addition with f64 should be accurate");
1609
1610 let sub_result = money - factor;
1611 let expected_sub = original_f64 - factor;
1612 prop_assert!((sub_result - expected_sub).abs() < 0.01,
1613 "Subtraction with f64 should be accurate");
1614 }
1615 }
1616 }
1617}