lib

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

unit.rs (8837B)


      1 mod common;
      2 
      3 use core::str::FromStr;
      4 
      5 use radroots_core::{
      6     Decimal, Unit,
      7     unit::{
      8         ConvertError, ParseError, convert_mass_decimal, convert_unit_decimal,
      9         convert_volume_decimal, parse_mass_unit, parse_volume_unit,
     10     },
     11 };
     12 
     13 #[test]
     14 fn parses_units_and_synonyms() {
     15     use Unit::*;
     16     let cases = [
     17         ("each", Each),
     18         ("ea", Each),
     19         ("count", Each),
     20         ("kg", MassKg),
     21         ("kilograms", MassKg),
     22         ("g", MassG),
     23         ("grams", MassG),
     24         ("oz", MassOz),
     25         ("ounces", MassOz),
     26         ("lb", MassLb),
     27         ("pounds", MassLb),
     28         ("l", VolumeL),
     29         ("liters", VolumeL),
     30         ("ml", VolumeMl),
     31         ("milliliters", VolumeMl),
     32     ];
     33     for (input, expected) in cases {
     34         assert_eq!(Unit::from_str(input).unwrap(), expected);
     35     }
     36 }
     37 
     38 #[test]
     39 fn rejects_unknown_units() {
     40     assert_eq!(Unit::from_str("unknown"), Err(ParseError::UnknownUnit));
     41 }
     42 
     43 #[test]
     44 fn same_dimension_matches_mass_and_volume_groups() {
     45     use Unit::*;
     46     assert!(Unit::same_dimension(MassKg, MassG));
     47     assert!(Unit::same_dimension(VolumeL, VolumeMl));
     48     assert!(Unit::same_dimension(Each, Each));
     49     assert!(!Unit::same_dimension(MassKg, VolumeL));
     50     assert!(!Unit::same_dimension(Each, MassG));
     51 }
     52 
     53 #[test]
     54 fn canonical_unit_maps_dimensions() {
     55     use Unit::*;
     56     assert_eq!(Each.canonical_unit(), Each);
     57     assert_eq!(MassKg.canonical_unit(), MassG);
     58     assert_eq!(MassLb.canonical_unit(), MassG);
     59     assert_eq!(VolumeL.canonical_unit(), VolumeMl);
     60 }
     61 
     62 #[test]
     63 fn code_and_predicate_helpers_cover_all_variants() {
     64     use Unit::*;
     65     assert_eq!(Each.code(), "each");
     66     assert_eq!(MassKg.code(), "kg");
     67     assert_eq!(MassG.code(), "g");
     68     assert_eq!(MassOz.code(), "oz");
     69     assert_eq!(MassLb.code(), "lb");
     70     assert_eq!(VolumeL.code(), "l");
     71     assert_eq!(VolumeMl.code(), "ml");
     72 
     73     assert!(Each.is_count());
     74     assert!(!Each.is_mass());
     75     assert!(!Each.is_volume());
     76     assert!(MassLb.is_mass());
     77     assert!(!MassLb.is_count());
     78     assert!(VolumeMl.is_volume());
     79 }
     80 
     81 #[test]
     82 fn parse_mass_unit_enforces_mass_only() {
     83     assert_eq!(parse_mass_unit("kg"), Ok(Unit::MassKg));
     84     assert_eq!(parse_mass_unit("each"), Err(ParseError::NotAMassUnit));
     85     assert_eq!(parse_mass_unit("bogus"), Err(ParseError::UnknownUnit));
     86 }
     87 
     88 #[test]
     89 fn parse_volume_unit_enforces_volume_only() {
     90     assert_eq!(parse_volume_unit("l"), Ok(Unit::VolumeL));
     91     assert_eq!(parse_volume_unit("kg"), Err(ParseError::NotAVolumeUnit));
     92     assert_eq!(parse_volume_unit("bogus"), Err(ParseError::UnknownUnit));
     93 }
     94 
     95 #[test]
     96 fn convert_mass_decimal_converts_between_mass_units() {
     97     use Unit::*;
     98     let kg_to_g = convert_mass_decimal(common::dec("1"), MassKg, MassG).unwrap();
     99     let g_to_kg = convert_mass_decimal(common::dec("1000"), MassG, MassKg).unwrap();
    100     let lb_to_g = convert_mass_decimal(common::dec("1"), MassLb, MassG).unwrap();
    101     let oz_to_g = convert_mass_decimal(common::dec("1"), MassOz, MassG).unwrap();
    102     let g_to_oz = convert_mass_decimal(common::dec("28.349523125"), MassG, MassOz).unwrap();
    103     let g_to_lb = convert_mass_decimal(common::dec("453.59237"), MassG, MassLb).unwrap();
    104 
    105     assert_eq!(kg_to_g, common::dec("1000"));
    106     assert_eq!(g_to_kg, common::dec("1"));
    107     assert_eq!(lb_to_g, common::dec("453.59237"));
    108     assert_eq!(oz_to_g, common::dec("28.349523125"));
    109     assert_eq!(g_to_oz, common::dec("1"));
    110     assert_eq!(g_to_lb, common::dec("1"));
    111 }
    112 
    113 #[test]
    114 fn convert_mass_decimal_rejects_non_mass_units() {
    115     let err = convert_mass_decimal(common::dec("1"), Unit::Each, Unit::MassG).unwrap_err();
    116     assert_eq!(
    117         err,
    118         ConvertError::NotMassUnit {
    119             from: Unit::Each,
    120             to: Unit::MassG
    121         }
    122     );
    123 
    124     let err = convert_mass_decimal(common::dec("1"), Unit::MassKg, Unit::Each).unwrap_err();
    125     assert_eq!(
    126         err,
    127         ConvertError::NotMassUnit {
    128             from: Unit::MassKg,
    129             to: Unit::Each
    130         }
    131     );
    132 }
    133 
    134 #[test]
    135 fn convert_volume_decimal_converts_between_volume_units() {
    136     use Unit::*;
    137     let l_to_ml = convert_volume_decimal(common::dec("1"), VolumeL, VolumeMl).unwrap();
    138     let ml_to_l = convert_volume_decimal(common::dec("250"), VolumeMl, VolumeL).unwrap();
    139     assert_eq!(l_to_ml, common::dec("1000"));
    140     assert_eq!(ml_to_l, common::dec("0.25"));
    141 }
    142 
    143 #[test]
    144 fn convert_volume_decimal_rejects_non_volume_units() {
    145     let err = convert_volume_decimal(common::dec("1"), Unit::Each, Unit::VolumeMl).unwrap_err();
    146     assert_eq!(
    147         err,
    148         ConvertError::NotVolumeUnit {
    149             from: Unit::Each,
    150             to: Unit::VolumeMl
    151         }
    152     );
    153 
    154     let err = convert_volume_decimal(common::dec("1"), Unit::VolumeMl, Unit::Each).unwrap_err();
    155     assert_eq!(
    156         err,
    157         ConvertError::NotVolumeUnit {
    158             from: Unit::VolumeMl,
    159             to: Unit::Each
    160         }
    161     );
    162 }
    163 
    164 #[test]
    165 fn convert_unit_decimal_converts_matching_dimensions() {
    166     use Unit::*;
    167     let kg_to_g = convert_unit_decimal(common::dec("1"), MassKg, MassG).unwrap();
    168     let l_to_ml = convert_unit_decimal(common::dec("2"), VolumeL, VolumeMl).unwrap();
    169     let each_to_each = convert_unit_decimal(common::dec("3"), Each, Each).unwrap();
    170     assert_eq!(kg_to_g, common::dec("1000"));
    171     assert_eq!(l_to_ml, common::dec("2000"));
    172     assert_eq!(each_to_each, common::dec("3"));
    173 }
    174 
    175 #[test]
    176 fn conversions_report_overflow_without_panicking() {
    177     assert_eq!(
    178         convert_mass_decimal(Decimal::MAX, Unit::MassKg, Unit::MassG),
    179         Err(ConvertError::ArithmeticOverflow {
    180             from: Unit::MassKg,
    181             to: Unit::MassG,
    182         })
    183     );
    184     assert_eq!(
    185         convert_volume_decimal(Decimal::MAX, Unit::VolumeL, Unit::VolumeMl),
    186         Err(ConvertError::ArithmeticOverflow {
    187             from: Unit::VolumeL,
    188             to: Unit::VolumeMl,
    189         })
    190     );
    191 }
    192 
    193 #[test]
    194 fn exact_conversion_pairs_roundtrip() {
    195     let cases = [
    196         (common::dec("12.5"), Unit::MassKg, Unit::MassG),
    197         (common::dec("3"), Unit::MassLb, Unit::MassG),
    198         (common::dec("7"), Unit::MassOz, Unit::MassG),
    199         (common::dec("1.25"), Unit::VolumeL, Unit::VolumeMl),
    200     ];
    201     for (amount, from, to) in cases {
    202         let converted = convert_unit_decimal(amount, from, to).unwrap();
    203         let roundtrip = convert_unit_decimal(converted, to, from).unwrap();
    204         assert_eq!(roundtrip, amount, "roundtrip for {from} -> {to}");
    205     }
    206 }
    207 
    208 #[test]
    209 fn display_paths_for_unit_and_errors_are_exercised() {
    210     assert_eq!(Unit::MassOz.to_string(), "oz");
    211     assert_eq!(ParseError::UnknownUnit.to_string(), "unknown unit string");
    212     assert_eq!(
    213         ParseError::NotAMassUnit.to_string(),
    214         "unit is not a mass unit"
    215     );
    216     assert_eq!(
    217         ParseError::NotAVolumeUnit.to_string(),
    218         "unit is not a volume unit"
    219     );
    220     assert_eq!(
    221         ConvertError::NotMassUnit {
    222             from: Unit::Each,
    223             to: Unit::MassG
    224         }
    225         .to_string(),
    226         "unit conversion requires mass units: each -> g"
    227     );
    228     assert_eq!(
    229         ConvertError::NotVolumeUnit {
    230             from: Unit::Each,
    231             to: Unit::VolumeL
    232         }
    233         .to_string(),
    234         "unit conversion requires volume units: each -> l"
    235     );
    236     assert_eq!(
    237         ConvertError::NotConvertibleUnits {
    238             from: Unit::Each,
    239             to: Unit::MassG
    240         }
    241         .to_string(),
    242         "unit conversion requires matching dimensions: each -> g"
    243     );
    244     assert_eq!(
    245         ConvertError::ArithmeticOverflow {
    246             from: Unit::MassKg,
    247             to: Unit::MassG,
    248         }
    249         .to_string(),
    250         "unit conversion arithmetic overflow: kg -> g"
    251     );
    252 }
    253 
    254 #[test]
    255 fn convert_unit_decimal_rejects_mismatched_dimensions() {
    256     let err = convert_unit_decimal(common::dec("1"), Unit::Each, Unit::MassG).unwrap_err();
    257     assert_eq!(
    258         err,
    259         ConvertError::NotConvertibleUnits {
    260             from: Unit::Each,
    261             to: Unit::MassG
    262         }
    263     );
    264 }
    265 
    266 #[cfg(feature = "serde")]
    267 #[test]
    268 fn serde_roundtrip_for_unit_paths() {
    269     use Unit::*;
    270     let all = [Each, MassKg, MassG, MassOz, MassLb, VolumeL, VolumeMl];
    271     for unit in all {
    272         let json = serde_json::to_string(&unit).unwrap();
    273         let back: Unit = serde_json::from_str(&json).unwrap();
    274         assert_eq!(back, unit);
    275     }
    276     let back: Unit = serde_json::from_str("\"kg\"").unwrap();
    277     assert_eq!(back, Unit::MassKg);
    278     let unknown_err = serde_json::from_str::<Unit>("\"bogus\"").unwrap_err();
    279     assert!(unknown_err.to_string().contains("unknown unit"));
    280     let err = serde_json::from_str::<Unit>("123").unwrap_err();
    281     assert!(err.to_string().contains("invalid type"));
    282     let missing_err = serde_json::from_str::<Unit>("{}").unwrap_err();
    283     assert!(!missing_err.to_string().is_empty());
    284 }