lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

money.rs (4493B)


      1 mod common;
      2 
      3 use radroots_core::{Currency, Decimal, Money, money::Error};
      4 
      5 #[test]
      6 fn zero_and_checked_constructor_preserve_invariants() {
      7     let zero = Money::zero(Currency::USD);
      8     assert!(zero.is_zero());
      9     assert_eq!(zero.amount(), Decimal::ZERO);
     10     assert_eq!(zero.currency(), Currency::USD);
     11 
     12     assert_eq!(
     13         Money::try_new(common::dec("-0.01"), Currency::USD),
     14         Err(Error::NegativeAmount)
     15     );
     16     let signed_zero = Money::try_new(common::dec("-0.00"), Currency::USD).unwrap();
     17     assert_eq!(signed_zero.amount(), Decimal::ZERO);
     18 }
     19 
     20 #[test]
     21 fn quantization_is_deterministic_midpoint_away_from_zero() {
     22     let a = common::money("1.234", "USD").quantize_to_currency();
     23     let b = common::money("1.235", "USD").quantize_to_currency();
     24     assert_eq!(a.amount(), common::dec("1.23"));
     25     assert_eq!(b.amount(), common::dec("1.24"));
     26 }
     27 
     28 #[test]
     29 fn checked_arithmetic_reports_domain_failures() {
     30     let one = common::money("1.00", "USD");
     31     let two = common::money("2.00", "USD");
     32     let euros = common::money("3.00", "EUR");
     33 
     34     assert_eq!(one.checked_add(&two).unwrap().amount(), common::dec("3"));
     35     assert_eq!(one.checked_add(&euros), Err(Error::CurrencyMismatch));
     36     assert_eq!(two.checked_sub(&one).unwrap().amount(), common::dec("1"));
     37     assert_eq!(one.checked_sub(&two), Err(Error::NegativeAmount));
     38     assert_eq!(
     39         one.checked_mul_decimal(common::dec("-1")),
     40         Err(Error::NegativeAmount)
     41     );
     42     assert_eq!(
     43         one.checked_div_decimal(Decimal::ZERO),
     44         Err(Error::DivisionByZero)
     45     );
     46 
     47     let max = Money::try_new(Decimal::MAX, Currency::USD).unwrap();
     48     assert_eq!(max.checked_add(&one), Err(Error::ArithmeticOverflow));
     49 }
     50 
     51 #[test]
     52 fn minor_units_exact_and_rounded_cover_currency_exponents() {
     53     let exact = common::money("1.23", "USD");
     54     let fractional = common::money("1.234", "USD");
     55     let rounded = common::money("1.235", "USD");
     56     assert_eq!(exact.to_minor_units_u64_exact(), Ok(123));
     57     assert_eq!(
     58         fractional.to_minor_units_u64_exact(),
     59         Err(Error::NotWholeMinorUnits)
     60     );
     61     assert_eq!(rounded.to_minor_units_u64_rounded(), Ok(124));
     62 
     63     assert_eq!(
     64         common::money("123", "JPY").to_minor_units_u64_exact(),
     65         Ok(123)
     66     );
     67     assert_eq!(
     68         common::money("1.234", "KWD").to_minor_units_u64_exact(),
     69         Ok(1234)
     70     );
     71 }
     72 
     73 #[test]
     74 fn minor_unit_constructors_roundtrip_and_detect_u32_overflow() {
     75     for currency in [Currency::JPY, Currency::USD, common::currency("KWD")] {
     76         for minor in [0, 1, 99, 12_345, u32::MAX as u64] {
     77             let money = Money::from_minor_units_u64(minor, currency);
     78             assert_eq!(money.to_minor_units_u64_exact(), Ok(minor));
     79         }
     80     }
     81 
     82     let from_u32 = Money::from_minor_units_u32(505, Currency::USD);
     83     assert_eq!(from_u32.amount(), common::dec("5.05"));
     84     assert_eq!(from_u32.to_minor_units_u32_rounded(), Ok(505));
     85 
     86     let too_large = Money::from_minor_units_u64(u64::from(u32::MAX) + 1, Currency::USD);
     87     assert_eq!(
     88         too_large.to_minor_units_u32_exact(),
     89         Err(Error::AmountOverflow)
     90     );
     91     assert_eq!(
     92         too_large.to_minor_units_u32_rounded(),
     93         Err(Error::AmountOverflow)
     94     );
     95 }
     96 
     97 #[test]
     98 fn exact_scale_change_never_rounds() {
     99     let money = common::money("1.2300", "USD");
    100     assert_eq!(money.try_with_scale_exact(2).unwrap().amount().scale(), 2);
    101     assert_eq!(
    102         common::money("1.235", "USD").try_with_scale_exact(2),
    103         Err(Error::PrecisionLoss)
    104     );
    105 }
    106 
    107 #[test]
    108 fn display_and_error_messages_are_stable() {
    109     assert_eq!(common::money("10", "USD").to_string(), "10 USD");
    110     assert_eq!(
    111         Error::NegativeAmount.to_string(),
    112         "money amount must be ≥ 0"
    113     );
    114     assert_eq!(
    115         Error::NotWholeMinorUnits.to_string(),
    116         "money not a whole number of minor units"
    117     );
    118     assert_eq!(
    119         Error::AmountOverflow.to_string(),
    120         "money minor-unit conversion overflow"
    121     );
    122     assert_eq!(
    123         Error::CurrencyMismatch.to_string(),
    124         "money currency mismatch"
    125     );
    126     assert_eq!(
    127         Error::ArithmeticOverflow.to_string(),
    128         "money arithmetic overflow"
    129     );
    130     assert_eq!(Error::DivisionByZero.to_string(), "money division by zero");
    131     assert_eq!(
    132         Error::ScaleOutOfRange.to_string(),
    133         "money scale is outside the supported range"
    134     );
    135     assert_eq!(
    136         Error::PrecisionLoss.to_string(),
    137         "money operation would lose precision"
    138     );
    139 }