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:
M | Cargo.toml | | | 9 | ++++++--- |
M | src/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 {