unit.rs (9286B)
1 use core::fmt; 2 use core::str::FromStr; 3 use rust_decimal_macros::dec; 4 5 #[cfg(all(feature = "serde", not(feature = "std")))] 6 use alloc::string::String; 7 #[cfg(feature = "serde")] 8 #[cfg(feature = "std")] 9 use std::string::String; 10 11 #[cfg(feature = "serde")] 12 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; 13 14 use crate::RadrootsCoreDecimal; 15 16 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 17 pub enum RadrootsCoreUnitDimension { 18 Count, 19 Mass, 20 Volume, 21 } 22 23 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 24 pub enum RadrootsCoreUnit { 25 Each, 26 MassKg, 27 MassG, 28 MassOz, 29 MassLb, 30 VolumeL, 31 VolumeMl, 32 } 33 34 impl RadrootsCoreUnit { 35 #[inline] 36 pub fn code(&self) -> &'static str { 37 match self { 38 Self::Each => "each", 39 Self::MassKg => "kg", 40 Self::MassG => "g", 41 Self::MassOz => "oz", 42 Self::MassLb => "lb", 43 Self::VolumeL => "l", 44 Self::VolumeMl => "ml", 45 } 46 } 47 48 pub fn same_dimension(a: Self, b: Self) -> bool { 49 a.dimension() == b.dimension() 50 } 51 52 #[inline] 53 pub fn dimension(&self) -> RadrootsCoreUnitDimension { 54 match self { 55 Self::Each => RadrootsCoreUnitDimension::Count, 56 Self::MassKg | Self::MassG | Self::MassOz | Self::MassLb => { 57 RadrootsCoreUnitDimension::Mass 58 } 59 Self::VolumeL | Self::VolumeMl => RadrootsCoreUnitDimension::Volume, 60 } 61 } 62 63 #[inline] 64 pub fn canonical_unit(&self) -> Self { 65 match self.dimension() { 66 RadrootsCoreUnitDimension::Count => Self::Each, 67 RadrootsCoreUnitDimension::Mass => Self::MassG, 68 RadrootsCoreUnitDimension::Volume => Self::VolumeMl, 69 } 70 } 71 72 #[inline] 73 pub fn is_volume(&self) -> bool { 74 matches!(self, Self::VolumeL | Self::VolumeMl) 75 } 76 77 #[inline] 78 pub fn is_mass(&self) -> bool { 79 matches!( 80 self, 81 Self::MassKg | Self::MassG | Self::MassOz | Self::MassLb 82 ) 83 } 84 85 #[inline] 86 pub fn is_count(&self) -> bool { 87 matches!(self, Self::Each) 88 } 89 } 90 91 impl fmt::Display for RadrootsCoreUnit { 92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 f.write_str(self.code()) 94 } 95 } 96 97 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 98 pub enum RadrootsCoreUnitParseError { 99 UnknownUnit, 100 NotAMassUnit, 101 NotAVolumeUnit, 102 } 103 104 impl fmt::Display for RadrootsCoreUnitParseError { 105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 106 match self { 107 Self::UnknownUnit => write!(f, "unknown unit string"), 108 Self::NotAMassUnit => write!(f, "unit is not a mass unit"), 109 Self::NotAVolumeUnit => write!(f, "unit is not a volume unit"), 110 } 111 } 112 } 113 114 #[cfg(feature = "std")] 115 impl std::error::Error for RadrootsCoreUnitParseError {} 116 117 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 118 pub enum RadrootsCoreUnitConvertError { 119 NotMassUnit { 120 from: RadrootsCoreUnit, 121 to: RadrootsCoreUnit, 122 }, 123 NotVolumeUnit { 124 from: RadrootsCoreUnit, 125 to: RadrootsCoreUnit, 126 }, 127 NotConvertibleUnits { 128 from: RadrootsCoreUnit, 129 to: RadrootsCoreUnit, 130 }, 131 } 132 133 impl fmt::Display for RadrootsCoreUnitConvertError { 134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 135 match self { 136 RadrootsCoreUnitConvertError::NotMassUnit { from, to } => { 137 write!(f, "unit conversion requires mass units: {from} -> {to}") 138 } 139 RadrootsCoreUnitConvertError::NotVolumeUnit { from, to } => { 140 write!(f, "unit conversion requires volume units: {from} -> {to}") 141 } 142 RadrootsCoreUnitConvertError::NotConvertibleUnits { from, to } => { 143 write!( 144 f, 145 "unit conversion requires matching dimensions: {from} -> {to}" 146 ) 147 } 148 } 149 } 150 } 151 152 #[cfg(feature = "std")] 153 impl std::error::Error for RadrootsCoreUnitConvertError {} 154 155 impl FromStr for RadrootsCoreUnit { 156 type Err = RadrootsCoreUnitParseError; 157 158 fn from_str(s: &str) -> Result<Self, Self::Err> { 159 let s = s.trim().to_ascii_lowercase(); 160 match s.as_str() { 161 "each" | "ea" | "count" => Ok(RadrootsCoreUnit::Each), 162 "kg" | "kilogram" | "kilograms" => Ok(RadrootsCoreUnit::MassKg), 163 "g" | "gram" | "grams" => Ok(RadrootsCoreUnit::MassG), 164 "oz" | "ounce" | "ounces" => Ok(RadrootsCoreUnit::MassOz), 165 "lb" | "pound" | "pounds" => Ok(RadrootsCoreUnit::MassLb), 166 "l" | "liter" | "litre" | "liters" | "litres" => Ok(RadrootsCoreUnit::VolumeL), 167 "ml" | "milliliter" | "millilitre" | "milliliters" | "millilitres" => { 168 Ok(RadrootsCoreUnit::VolumeMl) 169 } 170 _ => Err(RadrootsCoreUnitParseError::UnknownUnit), 171 } 172 } 173 } 174 175 #[cfg(feature = "serde")] 176 impl Serialize for RadrootsCoreUnit { 177 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> { 178 ser.serialize_str(self.code()) 179 } 180 } 181 182 #[cfg(feature = "serde")] 183 impl<'de> Deserialize<'de> for RadrootsCoreUnit { 184 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> { 185 let s = String::deserialize(de)?; 186 s.parse().map_err(D::Error::custom) 187 } 188 } 189 190 #[inline] 191 pub fn parse_mass_unit(s: &str) -> Result<RadrootsCoreUnit, RadrootsCoreUnitParseError> { 192 let u: RadrootsCoreUnit = RadrootsCoreUnit::from_str(s)?; 193 if u.is_mass() { 194 Ok(u) 195 } else { 196 Err(RadrootsCoreUnitParseError::NotAMassUnit) 197 } 198 } 199 200 #[inline] 201 pub fn parse_volume_unit(s: &str) -> Result<RadrootsCoreUnit, RadrootsCoreUnitParseError> { 202 let u: RadrootsCoreUnit = RadrootsCoreUnit::from_str(s)?; 203 if u.is_volume() { 204 Ok(u) 205 } else { 206 Err(RadrootsCoreUnitParseError::NotAVolumeUnit) 207 } 208 } 209 210 #[inline] 211 pub fn convert_mass_decimal( 212 amount: RadrootsCoreDecimal, 213 from: RadrootsCoreUnit, 214 to: RadrootsCoreUnit, 215 ) -> Result<RadrootsCoreDecimal, RadrootsCoreUnitConvertError> { 216 let amount_g = match from { 217 RadrootsCoreUnit::MassG => amount, 218 RadrootsCoreUnit::MassKg => amount * RadrootsCoreDecimal::from(1000u32), 219 RadrootsCoreUnit::MassOz => amount * RadrootsCoreDecimal(dec!(28.349523125)), 220 RadrootsCoreUnit::MassLb => amount * RadrootsCoreDecimal(dec!(453.59237)), 221 _ => { 222 return Err(RadrootsCoreUnitConvertError::NotMassUnit { from, to }); 223 } 224 }; 225 226 let to_factor = match to { 227 RadrootsCoreUnit::MassG => RadrootsCoreDecimal::ONE, 228 RadrootsCoreUnit::MassKg => RadrootsCoreDecimal::from(1000u32), 229 RadrootsCoreUnit::MassOz => RadrootsCoreDecimal(dec!(28.349523125)), 230 RadrootsCoreUnit::MassLb => RadrootsCoreDecimal(dec!(453.59237)), 231 _ => { 232 return Err(RadrootsCoreUnitConvertError::NotMassUnit { from, to }); 233 } 234 }; 235 236 Ok(amount_g / to_factor) 237 } 238 239 #[inline] 240 pub fn convert_volume_decimal( 241 amount: RadrootsCoreDecimal, 242 from: RadrootsCoreUnit, 243 to: RadrootsCoreUnit, 244 ) -> Result<RadrootsCoreDecimal, RadrootsCoreUnitConvertError> { 245 let amount_ml = match from { 246 RadrootsCoreUnit::VolumeMl => amount, 247 RadrootsCoreUnit::VolumeL => amount * RadrootsCoreDecimal::from(1000u32), 248 _ => { 249 return Err(RadrootsCoreUnitConvertError::NotVolumeUnit { from, to }); 250 } 251 }; 252 253 let to_factor = match to { 254 RadrootsCoreUnit::VolumeMl => RadrootsCoreDecimal::ONE, 255 RadrootsCoreUnit::VolumeL => RadrootsCoreDecimal::from(1000u32), 256 _ => { 257 return Err(RadrootsCoreUnitConvertError::NotVolumeUnit { from, to }); 258 } 259 }; 260 261 Ok(amount_ml / to_factor) 262 } 263 264 #[inline] 265 pub fn convert_unit_decimal( 266 amount: RadrootsCoreDecimal, 267 from: RadrootsCoreUnit, 268 to: RadrootsCoreUnit, 269 ) -> Result<RadrootsCoreDecimal, RadrootsCoreUnitConvertError> { 270 if !RadrootsCoreUnit::same_dimension(from, to) { 271 return Err(RadrootsCoreUnitConvertError::NotConvertibleUnits { from, to }); 272 } 273 match from.dimension() { 274 RadrootsCoreUnitDimension::Count => Ok(amount), 275 RadrootsCoreUnitDimension::Mass => convert_mass_decimal(amount, from, to), 276 RadrootsCoreUnitDimension::Volume => convert_volume_decimal(amount, from, to), 277 } 278 } 279 280 #[cfg(test)] 281 mod tests { 282 use super::*; 283 284 #[test] 285 fn convert_paths_cover_unit_branches() { 286 assert_eq!( 287 convert_mass_decimal( 288 RadrootsCoreDecimal::ONE, 289 RadrootsCoreUnit::Each, 290 RadrootsCoreUnit::MassG 291 ), 292 Err(RadrootsCoreUnitConvertError::NotMassUnit { 293 from: RadrootsCoreUnit::Each, 294 to: RadrootsCoreUnit::MassG 295 }) 296 ); 297 assert_eq!( 298 convert_volume_decimal( 299 RadrootsCoreDecimal::ONE, 300 RadrootsCoreUnit::Each, 301 RadrootsCoreUnit::VolumeMl 302 ), 303 Err(RadrootsCoreUnitConvertError::NotVolumeUnit { 304 from: RadrootsCoreUnit::Each, 305 to: RadrootsCoreUnit::VolumeMl 306 }) 307 ); 308 } 309 }