rational_extensions 0.1.0

Extensions for rational numbers.
Documentation
//! This crate extends how `num_rational::Ratio<T>` can be converted
//! from a string specifically by allowing decimal notation with the
//! ability to constrain the minimum and maximum number of fractional
//! digits allowed.
#![no_std]
#![no_implicit_prelude]
#![deny(
    unsafe_code,
    unused,
    warnings,
    clippy::all,
    clippy::cargo,
    clippy::nursery,
    clippy::pedantic
)]
#![allow(
    clippy::implicit_return,
    clippy::missing_trait_methods,
    clippy::unseparated_literal_suffix
)]
extern crate alloc;
#[allow(unused_extern_crates)]
extern crate core;
#[allow(unused_extern_crates)]
extern crate num_integer;
#[allow(unused_extern_crates)]
extern crate num_rational;
#[allow(unused_extern_crates)]
extern crate num_traits;
#[allow(unused_extern_crates)]
extern crate serde;
use crate::FromDecStrErr::{IntParseErr, TooFewFractionalDigits, TooManyFractionalDigits};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::clone::Clone;
use core::cmp::PartialOrd;
use core::convert::From;
use core::fmt::{self, Debug, Display, Formatter};
use core::marker::PhantomData;
use core::ops::Mul;
use core::option::Option;
use core::result::Result::{self, Err, Ok};
use core::str::FromStr;
use core::{unreachable, write};
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::Pow;
/// An ordered pair whose first value is <= to the second.
pub struct MinMax<T> {
    /// The first value which is <= the second.
    min: T,
    /// The second value which is >= the first.
    max: T,
}
impl<T> MinMax<T> {
    /// Returns a reference to the first value.
    pub const fn min(&self) -> &T {
        &self.min
    }
    /// Returns a reference to the second value.
    pub const fn max(&self) -> &T {
        &self.max
    }
}
impl<T> MinMax<T>
where
    T: PartialOrd<T>,
{
    /// Returns `None` iff `min` `>` `max`.
    pub fn new(min: T, max: T) -> Option<Self> {
        (min <= max).then_some(Self { min, max })
    }
    /// Returns `MinMax` without verifying `min` `<=` `max`.

    /// # Safety
    ///
    /// `max` `>=` `min`.
    #[allow(unsafe_code)]
    pub const unsafe fn new_unchecked(min: T, max: T) -> Self {
        Self { min, max }
    }
}
/// The error returned when parsing a string in decimal notation into
/// a `num_rational::Ratio<T>`.
#[allow(clippy::exhaustive_enums)]
pub enum FromDecStrErr<T> {
    /// Contains the error returned when parsing a string into a `T`.
    IntParseErr(T),
    /// The variant returned when a decimal string has fewer rational
    /// digits than allowed.
    TooFewFractionalDigits(usize),
    /// The variant returned when a decimal string has more rational
    /// digits than allowed.
    TooManyFractionalDigits(usize),
}
impl<T> Display for FromDecStrErr<T>
where
    T: Display,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match *self {
            IntParseErr(ref x) => x.fmt(f),
            TooFewFractionalDigits(ref x) => write!(
                f,
                "There were only {x} fractional digits which is fewer than the minimum required."
            ),
            TooManyFractionalDigits(ref x) => write!(
                f,
                "There were {x} fractional digits which is more than the maximum required."
            ),
        }
    }
}
impl<T> Debug for FromDecStrErr<T>
where
    T: Display,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        <Self as Display>::fmt(self, f)
    }
}
impl<T> From<T> for FromDecStrErr<T> {
    fn from(x: T) -> Self {
        IntParseErr(x)
    }
}
/// Converts a string in decimal notation into a `Ratio<T>`.

