rational_extensions

Extends num_rational::Ratio<T>.
git clone https://git.philomathiclife.com/repos/rational_extensions
Log | Files | Refs | README

lib.rs (12618B)


      1 //! This crate extends how `num_rational::Ratio<T>` can be converted
      2 //! from a string specifically by allowing decimal notation with the
      3 //! ability to constrain the minimum and maximum number of fractional
      4 //! digits allowed.
      5 #![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
      6 #![no_std]
      7 #![deny(
      8     unsafe_code,
      9     unused,
     10     warnings,
     11     clippy::all,
     12     clippy::cargo,
     13     clippy::nursery,
     14     clippy::pedantic
     15 )]
     16 #![allow(
     17     clippy::implicit_return,
     18     clippy::question_mark_used,
     19     clippy::unseparated_literal_suffix
     20 )]
     21 extern crate alloc;
     22 use crate::FromDecStrErr::{IntParseErr, TooFewFractionalDigits, TooManyFractionalDigits};
     23 use alloc::string::{String, ToString};
     24 use alloc::vec::Vec;
     25 use core::fmt::{self, Debug, Display, Formatter};
     26 use core::ops::Mul;
     27 use core::str::FromStr;
     28 use num_integer::Integer;
     29 use num_rational::Ratio;
     30 use num_traits::Pow;
     31 /// An ordered pair whose first value is <= to the second.
     32 pub struct MinMax<T> {
     33     /// The first value which is <= the second.
     34     min: T,
     35     /// The second value which is >= the first.
     36     max: T,
     37 }
     38 impl<T> MinMax<T> {
     39     /// Returns a reference to the first value.
     40     #[inline]
     41     pub const fn min(&self) -> &T {
     42         &self.min
     43     }
     44     /// Returns a reference to the second value.
     45     #[inline]
     46     pub const fn max(&self) -> &T {
     47         &self.max
     48     }
     49 }
     50 impl<T> MinMax<T>
     51 where
     52     T: PartialOrd<T>,
     53 {
     54     /// Returns `Some(T)` iff `min` `<=` `max`.
     55     #[inline]
     56     pub fn new(min: T, max: T) -> Option<Self> {
     57         (min <= max).then_some(Self { min, max })
     58     }
     59     /// Returns `MinMax` without verifying `min` `<=` `max`.
     60 
     61     /// # Safety
     62     ///
     63     /// `min` `<=` `max`.
     64     #[allow(unsafe_code)]
     65     #[inline]
     66     pub const unsafe fn new_unchecked(min: T, max: T) -> Self {
     67         Self { min, max }
     68     }
     69 }
     70 /// The error returned when parsing a string in decimal notation into
     71 /// a `num_rational::Ratio<T>`.
     72 #[allow(clippy::exhaustive_enums)]
     73 pub enum FromDecStrErr<T> {
     74     /// Contains the error returned when parsing a string into a `T`.
     75     IntParseErr(T),
     76     /// The variant returned when a decimal string has fewer rational
     77     /// digits than allowed.
     78     TooFewFractionalDigits(usize),
     79     /// The variant returned when a decimal string has more rational
     80     /// digits than allowed.
     81     TooManyFractionalDigits(usize),
     82 }
     83 impl<T> Display for FromDecStrErr<T>
     84 where
     85     T: Display,
     86 {
     87     #[inline]
     88     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     89         match *self {
     90             IntParseErr(ref x) => x.fmt(f),
     91             TooFewFractionalDigits(ref x) => write!(
     92                 f,
     93                 "There were only {x} fractional digits which is fewer than the minimum required."
     94             ),
     95             TooManyFractionalDigits(ref x) => write!(
     96                 f,
     97                 "There were {x} fractional digits which is more than the maximum required."
     98             ),
     99         }
    100     }
    101 }
    102 impl<T> Debug for FromDecStrErr<T>
    103 where
    104     T: Display,
    105 {
    106     #[inline]
    107     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    108         <Self as Display>::fmt(self, f)
    109     }
    110 }
    111 impl<T> From<T> for FromDecStrErr<T> {
    112     #[inline]
    113     fn from(x: T) -> Self {
    114         IntParseErr(x)
    115     }
    116 }
    117 /// Converts a string in decimal notation into a `Ratio<T>`.
    118 ///
    119 /// # Errors
    120 ///
    121 /// Will return `FromDecStrErr` iff `val` is not a valid rational
    122 /// number in decimal notation with number of fractional digits
    123 /// inclusively between `frac_digit_count.min()` and
    124 /// `frac_digit_count.max()`.
    125 #[allow(
    126     clippy::arithmetic_side_effects,
    127     clippy::indexing_slicing,
    128     clippy::single_char_lifetime_names,
    129     clippy::string_slice
    130 )]
    131 #[inline]
    132 pub fn try_from_dec_str<T>(
    133     val: &str,
    134     frac_digit_count: &MinMax<usize>,
    135 ) -> Result<Ratio<T>, FromDecStrErr<<T as FromStr>::Err>>
    136 where
    137     T: Clone
    138         + From<u8>
    139         + FromStr
    140         + Integer
    141         + for<'a> Mul<&'a T, Output = T>
    142         + Pow<usize, Output = T>,
    143 {
    144     val.split_once('.').map_or_else(
    145         || {
    146             if *frac_digit_count.min() == 0 {
    147                 Ok(Ratio::from(T::from_str(val)?))
    148             } else {
    149                 Err(TooFewFractionalDigits(val.len()))
    150             }
    151         },
    152         |(l, r)| {
    153             if r.len() >= *frac_digit_count.min() {
    154                 if r.len() <= *frac_digit_count.max() {
    155                     let mult = T::from(10).pow(r.len());
    156                     let numer = T::from_str(l)? * &mult;
    157                     let addend = T::from_str(r)?;
    158                     let zero = T::from(0);
    159                     Ok(Ratio::new(
    160                         if numer < zero || (numer == zero && &l[..1] == "-") {
    161                             numer - addend
    162                         } else {
    163                             numer + addend
    164                         },
    165                         mult,
    166                     ))
    167                 } else {
    168                     Err(TooManyFractionalDigits(r.len()))
    169                 }
    170             } else {
    171                 Err(TooFewFractionalDigits(r.len()))
    172             }
    173         },
    174     )
    175 }
    176 /// The error returned when parsing a string in decimal or
    177 /// rational notation into a `num_rational::Ratio<T>`.
    178 #[allow(clippy::exhaustive_enums)]
    179 pub enum FromStrErr<T> {
    180     /// Contains the error when a string fails to parse into a `T`.
    181     IntParseErr(T),
    182     /// The variant that is returned when a string in rational
    183     /// notation has a denominator that is zero.
    184     DenominatorIsZero,
    185 }
    186 impl<T> Display for FromStrErr<T>
    187 where
    188     T: Display,
    189 {
    190     #[inline]
    191     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    192         match *self {
    193             Self::IntParseErr(ref x) => x.fmt(f),
    194             Self::DenominatorIsZero => f.write_str("denominator is zero"),
    195         }
    196     }
    197 }
    198 impl<T> Debug for FromStrErr<T>
    199 where
    200     T: Display,
    201 {
    202     #[inline]
    203     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    204         <Self as Display>::fmt(self, f)
    205     }
    206 }
    207 impl<T> From<T> for FromStrErr<T> {
    208     #[inline]
    209     fn from(x: T) -> Self {
    210         Self::IntParseErr(x)
    211     }
    212 }
    213 /// Converts a string in rational or decimal notation into a `Ratio<T>`.
    214 ///
    215 /// # Errors
    216 ///
    217 /// Will return `FromStrErr` iff `val` is not a rational number in
    218 /// rational or decimal notation.
    219 #[allow(
    220     unsafe_code,
    221     clippy::arithmetic_side_effects,
    222     clippy::single_char_lifetime_names,
    223     clippy::unreachable
    224 )]
    225 #[inline]
    226 pub fn try_from_str<T>(val: &str) -> Result<Ratio<T>, FromStrErr<<T as FromStr>::Err>>
    227 where
    228     T: Clone
    229         + From<u8>
    230         + FromStr
    231         + Integer
    232         + for<'a> Mul<&'a T, Output = T>
    233         + Pow<usize, Output = T>,
    234 {
    235     /// Used as the closure that transforms a `FromDecStrErr` into a `FromStrErr`.
    236     #[inline]
    237     fn dec_err<S>(err: FromDecStrErr<<S as FromStr>::Err>) -> FromStrErr<<S as FromStr>::Err>
    238     where
    239         S: FromStr,
    240     {
    241         match err {
    242             IntParseErr(x) => FromStrErr::IntParseErr(x),
    243             TooFewFractionalDigits(_) | TooManyFractionalDigits(_) => unreachable!("There is a bug in rational::try_from_dec_str. 0 and usize::MAX were passed as the minimum and maximum number of fractional digits allowed respectively, but it still errored due to too few or too many fractional digits"),
    244         }
    245     }
    246     /// Used as the closure that attempts to transform a `String` split from a `/`
    247     /// into a `Ratio<S>`.
    248     #[inline]
    249     fn frac_split<S>(split: (&str, &str)) -> Result<Ratio<S>, FromStrErr<<S as FromStr>::Err>>
    250     where
    251         S: Clone + From<u8> + FromStr + Integer + for<'a> Mul<&'a S, Output = S>,
    252     {
    253         let denom = S::from_str(split.1)?;
    254         let zero = S::from(0);
    255         if denom == zero {
    256             Err(FromStrErr::DenominatorIsZero)
    257         } else {
    258             split.0.split_once(' ').map_or_else(
    259                 || Ok(Ratio::new(S::from_str(split.0)?, denom.clone())),
    260                 |(l2, r2)| {
    261                     let numer = S::from_str(l2)? * &denom;
    262                     let addend = S::from_str(r2)?;
    263                     Ok(Ratio::new(
    264                         if numer < zero {
    265                             numer - addend
    266                         } else {
    267                             numer + addend
    268                         },
    269                         denom.clone(),
    270                     ))
    271                 },
    272             )
    273         }
    274     }
    275     val.split_once('/').map_or_else(
    276         || {
    277             // SAFETY:
    278             // usize::MAX >= 0
    279             try_from_dec_str(val, &unsafe { MinMax::new_unchecked(0, usize::MAX) })
    280                 .map_err(dec_err::<T>)
    281         },
    282         frac_split,
    283     )
    284 }
    285 /// Returns a `String` representing `val` in decimal notation with
    286 /// `frac_digit_count` fractional digits using normal rounding rules.
    287 #[allow(
    288     unsafe_code,
    289     clippy::arithmetic_side_effects,
    290     clippy::as_conversions,
    291     clippy::cast_lossless,
    292     clippy::indexing_slicing,
    293     clippy::string_slice
    294 )]
    295 #[inline]
    296 pub fn to_dec_string<T>(val: &Ratio<T>, frac_digit_count: usize) -> String
    297 where
    298     T: Clone + Display + From<u8> + Integer + Pow<usize, Output = T>,
    299 {
    300     /// Returns 1 iff `start` is `"-"`; otherwise returns 0.
    301     /// This function is used as the closure passed to `map_or` when checking
    302     /// if the first "character" of the fraction string is a negative sign.
    303     #[inline]
    304     fn neg_sign(start: &str) -> usize {
    305         (start == "-") as usize
    306     }
    307     let mult = T::from(10).pow(frac_digit_count);
    308     let (int, frac) = (val * &mult).round().numer().div_rem(&mult);
    309     let int_str = int.to_string();
    310     let mut v = Vec::with_capacity(int_str.len() + frac_digit_count + 2);
    311     let zero = T::from(0);
    312     if int >= zero && frac < zero {
    313         v.push(b'-');
    314     }
    315     v.extend_from_slice(int.to_string().as_bytes());
    316     if frac_digit_count > 0 {
    317         v.push(b'.');
    318         let len = v.len();
    319         let frac_str = frac.to_string();
    320         let frac_val = &frac_str[frac_str.get(..1).map_or(0, neg_sign)..];
    321         while v.len() < len + (frac_digit_count - frac_val.len()) {
    322             v.push(b'0');
    323         }
    324         v.extend_from_slice(frac_val.as_bytes());
    325     }
    326     // SAFETY:
    327     // v contains precisely the UTF-8 code units returned from Strings
    328     // returned from the to_string function on the integer and fraction part of
    329     // the passed value plus optionally the single byte encodings of ".", "-", and "0".
    330     unsafe { String::from_utf8_unchecked(v) }
    331 }
    332 #[cfg(feature = "rational")]
    333 // Enables deserialization of strings in decimal or fractional format
    334 // into [`rational::Rational<T>`].
    335 pub mod rational;
    336 
    337 #[cfg(test)]
    338 mod tests {
    339     use super::*;
    340     use alloc::format;
    341     use core::assert_eq;
    342     use core::num::ParseIntError;
    343 
    344     #[test]
    345     fn test_min_max() -> Result<(), String> {
    346         let mut m: MinMax<u32>;
    347         for i in 0..1000 {
    348             for j in 0..1000 {
    349                 m = MinMax::new(i, i + j)
    350                     .ok_or_else(|| format!("Failed for {} and {}.", i, i + j))?;
    351                 assert_eq!(*m.min(), i);
    352                 assert_eq!(*m.max(), i + j);
    353             }
    354         }
    355         Ok(())
    356     }
    357     #[test]
    358     #[should_panic]
    359     fn test_min_max_err() {
    360         MinMax::new(f64::NAN, 0.0).unwrap();
    361     }
    362     #[test]
    363     fn test_dec_string() -> Result<(), String> {
    364         assert_eq!("0", &to_dec_string(&Ratio::<u32>::new(0, 1), 0));
    365         assert_eq!("-1.33", &to_dec_string(&Ratio::<i32>::new(-4, 3), 2));
    366         assert_eq!("-0.33", &to_dec_string(&Ratio::<i32>::new(-1, 3), 2));
    367         assert_eq!("5.000", &to_dec_string(&Ratio::<u32>::new(5, 1), 3));
    368         assert_eq!("0.66667", &to_dec_string(&Ratio::<u32>::new(2, 3), 5));
    369         Ok(())
    370     }
    371     #[test]
    372     fn test_from_str() -> Result<(), FromStrErr<ParseIntError>> {
    373         assert_eq!(try_from_str::<u32>("4")?, Ratio::new(4, 1));
    374         assert_eq!(try_from_str::<u32>("4/8")?, Ratio::new(1, 2));
    375         assert_eq!(try_from_str::<u32>("5 9/8")?, Ratio::new(49, 8));
    376         assert_eq!(try_from_str::<i32>("-2 7/6")?, Ratio::new(-19, 6));
    377         assert_eq!(try_from_str::<i32>("-5/6")?, Ratio::new(-5, 6));
    378         assert_eq!(try_from_str::<u32>("0.1249")?, Ratio::new(1249, 10000));
    379         assert_eq!(try_from_str::<i32>("-1.33")?, Ratio::new(-133, 100));
    380         assert_eq!(try_from_str::<i32>("-0.33")?, Ratio::new(-33, 100));
    381         assert_eq!(try_from_str::<u32>("0.0")?, Ratio::new(0, 1));
    382         try_from_str::<u32>("1/0").map_or_else(
    383             |e| match e {
    384                 FromStrErr::DenominatorIsZero => (),
    385                 _ => assert_eq!(false, true),
    386             },
    387             |_| assert_eq!(false, true),
    388         );
    389         Ok(())
    390     }
    391 }