lib

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

quantity.rs (4486B)


      1 mod common;
      2 
      3 use radroots_core::{Decimal, Quantity, Unit, quantity::Error};
      4 
      5 #[test]
      6 fn zero_and_checked_constructor_preserve_invariants() {
      7     let zero = Quantity::zero(Unit::MassKg);
      8     assert!(zero.is_zero());
      9     assert_eq!(zero.amount(), Decimal::ZERO);
     10     assert_eq!(zero.unit(), Unit::MassKg);
     11     assert_eq!(zero.canonical_unit(), Unit::MassG);
     12     assert!(!zero.is_canonical());
     13 
     14     assert_eq!(
     15         Quantity::try_new(common::dec("-1"), Unit::Each),
     16         Err(Error::NegativeAmount)
     17     );
     18 }
     19 
     20 #[test]
     21 fn label_helpers_set_replace_and_clear() {
     22     let quantity = common::qty("1", Unit::Each).with_label("box");
     23     assert_eq!(quantity.label(), Some("box"));
     24     let quantity = quantity.with_optional_label(Some("case"));
     25     assert_eq!(quantity.label(), Some("case"));
     26     let quantity = quantity.with_optional_label::<&str>(None);
     27     assert_eq!(quantity.label(), None);
     28     assert_eq!(quantity.with_label("bag").clear_label().label(), None);
     29 }
     30 
     31 #[test]
     32 fn checked_addition_and_subtraction_require_matching_units() {
     33     let first = common::qty("5", Unit::Each).with_label("lhs");
     34     let second = common::qty("2", Unit::Each);
     35     let kilograms = common::qty("1", Unit::MassKg);
     36 
     37     let sum = first.try_add(&second).unwrap();
     38     assert_eq!(sum.amount(), common::dec("7"));
     39     assert_eq!(sum.label(), Some("lhs"));
     40     let difference = first.try_sub(&second).unwrap();
     41     assert_eq!(difference.amount(), common::dec("3"));
     42     assert_eq!(difference.label(), Some("lhs"));
     43 
     44     assert_eq!(first.try_add(&kilograms), Err(Error::UnitMismatch));
     45     assert_eq!(first.try_sub(&kilograms), Err(Error::UnitMismatch));
     46     assert!(first.checked_add(&kilograms).is_none());
     47     assert!(first.checked_sub(&kilograms).is_none());
     48     assert_eq!(second.try_sub(&first), Err(Error::NegativeAmount));
     49 }
     50 
     51 #[test]
     52 fn checked_scaling_reports_negative_overflow_and_zero_division() {
     53     let quantity = common::qty("2", Unit::Each).with_label("unit");
     54     let scaled = quantity.checked_mul_decimal(common::dec("2.5")).unwrap();
     55     assert_eq!(scaled.amount(), common::dec("5"));
     56     assert_eq!(scaled.unit(), Unit::Each);
     57     assert_eq!(scaled.label(), Some("unit"));
     58 
     59     let divided = quantity.checked_div_decimal(common::dec("2")).unwrap();
     60     assert_eq!(divided.amount(), common::dec("1"));
     61     assert_eq!(
     62         quantity.checked_mul_decimal(common::dec("-1")),
     63         Err(Error::NegativeAmount)
     64     );
     65     assert_eq!(
     66         quantity.checked_div_decimal(Decimal::ZERO),
     67         Err(Error::DivisionByZero)
     68     );
     69 
     70     let max = Quantity::try_new(Decimal::MAX, Unit::Each).unwrap();
     71     assert_eq!(max.try_add(&quantity), Err(Error::ArithmeticOverflow));
     72 }
     73 
     74 #[test]
     75 fn conversion_is_checked_and_preserves_labels() {
     76     let kilograms = common::qty("1", Unit::MassKg).with_label("bag");
     77     let grams = kilograms.try_convert_to(Unit::MassG).unwrap();
     78     assert_eq!(grams.amount(), common::dec("1000"));
     79     assert_eq!(grams.unit(), Unit::MassG);
     80     assert_eq!(grams.label(), Some("bag"));
     81     assert!(grams.is_canonical());
     82 
     83     let canonical = common::qty("2", Unit::VolumeL).to_canonical().unwrap();
     84     assert_eq!(canonical.unit(), Unit::VolumeMl);
     85     assert_eq!(canonical.amount(), common::dec("2000"));
     86 
     87     assert_eq!(
     88         common::qty("1", Unit::Each).try_convert_to(Unit::MassG),
     89         Err(radroots_core::unit::ConvertError::NotConvertibleUnits {
     90             from: Unit::Each,
     91             to: Unit::MassG,
     92         })
     93     );
     94 }
     95 
     96 #[test]
     97 fn display_and_error_messages_are_stable() {
     98     assert_eq!(
     99         common::qty("1.5", Unit::Each).with_label("bag").to_string(),
    100         "1.5 each (bag)"
    101     );
    102     assert_eq!(common::qty("1.5", Unit::Each).to_string(), "1.5 each");
    103     assert_eq!(
    104         Error::NegativeAmount.to_string(),
    105         "quantity amount must be ≥ 0"
    106     );
    107     assert_eq!(Error::UnitMismatch.to_string(), "quantity unit mismatch");
    108     assert_eq!(
    109         Error::ArithmeticOverflow.to_string(),
    110         "quantity arithmetic overflow"
    111     );
    112     assert_eq!(
    113         Error::DivisionByZero.to_string(),
    114         "quantity division by zero"
    115     );
    116 }
    117 
    118 #[test]
    119 fn display_propagates_formatter_errors() {
    120     use core::fmt::{self, Write};
    121 
    122     struct FailWriter;
    123     impl Write for FailWriter {
    124         fn write_str(&mut self, _: &str) -> fmt::Result {
    125             Err(fmt::Error)
    126         }
    127     }
    128 
    129     let quantity = common::qty("1.5", Unit::Each).with_label("bag");
    130     assert!(fmt::write(&mut FailWriter, format_args!("{quantity}")).is_err());
    131 }