lib.rs (13020B)
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(docsrs, feature(doc_cfg))] 6 #![no_std] 7 extern crate alloc; 8 use crate::FromDecStrErr::{IntParseErr, TooFewFractionalDigits, TooManyFractionalDigits}; 9 use alloc::{ 10 string::{String, ToString as _}, 11 vec::Vec, 12 }; 13 use core::{ 14 fmt::{self, Debug, Display, Formatter}, 15 ops::Mul, 16 str::FromStr, 17 }; 18 use num_integer::Integer; 19 use num_rational::Ratio; 20 use num_traits::Pow; 21 /// An ordered pair whose first value is <= to the second. 22 #[derive(Clone, Copy, Debug)] 23 pub struct MinMax<T> { 24 /// The first value which is <= the second. 25 min: T, 26 /// The second value which is >= the first. 27 max: T, 28 } 29 impl<T> MinMax<T> { 30 /// Returns a reference to the first value. 31 #[inline] 32 pub const fn min(&self) -> &T { 33 &self.min 34 } 35 /// Returns a reference to the second value. 36 #[inline] 37 pub const fn max(&self) -> &T { 38 &self.max 39 } 40 } 41 impl<T> MinMax<T> 42 where 43 T: PartialOrd<T>, 44 { 45 /// Returns `Some(T)` iff `min` `<=` `max`. 46 #[inline] 47 pub fn new(min: T, max: T) -> Option<Self> { 48 (min <= max).then_some(Self { min, max }) 49 } 50 /// Returns `MinMax` without verifying `min` `<=` `max`. 51 /// 52 /// # Safety 53 /// 54 /// `min` `<=` `max`. 55 #[expect( 56 unsafe_code, 57 reason = "want to expose a function that does not uphold the invariants" 58 )] 59 #[inline] 60 pub const unsafe fn new_unchecked(min: T, max: T) -> Self { 61 Self { min, max } 62 } 63 } 64 /// The error returned when parsing a string in decimal notation into 65 /// a `num_rational::Ratio<T>`. 66 pub enum FromDecStrErr<T> { 67 /// Contains the error returned when parsing a string into a `T`. 68 IntParseErr(T), 69 /// The variant returned when a decimal string has fewer rational 70 /// digits than allowed. 71 TooFewFractionalDigits(usize), 72 /// The variant returned when a decimal string has more rational 73 /// digits than allowed. 74 TooManyFractionalDigits(usize), 75 } 76 impl<T> Display for FromDecStrErr<T> 77 where 78 T: Display, 79 { 80 #[inline] 81 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 82 match *self { 83 IntParseErr(ref x) => x.fmt(f), 84 TooFewFractionalDigits(ref x) => write!( 85 f, 86 "There were only {x} fractional digits which is fewer than the minimum required." 87 ), 88 TooManyFractionalDigits(ref x) => write!( 89 f, 90 "There were {x} fractional digits which is more than the maximum required." 91 ), 92 } 93 } 94 } 95 impl<T> Debug for FromDecStrErr<T> 96 where 97 T: Display, 98 { 99 #[inline] 100 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 101 <Self as Display>::fmt(self, f) 102 } 103 } 104 impl<T> From<T> for FromDecStrErr<T> { 105 #[inline] 106 fn from(x: T) -> Self { 107 IntParseErr(x) 108 } 109 } 110 /// Converts a string in decimal notation into a `Ratio<T>`. 111 /// 112 /// # Panics 113 /// 114 /// May `panic` if `T` implements arithmetic in a way where `panic`s occur on overflow or underflow. 115 /// 116 /// # Errors 117 /// 118 /// Will return `FromDecStrErr` iff `val` is not a valid rational number in decimal notation with number of 119 /// fractional digits inclusively between `frac_digit_count.min()` and `frac_digit_count.max()`. 120 #[expect( 121 clippy::arithmetic_side_effects, 122 reason = "calling code's responsibility to ensure T implements arithmetic correctly" 123 )] 124 #[inline] 125 pub fn try_from_dec_str<T>( 126 val: &str, 127 frac_digit_count: &MinMax<usize>, 128 ) -> Result<Ratio<T>, FromDecStrErr<<T as FromStr>::Err>> 129 where 130 T: Clone 131 + From<u8> 132 + FromStr 133 + Integer 134 + for<'a> Mul<&'a T, Output = T> 135 + Pow<usize, Output = T>, 136 { 137 val.split_once('.').map_or_else( 138 || { 139 if *frac_digit_count.min() == 0 { 140 Ok(Ratio::from(T::from_str(val)?)) 141 } else { 142 Err(TooFewFractionalDigits(val.len())) 143 } 144 }, 145 |(l, r)| { 146 if r.len() >= *frac_digit_count.min() { 147 if r.len() <= *frac_digit_count.max() { 148 let mult = T::from(10).pow(r.len()); 149 let numer = T::from_str(l)? * &mult; 150 let addend = T::from_str(r)?; 151 let zero = T::from(0); 152 Ok(Ratio::new( 153 if numer < zero 154 || (numer == zero 155 && l.as_bytes().first().is_some_and(|fst| *fst == b'-')) 156 { 157 numer - addend 158 } else { 159 numer + addend 160 }, 161 mult, 162 )) 163 } else { 164 Err(TooManyFractionalDigits(r.len())) 165 } 166 } else { 167 Err(TooFewFractionalDigits(r.len())) 168 } 169 }, 170 ) 171 } 172 /// The error returned when parsing a string in decimal or 173 /// rational notation into a `num_rational::Ratio<T>`. 174 pub enum FromStrErr<T> { 175 /// Contains the error when a string fails to parse into a `T`. 176 IntParseErr(T), 177 /// The variant that is returned when a string in rational 178 /// notation has a denominator that is zero. 179 DenominatorIsZero, 180 } 181 impl<T> Display for FromStrErr<T> 182 where 183 T: Display, 184 { 185 #[inline] 186 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 187 match *self { 188 Self::IntParseErr(ref x) => x.fmt(f), 189 Self::DenominatorIsZero => f.write_str("denominator is zero"), 190 } 191 } 192 } 193 impl<T> Debug for FromStrErr<T> 194 where 195 T: Display, 196 { 197 #[inline] 198 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 199 <Self as Display>::fmt(self, f) 200 } 201 } 202 impl<T> From<T> for FromStrErr<T> { 203 #[inline] 204 fn from(x: T) -> Self { 205 Self::IntParseErr(x) 206 } 207 } 208 /// Converts a string in rational or decimal notation into a `Ratio<T>`. 209 /// 210 /// # Panics 211 /// 212 /// May `panic` if `T` implements arithmetic in a way where `panic`s occur on overflow or underflow. 213 /// 214 /// # Errors 215 /// 216 /// Will return `FromStrErr` iff `val` is not a rational number in 217 /// rational or decimal notation. 218 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 219 #[expect( 220 clippy::arithmetic_side_effects, 221 reason = "calling code's responsibility to ensure T implements arithmetic correctly" 222 )] 223 #[inline] 224 pub fn try_from_str<T>(val: &str) -> Result<Ratio<T>, FromStrErr<<T as FromStr>::Err>> 225 where 226 T: Clone 227 + From<u8> 228 + FromStr 229 + Integer 230 + for<'a> Mul<&'a T, Output = T> 231 + Pow<usize, Output = T>, 232 { 233 val.split_once('/').map_or_else( 234 || { 235 try_from_dec_str( 236 val, 237 &MinMax { 238 min: 0, 239 max: usize::MAX, 240 }, 241 ) 242 .map_err(|err| match err { 243 IntParseErr(x) => FromStrErr::IntParseErr(x), 244 TooFewFractionalDigits(_) | TooManyFractionalDigits(_) => unreachable!( 245 "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" 246 ), 247 }) 248 }, 249 |split| { 250 let denom = T::from_str(split.1)?; 251 let zero = T::from(0); 252 if denom == zero { 253 Err(FromStrErr::DenominatorIsZero) 254 } else { 255 split.0.split_once(' ').map_or_else( 256 || Ok(Ratio::new(T::from_str(split.0)?, denom.clone())), 257 |(l2, r2)| { 258 let numer = T::from_str(l2)? * &denom; 259 let addend = T::from_str(r2)?; 260 Ok(Ratio::new( 261 if numer < zero { 262 numer - addend 263 } else { 264 numer + addend 265 }, 266 denom.clone(), 267 )) 268 }, 269 ) 270 } 271 } 272 ) 273 } 274 /// Returns a `String` representing `val` in decimal notation with `frac_digit_count` fractional digits 275 /// using normal rounding rules. 276 /// 277 /// # Panics 278 /// 279 /// May `panic` if `T` implements arithmetic in a way where `panic`s occur on overflow or underflow. 280 #[expect(unsafe_code, reason = "comment justifies correctness")] 281 #[expect( 282 clippy::arithmetic_side_effects, 283 clippy::expect_used, 284 clippy::indexing_slicing, 285 reason = "calling code's responsibility to ensure T implements arithmetic correctly" 286 )] 287 #[inline] 288 pub fn to_dec_string<T>(val: &Ratio<T>, frac_digit_count: usize) -> String 289 where 290 T: Clone + Display + From<u8> + Integer + Pow<usize, Output = T>, 291 { 292 let mult = T::from(10).pow(frac_digit_count); 293 let (int, frac) = (val * &mult).round().numer().div_rem(&mult); 294 let int_str = int.to_string(); 295 let mut v = Vec::with_capacity( 296 int_str 297 .len() 298 .saturating_add(frac_digit_count.saturating_add(2)), 299 ); 300 let zero = T::from(0); 301 if int >= zero && frac < zero { 302 v.push(b'-'); 303 } 304 v.extend_from_slice(int.to_string().as_bytes()); 305 if frac_digit_count > 0 { 306 v.push(b'.'); 307 let len = v.len(); 308 let frac_vec = frac.to_string().into_bytes(); 309 // This cannot `panic` since we start at index 0 when it's empty or does not begin with `b'-'`; 310 // otherwise we start at index 1. 311 let frac_val = &frac_vec[frac_vec 312 .first() 313 .map_or(0, |start| usize::from(*start == b'-'))..]; 314 // We rely on saturating add. If overflow occurs, then the code will `panic` anyway due to 315 // the below loop causing the underlying `Vec` to be too large. 316 let term = len.saturating_add( 317 frac_digit_count 318 .checked_sub(frac_val.len()) 319 .expect("T::to_string returns an unexpected large string"), 320 ); 321 while v.len() < term { 322 v.push(b'0'); 323 } 324 v.extend_from_slice(frac_val); 325 } 326 // SAFETY: 327 // `v` contains precisely the UTF-8 code units returned from `String`s 328 // returned from `to_string` 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 /// Enables deserialization of strings in decimal or fractional format into [`rational::Rational<T>`]. 333 #[cfg_attr(docsrs, doc(cfg(feature = "rational")))] 334 #[cfg(feature = "rational")] 335 pub mod rational; 336 337 #[cfg(test)] 338 mod tests { 339 use super::*; 340 use alloc::format; 341 use core::{assert_eq, num::ParseIntError}; 342 use serde_json as _; 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 }