rational_extensions

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

commit 1dafc20564a95d3fc9ddf61c360a7aed12e69119
parent 14d54b1d29f150a069b507b5ddf2558289d7b510
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 10 Apr 2023 13:34:26 -0600

fixed bug dealing with negative integers

Diffstat:
MCargo.toml | 9++++++---
Msrc/lib.rs | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
2 files changed, 116 insertions(+), 34 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" name = "rational_extensions" readme = "README.md" repository = "https://git.philomathiclife.com/repos/rational_extensions/" -version = "0.1.1" +version = "0.3.0" [lib] name = "rational_extensions" @@ -19,7 +19,10 @@ path = "src/lib.rs" num-integer = { version = "0.1.45", default-features = false } num-rational = { version = "0.4.1", default-features = false } num-traits = { version = "0.2.15", default-features = false } -serde = { version = "1.0.152", default-features = false } +serde = { version = "1.0.159", default-features = false } [dev-dependencies] -serde_json = { version = "1.0.91", default-features = false, features = ["alloc"] } +serde_json = { version = "1.0.95", default-features = false, features = ["alloc"] } + +[badges] +maintenance = { status = "passively-maintained" } diff --git a/src/lib.rs b/src/lib.rs @@ -16,6 +16,7 @@ #![allow( clippy::implicit_return, clippy::missing_trait_methods, + clippy::question_mark_used, clippy::unseparated_literal_suffix )] extern crate alloc; @@ -54,10 +55,12 @@ pub struct MinMax<T> { } impl<T> MinMax<T> { /// Returns a reference to the first value. + #[inline] pub const fn min(&self) -> &T { &self.min } /// Returns a reference to the second value. + #[inline] pub const fn max(&self) -> &T { &self.max } @@ -66,7 +69,8 @@ impl<T> MinMax<T> where T: PartialOrd<T>, { - /// Returns `None` iff `min` `<=` `max` is `false`. + /// Returns `Some(T)` iff `min` `<=` `max`. + #[inline] pub fn new(min: T, max: T) -> Option<Self> { (min <= max).then_some(Self { min, max }) } @@ -76,6 +80,7 @@ where /// /// `min` `<=` `max`. #[allow(unsafe_code)] + #[inline] pub const unsafe fn new_unchecked(min: T, max: T) -> Self { Self { min, max } } @@ -97,6 +102,7 @@ impl<T> Display for FromDecStrErr<T> where T: Display, { + #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { IntParseErr(ref x) => x.fmt(f), @@ -115,11 +121,13 @@ impl<T> Debug for FromDecStrErr<T> where T: Display, { + #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { <Self as Display>::fmt(self, f) } } impl<T> From<T> for FromDecStrErr<T> { + #[inline] fn from(x: T) -> Self { IntParseErr(x) } @@ -129,10 +137,16 @@ impl<T> From<T> for FromDecStrErr<T> { /// # Errors /// /// Will return `FromDecStrErr` iff `val` is not a valid rational -/// number in decimal notation with number of fractional digits +/// 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)] +#[allow( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::single_char_lifetime_names, + clippy::string_slice +)] +#[inline] pub fn try_from_dec_str<T>( val: &str, frac_digit_count: &MinMax<usize>, @@ -157,8 +171,15 @@ where if r.len() >= *frac_digit_count.min() { if r.len() <= *frac_digit_count.max() { let mult = T::from(10).pow(r.len()); + let numer = T::from_str(l)? * &mult; + let addend = T::from_str(r)?; + let zero = T::from(0); Ok(Ratio::new( - (T::from_str(l)? * &mult) + T::from_str(r)?, + if numer < zero || (numer == zero && &l[..1] == "-") { + numer - addend + } else { + numer + addend + }, mult, )) } else { @@ -184,6 +205,7 @@ impl<T> Display for FromStrErr<T> where T: Display, { + #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Self::IntParseErr(ref x) => x.fmt(f), @@ -195,11 +217,13 @@ impl<T> Debug for FromStrErr<T> where T: Display, { + #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { <Self as Display>::fmt(self, f) } } impl<T> From<T> for FromStrErr<T> { + #[inline] fn from(x: T) -> Self { Self::IntParseErr(x) } @@ -216,6 +240,7 @@ impl<T> From<T> for FromStrErr<T> { clippy::single_char_lifetime_names, clippy::unreachable )] +#[inline] pub fn try_from_str<T>(val: &str) -> Result<Ratio<T>, FromStrErr<<T as FromStr>::Err>> where T: Clone @@ -225,31 +250,54 @@ where + for<'a> Mul<&'a T, Output = T> + Pow<usize, Output = T>, { + /// Used as the closure that transforms a `FromDecStrErr` into a `FromStrErr`. + #[inline] + fn dec_err<S>(err: FromDecStrErr<<S as FromStr>::Err>) -> FromStrErr<<S as FromStr>::Err> + where + S: FromStr, + { + match err { + 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"), + } + } + /// Used as the closure that attempts to transform a `String` split from a `/` + /// into a `Ratio<S>`. + #[inline] + fn frac_split<S>(split: (&str, &str)) -> Result<Ratio<S>, FromStrErr<<S as FromStr>::Err>> + where + S: Clone + From<u8> + FromStr + Integer + for<'a> Mul<&'a S, Output = S>, + { + let denom = S::from_str(split.1)?; + let zero = S::from(0); + if denom == zero { + Err(FromStrErr::DenominatorIsZero) + } else { + split.0.split_once(' ').map_or_else( + || Ok(Ratio::new(S::from_str(split.0)?, denom.clone())), + |(l2, r2)| { + let numer = S::from_str(l2)? * &denom; + let addend = S::from_str(r2)?; + Ok(Ratio::new( + if numer < zero { + numer - addend + } else { + numer + addend + }, + denom.clone(), + )) + }, + ) + } + } 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(), - )) - }, - ) - } + try_from_dec_str(val, &unsafe { MinMax::new_unchecked(0, usize::MAX) }) + .map_err(dec_err::<T>) }, + frac_split, ) } /// Returns a `String` representing `val` in decimal notation with @@ -257,29 +305,47 @@ where #[allow( unsafe_code, clippy::arithmetic_side_effects, - clippy::integer_arithmetic + clippy::as_conversions, + clippy::cast_lossless, + clippy::indexing_slicing, + clippy::integer_arithmetic, + clippy::string_slice )] +#[inline] 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>, { + /// Returns 1 iff `start` is `"-"`; otherwise returns 0. + /// This function is used as the closure passed to `map_or` when checking + /// if the first "character" of the fraction string is a negative sign. + #[inline] + fn neg_sign(start: &str) -> usize { + (start == "-") as usize + } 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); + let mut v = Vec::with_capacity(int_str.len() + frac_digit_count + 2); + let zero = T::from(0); + if int >= zero && frac < zero { + v.push(b'-'); + } 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()) { + let frac_val = &frac_str[frac_str.get(..1).map_or(0, neg_sign)..]; + while v.len() < len + (frac_digit_count - frac_val.len()) { v.push(b'0'); } - v.extend_from_slice(frac.to_string().as_bytes()); + v.extend_from_slice(frac_val.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). + // v contains precisely the UTF-8 code units returned from Strings + // returned from the to_string function on the integer and fraction part of + // the passed value plus optionally the single byte encodings of ".", "-", and "0". unsafe { String::from_utf8_unchecked(v) } } use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor}; @@ -298,6 +364,7 @@ where + for<'a> Mul<&'a T, Output = T> + Pow<usize, Output = T>, { + #[inline] fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -325,6 +392,12 @@ where where E: de::Error, { + #[allow(clippy::unnecessary_wraps)] + /// Used as the closure that maps a `Ratio<R>`into an `Ok(Rational<R>)`. + #[inline] + const fn to_rational<R, E2>(val: Ratio<R>) -> Result<Rational<R>, E2> { + Ok(Rational(val)) + } try_from_str(v).map_or_else( |_| { Err(E::invalid_value( @@ -332,7 +405,7 @@ where &"a rational number in fraction or decimal notation", )) }, - |r| Ok(Rational(r)), + to_rational, ) } } @@ -369,6 +442,8 @@ mod tests { #[test] fn test_dec_string() -> Result<(), String> { assert_eq!("0", &to_dec_string(&Ratio::<u32>::new(0, 1), 0)); + assert_eq!("-1.33", &to_dec_string(&Ratio::<i32>::new(-4, 3), 2)); + assert_eq!("-0.33", &to_dec_string(&Ratio::<i32>::new(-1, 3), 2)); 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(()) @@ -378,7 +453,11 @@ mod tests { 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::<i32>("-2 7/6")?, Ratio::new(-19, 6)); + assert_eq!(try_from_str::<i32>("-5/6")?, Ratio::new(-5, 6)); assert_eq!(try_from_str::<u32>("0.1249")?, Ratio::new(1249, 10000)); + assert_eq!(try_from_str::<i32>("-1.33")?, Ratio::new(-133, 100)); + assert_eq!(try_from_str::<i32>("-0.33")?, Ratio::new(-33, 100)); assert_eq!(try_from_str::<u32>("0.0")?, Ratio::new(0, 1)); try_from_str::<u32>("1/0").map_or_else( |e| match e {