lib

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

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 }