/// # Errors
///
/// Will return `FromDecStrErr` iff `val` is not a valid rational
/// number in decimal notation with number of  fractional digits
/// inclusively between `frac_digit_count.min()` and
/// `frac_digit_count.max()`.
#[allow(clippy::arithmetic_side_effects, clippy::single_char_lifetime_names)]
pub fn try_from_dec_str<T>(
    val: &str,
    frac_digit_count: &MinMax<usize>,
) -> Result<Ratio<T>, FromDecStrErr<<T as FromStr>::Err>>
where
    T: Clone
        + From<u8>
        + FromStr
        + Integer
        + for<'a> Mul<&'a T, Output = T>
        + Pow<usize, Output = T>,
{
    val.split_once('.').map_or_else(
        || {
            if *frac_digit_count.min() == 0 {
                Ok(Ratio::from(T::from_str(val)?))
            } else {
                Err(TooFewFractionalDigits(val.len()))
            }
        },
        |(l, r)| {
            if r.len() >= *frac_digit_count.min() {
                if r.len() <= *frac_digit_count.max() {
                    let mult = T::from(10).pow(r.len());
                    Ok(Ratio::new(
                        (T::from_str(l)? * &mult) + T::from_str(r)?,
                        mult,
                    ))
                } else {
                    Err(TooManyFractionalDigits(r.len()))
                }
            } else {
                Err(TooFewFractionalDigits(r.len()))
            }
        },
    )
}
/// The error returned when parsing a string in decimal or
/// rational notation into a `num_rational::Ratio<T>`.
#[allow(clippy::exhaustive_enums)]
pub enum FromStrErr<T> {
    /// Contains the error when a string fails to parse into a `T`.
    IntParseErr(T),
    /// The variant that is returned when a string in rational
    /// notation has a denominator that is zero.
    DenominatorIsZero,
}
impl<T> Display for FromStrErr<T>
where
    T: Display,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match *self {
            Self::IntParseErr(ref x) => x.fmt(f),
            Self::DenominatorIsZero => f.write_str("denominator is zero"),
        }
    }
}
impl<T> Debug for FromStrErr<T>
where
    T: Display,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        <Self as Display>::fmt(self, f)
    }
}
impl<T> From<T> for FromStrErr<T> {
    fn from(x: T) -> Self {
        Self::IntParseErr(x)
    }
}
/// Converts a string in rational or decimal notation into a `Ratio<T>`.

/// # Errors
///
/// Will return `FromStrErr` iff `val` is not a rational number in
/// rational or decimal notation.
#[allow(
    unsafe_code,
    clippy::arithmetic_side_effects,
    clippy::single_char_lifetime_names,
    clippy::unreachable
)]
pub fn try_from_str<T>(val: &str) -> Result<Ratio<T>, FromStrErr<<T as FromStr>::Err>>
where
    T: Clone
        + From<u8>
        + FromStr
        + Integer
        + for<'a> Mul<&'a T, Output = T>
        + Pow<usize, Output = T>,
{
    val.split_once('/').map_or_else(
        || {
            // SAFETY:
            // usize::MAX >= 0
            try_from_dec_str(val, &unsafe{MinMax::new_unchecked(0, usize::MAX)}).map_err(|e| match e {
                IntParseErr(x) => FromStrErr::IntParseErr(x),
                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"),
            })
        },
        |(l, r)| {
            let denom = T::from_str(r)?;
            if denom == T::from(0) {
                Err(FromStrErr::DenominatorIsZero)
            } else {
                l.split_once(' ').map_or_else(
                    || Ok(Ratio::new(T::from_str(l)?, denom.clone())),
                    |(l2, r2)| {
                        Ok(Ratio::new(
                            (T::from_str(l2)? * &denom) + T::from_str(r2)?,
                            denom.clone(),
                        ))
                    },
                )
            }
        },
    )
}
/// Returns a `String` representing `val` in decimal notation with
/// `frac_digit_count` fractional digits using normal rounding rules.
#[allow(
    unsafe_code,
    clippy::arithmetic_side_effects,
    clippy::integer_arithmetic
)]
pub fn to_dec_string<T>(val: &Ratio<T>, frac_digit_count: usize) -> String
where
    T: Clone + Display + From<u8> + Integer + Pow<usize, Output = T>,
{
    let mult = T::from(10).pow(frac_digit_count);
    let (int, frac) = (val * &mult).round().numer().div_rem(&mult);
    let int_str = int.to_string();
    let mut v = Vec::with_capacity(int_str.len() + frac_digit_count + 1);
    v.extend_from_slice(int.to_string().as_bytes());
    if frac_digit_count > 0 {
        v.push(b'.');
        let len = v.len();
        let frac_str = frac.to_string();
        while v.len() < len + (frac_digit_count - frac_str.len()) {
            v.push(b'0');
        }
        v.extend_from_slice(frac.to_string().as_bytes());
    }
    // SAFETY:
    // Each byte in v corresponds to the UTF-8 code unit used to encode
    // U+0030–U+0039 (i.e., decimal digits).
    unsafe { String::from_utf8_unchecked(v) }
}
use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor};
/// Wrapper around a `num_rational::Ratio` that
/// deserializes a JSON string representing a rational number in
/// rational or decimal notation to a Ratio&lt;T&gt;.
#[allow(clippy::exhaustive_structs)]
pub struct Rational<T>(pub Ratio<T>);
#[allow(clippy::single_char_lifetime_names)]
impl<'de, T> Deserialize<'de> for Rational<T>
where
    T: Clone
        + From<u8>
        + FromStr
        + Integer
        + for<'a> Mul<&'a T, Output = T>
        + Pow<usize, Output = T>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        /// Visitor used to deserialize a JSON string into a Rational.
        struct RationalVisitor<T> {
            /// Does not own nor drop a `T`.
            _x: PhantomData<fn() -> T>,
        }
        #[allow(clippy::single_char_lifetime_names)]
        impl<'de, T> Visitor<'de> for RationalVisitor<T>
        where
            T: Clone
                + From<u8>
                + FromStr
                + Integer
                + for<'a> Mul<&'a T, Output = T>
                + Pow<usize, Output = T>,
        {
            type Value = Rational<T>;
            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
                formatter.write_str("struct Rational")
            }
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                try_from_str(v).map_or_else(
                    |_| {
                        Err(E::invalid_value(
                            Unexpected::Str(v),
                            &"a rational number in fraction or decimal notation",
                        ))
                    },
                    |r| Ok(Rational(r)),
                )
            }
        }
        deserializer.deserialize_str(RationalVisitor { _x: PhantomData })
    }
}
#[cfg(test)]
mod tests {
    #[allow(unused_extern_crates)]
    extern crate serde_json;
    use super::*;
    use alloc::format;
    use core::assert_eq;
    use core::num::ParseIntError;

