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