#![cfg_attr(docsrs, feature(doc_cfg))]
#![no_std]
extern crate alloc;
use crate::FromDecStrErr::{IntParseErr, TooFewFractionalDigits, TooManyFractionalDigits};
use alloc::{
string::{String, ToString as _},
vec::Vec,
};
use core::{
fmt::{self, Debug, Display, Formatter},
ops::Mul,
str::FromStr,
};
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::Pow;
#[derive(Clone, Copy, Debug)]
pub struct MinMax<T> {
min: T,
max: T,
}
impl<T> MinMax<T> {
#[inline]
pub const fn min(&self) -> &T {
&self.min
}
#[inline]
pub const fn max(&self) -> &T {
&self.max
}
}
impl<T> MinMax<T>
where
T: PartialOrd<T>,
{
#[inline]
pub fn new(min: T, max: T) -> Option<Self> {
(min <= max).then_some(Self { min, max })
}
#[expect(
unsafe_code,
reason = "want to expose a function that does not uphold the invariants"
)]
#[inline]
pub const unsafe fn new_unchecked(min: T, max: T) -> Self {
Self { min, max }
}
}
pub enum FromDecStrErr<T> {
IntParseErr(T),
TooFewFractionalDigits(usize),
TooManyFractionalDigits(usize),
}
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),
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,
{
#[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)
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "calling code's responsibility to ensure T implements arithmetic correctly"
)]
#[inline]
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());
let numer = T::from_str(l)? * &mult;
let addend = T::from_str(r)?;
let zero = T::from(0);
Ok(Ratio::new(
if numer < zero
|| (numer == zero
&& l.as_bytes().first().is_some_and(|fst| *fst == b'-'))
{
numer - addend
} else {
numer + addend
},
mult,
))
} else {
Err(TooManyFractionalDigits(r.len()))
}
} else {
Err(TooFewFractionalDigits(r.len()))
}
},
)
}
pub enum FromStrErr<T> {
IntParseErr(T),
DenominatorIsZero,
}
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),
Self::DenominatorIsZero => f.write_str("denominator is zero"),
}
}
}
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)
}
}
#[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
#[expect(
clippy::arithmetic_side_effects,
reason = "calling code's responsibility to ensure T implements arithmetic correctly"
)]
#[inline]
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(
|| {
try_from_dec_str(
val,
&MinMax {
min: 0,
max: usize::MAX,
},
)
.map_err(|err| 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"
),
})
},
|split| {
let denom = T::from_str(split.1)?;
let zero = T::from(0);
if denom == zero {
Err(FromStrErr::DenominatorIsZero)
} else {
split.0.split_once(' ').map_or_else(
|| Ok(Ratio::new(T::from_str(split.0)?, denom.clone())),
|(l2, r2)| {
let numer = T::from_str(l2)? * &denom;
let addend = T::from_str(r2)?;
Ok(Ratio::new(
if numer < zero {
numer - addend
} else {
numer + addend
},
denom.clone(),
))
},
)
}
}
)
}
#[expect(unsafe_code, reason = "comment justifies correctness")]
#[expect(
clippy::arithmetic_side_effects,
clippy::expect_used,
clippy::indexing_slicing,
reason = "calling code's responsibility to ensure T implements arithmetic correctly"
)]
#[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>,
{
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()
.saturating_add(frac_digit_count.saturating_add(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_vec = frac.to_string().into_bytes();
let frac_val = &frac_vec[frac_vec
.first()
.map_or(0, |start| usize::from(*start == b'-'))..];
let term = len.saturating_add(
frac_digit_count
.checked_sub(frac_val.len())
.expect("T::to_string returns an unexpected large string"),
);
while v.len() < term {
v.push(b'0');
}
v.extend_from_slice(frac_val);
}
unsafe { String::from_utf8_unchecked(v) }
}
#[cfg_attr(docsrs, doc(cfg(feature = "rational")))]
#[cfg(feature = "rational")]
pub mod rational;
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
use core::{assert_eq, num::ParseIntError};
use serde_json as _;
#[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!("-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(())
}
#[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::<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 {
FromStrErr::DenominatorIsZero => (),
_ => assert_eq!(false, true),
},
|_| assert_eq!(false, true),
);
Ok(())
}
}