lib

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

quantity.rs (6954B)


      1 mod common;
      2 
      3 use radroots_core::{RadrootsCoreQuantityInvariantError, RadrootsCoreUnit};
      4 
      5 #[test]
      6 fn zero_helpers_and_scale_paths_are_exercised() {
      7     let zero = radroots_core::RadrootsCoreQuantity::zero(RadrootsCoreUnit::MassKg);
      8     assert!(zero.is_zero());
      9     assert_eq!(zero.canonical_unit(), RadrootsCoreUnit::MassG);
     10     assert!(!zero.is_canonical());
     11 
     12     let scaled = common::qty("1.2300", RadrootsCoreUnit::Each).with_scale(1);
     13     assert_eq!(scaled.amount, common::dec("1.2"));
     14 }
     15 
     16 #[test]
     17 fn label_helpers_set_and_clear() {
     18     let q = common::qty("1", RadrootsCoreUnit::Each).with_label("box");
     19     assert_eq!(q.label.as_deref(), Some("box"));
     20 
     21     let q = q.clear_label();
     22     assert!(q.label.is_none());
     23 
     24     let q = common::qty("1", RadrootsCoreUnit::Each).with_optional_label(Some("case"));
     25     assert_eq!(q.label.as_deref(), Some("case"));
     26 
     27     let q = q.with_optional_label::<&str>(None);
     28     assert!(q.label.is_none());
     29 }
     30 
     31 #[test]
     32 fn ensure_non_negative_rejects_negative_amount() {
     33     let q = common::qty("-1", RadrootsCoreUnit::Each);
     34     assert_eq!(
     35         q.ensure_non_negative(),
     36         Err(RadrootsCoreQuantityInvariantError::NegativeAmount)
     37     );
     38 }
     39 
     40 #[test]
     41 fn ensure_non_negative_accepts_non_negative_amount() {
     42     let q = common::qty("0", RadrootsCoreUnit::Each);
     43     assert_eq!(q.ensure_non_negative(), Ok(()));
     44 }
     45 
     46 #[test]
     47 fn try_add_and_try_sub_require_matching_units() {
     48     let a = common::qty("1", RadrootsCoreUnit::Each).with_label("lhs");
     49     let b = common::qty("2", RadrootsCoreUnit::Each);
     50     let c = common::qty("1", RadrootsCoreUnit::MassKg);
     51 
     52     let sum = a.try_add(&b).unwrap();
     53     assert_eq!(sum.amount, common::dec("3"));
     54     assert_eq!(sum.label.as_deref(), Some("lhs"));
     55 
     56     assert_eq!(
     57         a.try_add(&c),
     58         Err(RadrootsCoreQuantityInvariantError::UnitMismatch)
     59     );
     60     assert_eq!(
     61         b.try_sub(&c),
     62         Err(RadrootsCoreQuantityInvariantError::UnitMismatch)
     63     );
     64 }
     65 
     66 #[test]
     67 fn try_sub_success_path_is_exercised() {
     68     let a = common::qty("4", RadrootsCoreUnit::Each).with_label("lhs");
     69     let b = common::qty("1", RadrootsCoreUnit::Each);
     70     let out = a.try_sub(&b).expect("sub result");
     71     assert_eq!(out.amount, common::dec("3"));
     72     assert_eq!(out.label.as_deref(), Some("lhs"));
     73 }
     74 
     75 #[test]
     76 fn checked_add_and_sub_return_none_on_mismatch() {
     77     let a = common::qty("1", RadrootsCoreUnit::Each);
     78     let b = common::qty("2", RadrootsCoreUnit::MassG);
     79     assert!(a.checked_add(&b).is_none());
     80     assert!(a.checked_sub(&b).is_none());
     81 }
     82 
     83 #[test]
     84 fn checked_add_and_sub_return_some_on_matching_units() {
     85     let a = common::qty("5", RadrootsCoreUnit::Each).with_label("lhs");
     86     let b = common::qty("2", RadrootsCoreUnit::Each);
     87     let added = a.checked_add(&b).expect("added quantity");
     88     assert_eq!(added.amount, common::dec("7"));
     89     assert_eq!(added.label.as_deref(), Some("lhs"));
     90 
     91     let subbed = a.checked_sub(&b).expect("subbed quantity");
     92     assert_eq!(subbed.amount, common::dec("3"));
     93     assert_eq!(subbed.label.as_deref(), Some("lhs"));
     94 }
     95 
     96 #[test]
     97 fn mul_and_div_preserve_unit_and_label() {
     98     let q = common::qty("2", RadrootsCoreUnit::Each).with_label("unit");
     99     let scaled = q.clone().mul_decimal(common::dec("2.5"));
    100     assert_eq!(scaled.amount, common::dec("5"));
    101     assert_eq!(scaled.unit, RadrootsCoreUnit::Each);
    102     assert_eq!(scaled.label.as_deref(), Some("unit"));
    103 
    104     let divided = q.div_decimal(common::dec("2"));
    105     assert_eq!(divided.amount, common::dec("1"));
    106     assert_eq!(divided.unit, RadrootsCoreUnit::Each);
    107     assert_eq!(divided.label.as_deref(), Some("unit"));
    108 }
    109 
    110 #[test]
    111 fn mul_and_div_operator_impls_are_exercised() {
    112     let qty = common::qty("4", RadrootsCoreUnit::Each).with_label("bag");
    113     let mul = qty.clone() * common::dec("1.5");
    114     assert_eq!(mul.amount, common::dec("6"));
    115     assert_eq!(mul.label.as_deref(), Some("bag"));
    116 
    117     let div = qty / common::dec("2");
    118     assert_eq!(div.amount, common::dec("2"));
    119     assert_eq!(div.label.as_deref(), Some("bag"));
    120 }
    121 
    122 #[test]
    123 fn display_includes_label_when_present() {
    124     let q = common::qty("1.5", RadrootsCoreUnit::Each).with_label("bag");
    125     assert_eq!(q.to_string(), "1.5 each (bag)");
    126 }
    127 
    128 #[test]
    129 fn display_without_label_and_error_display_are_exercised() {
    130     let q = common::qty("1.5", RadrootsCoreUnit::Each);
    131     assert_eq!(q.to_string(), "1.5 each");
    132 
    133     assert_eq!(
    134         RadrootsCoreQuantityInvariantError::NegativeAmount.to_string(),
    135         "quantity amount must be ≥ 0"
    136     );
    137     assert_eq!(
    138         RadrootsCoreQuantityInvariantError::UnitMismatch.to_string(),
    139         "quantity unit mismatch"
    140     );
    141 }
    142 
    143 #[test]
    144 fn display_propagates_formatter_errors() {
    145     use core::fmt::{self, Write};
    146 
    147     struct FailWriter {
    148         fail_on_paren: bool,
    149         fail_on_call: usize,
    150         calls: usize,
    151     }
    152 
    153     impl Write for FailWriter {
    154         fn write_str(&mut self, s: &str) -> fmt::Result {
    155             self.calls += 1;
    156             if self.fail_on_paren && s.contains('(') {
    157                 return Err(fmt::Error);
    158             }
    159             if self.calls == self.fail_on_call {
    160                 Err(fmt::Error)
    161             } else {
    162                 Ok(())
    163             }
    164         }
    165     }
    166 
    167     let with_label = common::qty("1.5", RadrootsCoreUnit::Each).with_label("bag");
    168     let mut first_write_fails = FailWriter {
    169         fail_on_paren: false,
    170         fail_on_call: 1,
    171         calls: 0,
    172     };
    173     assert!(fmt::write(&mut first_write_fails, format_args!("{with_label}")).is_err());
    174 
    175     let mut second_write_fails = FailWriter {
    176         fail_on_paren: true,
    177         fail_on_call: usize::MAX,
    178         calls: 0,
    179     };
    180     assert!(fmt::write(&mut second_write_fails, format_args!("{with_label}")).is_err());
    181 }
    182 
    183 #[test]
    184 fn try_convert_to_changes_unit_and_amount() {
    185     let q = common::qty("1", RadrootsCoreUnit::MassKg);
    186     let converted = q.try_convert_to(RadrootsCoreUnit::MassG).unwrap();
    187     assert_eq!(converted.amount, common::dec("1000"));
    188     assert_eq!(converted.unit, RadrootsCoreUnit::MassG);
    189 }
    190 
    191 #[test]
    192 fn to_canonical_converts_mass_and_volume() {
    193     let q = common::qty("2", RadrootsCoreUnit::VolumeL);
    194     let canonical = q.to_canonical().unwrap();
    195     assert_eq!(canonical.unit, RadrootsCoreUnit::VolumeMl);
    196     assert_eq!(canonical.amount, common::dec("2000"));
    197 }
    198 
    199 #[test]
    200 fn try_convert_to_rejects_mismatched_dimensions() {
    201     let q = common::qty("1", RadrootsCoreUnit::Each);
    202     let err = q.try_convert_to(RadrootsCoreUnit::MassG).unwrap_err();
    203     assert_eq!(
    204         err,
    205         radroots_core::RadrootsCoreUnitConvertError::NotConvertibleUnits {
    206             from: RadrootsCoreUnit::Each,
    207             to: RadrootsCoreUnit::MassG
    208         }
    209     );
    210 }
    211 
    212 #[test]
    213 fn try_convert_to_same_unit_returns_self_clone() {
    214     let q = common::qty("2", RadrootsCoreUnit::MassG).with_label("x");
    215     let converted = q
    216         .try_convert_to(RadrootsCoreUnit::MassG)
    217         .expect("same unit");
    218     assert_eq!(converted, q);
    219 }