    #[test]
    fn test_min_max() -> Result<(), String> {
        let mut m: MinMax<u32>;
        for i in 0..1000 {
            for j in 0..1000 {
                m = MinMax::new(i, i + j)
                    .ok_or_else(|| format!("Failed for {} and {}.", i, i + j))?;
                assert_eq!(*m.min(), i);
                assert_eq!(*m.max(), i + j);
            }
        }
        Ok(())
    }
    #[test]
    #[should_panic]
    fn test_min_max_err() {
        MinMax::new(f64::NAN, 0.0).unwrap();
    }
    #[test]
    fn test_dec_string() -> Result<(), String> {
        assert_eq!("0", &to_dec_string(&Ratio::<u32>::new(0, 1), 0));
        assert_eq!("5.000", &to_dec_string(&Ratio::<u32>::new(5, 1), 3));
        assert_eq!("0.66667", &to_dec_string(&Ratio::<u32>::new(2, 3), 5));
        Ok(())
    }
    #[test]
    fn test_from_str() -> Result<(), FromStrErr<ParseIntError>> {
        assert_eq!(try_from_str::<u32>("4")?, Ratio::new(4, 1));
        assert_eq!(try_from_str::<u32>("4/8")?, Ratio::new(1, 2));
        assert_eq!(try_from_str::<u32>("5 9/8")?, Ratio::new(49, 8));
        assert_eq!(try_from_str::<u32>("0.1249")?, Ratio::new(1249, 10000));
        assert_eq!(try_from_str::<u32>("0.0")?, Ratio::new(0, 1));
        try_from_str::<u32>("1/0").map_or_else(
            |e| match e {
                FromStrErr::DenominatorIsZero => (),
                _ => assert_eq!(false, true),
            },
            |_| assert_eq!(false, true),
        );
        Ok(())
    }
    #[test]
    fn test_serde() -> Result<(), serde_json::Error> {
        assert_eq!(
            Ratio::new(2u8, 3u8),
            serde_json::from_str::<Rational::<u8>>(r#""2/3""#)?.0
        );
        assert_eq!(
            Ratio::new(67u8, 100u8),
            serde_json::from_str::<Rational::<u8>>(r#""0.67""#)?.0
        );
        Ok(())
    }
}