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 }