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