num_rational_parse/
lib.rs1use num_integer::Integer;
24use num_rational::Ratio;
25use num_traits::{CheckedAdd, CheckedMul, FromPrimitive, Signed};
26use regex::Regex;
27use std::str::FromStr;
28
29#[derive(Copy, Clone, Debug, PartialEq)]
31pub struct ParseRatioError {
32 kind: RatioErrorKind,
33}
34
35impl ParseRatioError {
36 pub fn kind(&self) -> &RatioErrorKind {
38 &self.kind
39 }
40}
41
42impl std::fmt::Display for ParseRatioError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 self.kind.description().fmt(f)
45 }
46}
47
48#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum RatioErrorKind {
52 ParseError,
57 ZeroDenominator,
61 Overflow,
66}
67
68impl RatioErrorKind {
69 fn description(&self) -> &'static str {
70 match *self {
71 RatioErrorKind::ParseError => "failed to parse integer",
72 RatioErrorKind::ZeroDenominator => "zero value denominator",
73 RatioErrorKind::Overflow => "overflow",
74 }
75 }
76}
77
78impl std::fmt::Display for RatioErrorKind {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 self.description().fmt(f)
81 }
82}
83
84pub trait RationalParse: Sized {
92 fn from_str_flex(s: &str) -> Result<Self, ParseRatioError>;
105}
106
107use std::sync::LazyLock;
108
109static RATIONAL_FORMAT: LazyLock<Regex> = LazyLock::new(|| {
121 Regex::new(
122 r"(?xi) # Case-insensitive, verbose mode
123 \A\s* # optional whitespace at the start,
124 (?P<sign>[-+]?) # an optional sign, then
125 (?P<num>\d*|\d+(_\d+)*) # numerator (possibly empty)
126 (?: # followed by
127 (?:\s*/\s*(?P<denom>\d+(_\d+)*))? # an optional denominator
128 | # or
129 (?:\.(?P<decimal>\d*|\d+(_\d+)*))? # an optional fractional part
130 (?:E(?P<exp>[-+]?\d+(_\d+)*))? # and optional exponent
131 )
132 \s*\z # and optional whitespace to finish
133 ",
134 )
135 .unwrap()
136});
137
138impl<T> RationalParse for Ratio<T>
139where
140 T: Clone + Integer + Signed + FromStr + CheckedMul + CheckedAdd + FromPrimitive,
141 <T as FromStr>::Err: std::fmt::Display,
142{
143 fn from_str_flex(input: &str) -> Result<Self, ParseRatioError> {
144 let cap = RATIONAL_FORMAT.captures(input).ok_or(ParseRatioError {
145 kind: RatioErrorKind::ParseError,
146 })?;
147
148 let sign_str = cap.name("sign").map(|m| m.as_str()).unwrap_or("");
149 let num_str = cap.name("num").map(|m| m.as_str()).unwrap_or("");
150 let denom_str = cap.name("denom").map(|m| m.as_str());
151 let decimal_str = cap.name("decimal").map(|m| m.as_str());
152 let exp_str = cap.name("exp").map(|m| m.as_str());
153
154 let num_has_digits = !num_str.is_empty();
156 let decimal_has_digits = decimal_str.is_some_and(|s| !s.is_empty());
157
158 if !num_has_digits && !decimal_has_digits {
159 return Err(ParseRatioError {
160 kind: RatioErrorKind::ParseError,
161 });
162 }
163
164 let parse_val = |s: &str| -> Result<T, ParseRatioError> {
165 if s.is_empty() {
166 return Ok(T::zero());
167 }
168 if s.contains('_') {
169 let s_clean = s.replace('_', "");
170 T::from_str(&s_clean).map_err(|_| ParseRatioError {
171 kind: RatioErrorKind::Overflow,
172 })
173 } else {
174 T::from_str(s).map_err(|_| ParseRatioError {
175 kind: RatioErrorKind::Overflow,
176 })
177 }
178 };
179
180 let ten = T::from_u8(10).ok_or(ParseRatioError {
181 kind: RatioErrorKind::ParseError,
182 })?;
183
184 let checked_pow = |base: &T, exp: u32| -> Result<T, ParseRatioError> {
185 num_traits::checked_pow(base.clone(), exp as usize).ok_or(ParseRatioError {
186 kind: RatioErrorKind::Overflow,
187 })
188 };
189
190 let mut numerator: T = parse_val(num_str)?;
191 let mut denominator: T;
192
193 if let Some(d_str) = denom_str {
194 denominator = parse_val(d_str)?;
195 } else {
196 denominator = T::one();
197 if let Some(dec) = decimal_str {
198 let dec_trimmed = dec.trim_end_matches('0');
201 let dec_clean_owned: String;
202 let dec_final = if dec_trimmed.contains('_') {
203 dec_clean_owned = dec_trimmed.replace('_', "");
204 &dec_clean_owned
205 } else {
206 dec_trimmed
207 };
208
209 let scale = checked_pow(&ten, dec_final.len() as u32)?;
211
212 let dec_val = if dec_final.is_empty() {
213 T::zero()
214 } else {
215 T::from_str(dec_final).map_err(|_| ParseRatioError {
216 kind: RatioErrorKind::Overflow,
217 })?
218 };
219
220 numerator = numerator
221 .checked_mul(&scale)
222 .ok_or(ParseRatioError {
223 kind: RatioErrorKind::Overflow,
224 })?
225 .checked_add(&dec_val)
226 .ok_or(ParseRatioError {
227 kind: RatioErrorKind::Overflow,
228 })?;
229
230 denominator = denominator.checked_mul(&scale).ok_or(ParseRatioError {
231 kind: RatioErrorKind::Overflow,
232 })?;
233 }
234 if let Some(exp_s) = exp_str {
235 let exp_clean_owned: String;
236 let exp_final = if exp_s.contains('_') {
237 exp_clean_owned = exp_s.replace('_', "");
238 &exp_clean_owned
239 } else {
240 exp_s
241 };
242 let exp_val = exp_final.parse::<i32>().map_err(|_| ParseRatioError {
243 kind: RatioErrorKind::ParseError,
244 })?;
245
246 let abs_exp = exp_val.unsigned_abs();
247 let scale = checked_pow(&ten, abs_exp)?;
248
249 if exp_val >= 0 {
250 numerator = numerator.checked_mul(&scale).ok_or(ParseRatioError {
251 kind: RatioErrorKind::Overflow,
252 })?;
253 } else {
254 denominator = denominator.checked_mul(&scale).ok_or(ParseRatioError {
255 kind: RatioErrorKind::Overflow,
256 })?;
257 }
258 }
259 }
260
261 if sign_str == "-" {
262 numerator = -numerator;
263 }
264
265 if denominator.is_zero() {
266 return Err(ParseRatioError {
267 kind: RatioErrorKind::ZeroDenominator,
268 });
269 }
270
271 Ok(Ratio::new(numerator, denominator))
272 }
273}