num2words2_core/
strnum.rs1use crate::base::{N2WError, Result};
13use bigdecimal::BigDecimal;
14use num_bigint::BigInt;
15use std::str::FromStr;
16
17#[derive(Debug, Clone)]
20pub enum ParsedNumber {
21 Dec(BigDecimal),
23 Inf { negative: bool },
26 NaN,
29 EsOrdinal { n: BigInt, gender: char },
32 DecPoint {
35 value: BigDecimal,
36 pointword: &'static str,
37 },
38}
39
40fn invalid() -> N2WError {
41 N2WError::Custom {
44 module: "decimal",
45 class: "InvalidOperation",
46 msg: "[<class 'decimal.ConversionSyntax'>]".into(),
47 }
48}
49
50pub fn unicode_digit(ch: char) -> Option<u32> {
55 if ch.is_ascii_digit() {
56 return Some(ch as u32 - '0' as u32);
57 }
58 let c = ch as u32;
59 for &start in &[
60 0x0660, 0x06F0, 0x0966, 0x09E6, 0x0A66, 0x0AE6, 0x0B66, 0x0BE6, 0x0C66, 0x0CE6, 0x0D66, 0x0E50, 0x0ED0, 0x0F20, 0x1040, 0x17E0, 0x1810, 0xFF10, ] {
79 if (start..start + 10).contains(&c) {
80 return Some(c - start);
81 }
82 }
83 None
84}
85
86pub fn has_py_digit(s: &str) -> bool {
91 s.chars()
92 .any(|c| unicode_digit(c).is_some() || c.is_numeric())
93}
94
95pub fn python_decimal_parse(s: &str) -> Result<ParsedNumber> {
103 let t = s.trim();
104 if t.is_empty() {
105 return Err(invalid());
106 }
107 let lower = t.to_lowercase();
108 let (sign_neg, rest) = match lower.strip_prefix('-') {
109 Some(r) => (true, r),
110 None => (false, lower.strip_prefix('+').unwrap_or(&lower)),
111 };
112 if rest == "inf" || rest == "infinity" {
113 return Ok(ParsedNumber::Inf { negative: sign_neg });
114 }
115 if rest == "nan"
116 || rest == "snan"
117 || (rest.starts_with("nan")
118 && rest[3..].chars().all(|c| c.is_ascii_digit())
119 && !rest[3..].is_empty())
120 || (rest.starts_with("snan")
121 && rest[4..].chars().all(|c| c.is_ascii_digit())
122 && !rest[4..].is_empty())
123 {
124 return Ok(ParsedNumber::NaN);
125 }
126
127 let src = t; let mut out = String::with_capacity(src.len());
131 let mut chars = src.chars().peekable();
132 if let Some(&c) = chars.peek() {
133 if c == '+' || c == '-' {
134 if c == '-' {
135 out.push('-');
136 }
137 chars.next();
138 }
139 }
140 let mut int_digits = 0usize;
141 let mut frac_digits = 0usize;
142 let mut seen_dot = false;
143 let mut exp_part = String::new();
144 let mut prev_was_digit = false;
145 while let Some(c) = chars.next() {
146 if let Some(d) = unicode_digit(c) {
147 out.push(char::from(b'0' + d as u8));
148 if seen_dot {
149 frac_digits += 1;
150 } else {
151 int_digits += 1;
152 }
153 prev_was_digit = true;
154 } else if c == '_' {
155 let next_is_digit = chars.peek().is_some_and(|&n| unicode_digit(n).is_some());
157 if !prev_was_digit || !next_is_digit {
158 return Err(invalid());
159 }
160 prev_was_digit = false;
161 } else if c == '.' {
162 if seen_dot {
163 return Err(invalid());
164 }
165 seen_dot = true;
166 out.push('.');
167 prev_was_digit = false;
168 } else if c == 'e' || c == 'E' {
169 exp_part.push('E');
171 if let Some(&sc) = chars.peek() {
172 if sc == '+' || sc == '-' {
173 exp_part.push(sc);
174 chars.next();
175 }
176 }
177 let mut exp_digits = 0usize;
178 let mut prev_exp_digit = false;
179 for ec in chars.by_ref() {
180 if let Some(d) = unicode_digit(ec) {
181 exp_part.push(char::from(b'0' + d as u8));
182 exp_digits += 1;
183 prev_exp_digit = true;
184 } else if ec == '_' {
185 if !prev_exp_digit {
186 return Err(invalid());
187 }
188 prev_exp_digit = false;
189 } else {
190 return Err(invalid());
191 }
192 }
193 if exp_digits == 0 || !prev_exp_digit {
194 return Err(invalid());
195 }
196 break;
197 } else {
198 return Err(invalid());
199 }
200 }
201 if int_digits == 0 && frac_digits == 0 {
202 return Err(invalid());
203 }
204 if out.ends_with('.') {
206 out.pop();
207 }
208 if out.starts_with('.') {
210 out.insert(0, '0');
211 } else if out.starts_with("-.") {
212 out.insert(1, '0');
213 }
214 out.push_str(&exp_part);
215 BigDecimal::from_str(&out)
216 .map(ParsedNumber::Dec)
217 .map_err(|_| invalid())
218}
219
220pub fn python_int_parse(s: &str) -> Option<BigInt> {
224 let t = s.trim();
225 if t.is_empty() {
226 return None;
227 }
228 let (neg, rest) = match t.strip_prefix('-') {
229 Some(r) => (true, r),
230 None => (false, t.strip_prefix('+').unwrap_or(t)),
231 };
232 if rest.is_empty() {
233 return None;
234 }
235 let mut ascii = String::with_capacity(rest.len());
236 let mut prev_was_digit = false;
237 let mut iter = rest.chars().peekable();
238 while let Some(c) = iter.next() {
239 if let Some(d) = unicode_digit(c) {
240 ascii.push(char::from(b'0' + d as u8));
241 prev_was_digit = true;
242 } else if c == '_' {
243 let next_is_digit = iter.peek().is_some_and(|&n| unicode_digit(n).is_some());
244 if !prev_was_digit || !next_is_digit {
245 return None;
246 }
247 prev_was_digit = false;
248 } else {
249 return None;
250 }
251 }
252 let mut v = BigInt::from_str(&ascii).ok()?;
253 if neg {
254 v = -v;
255 }
256 Some(v)
257}
258
259pub fn python_decimal_str(d: &BigDecimal) -> String {
264 let (mant, scale) = d.as_bigint_and_exponent();
265 let exponent = -scale; let neg = mant.sign() == num_bigint::Sign::Minus;
267 let digits = mant.magnitude().to_string();
268 let ndigits = digits.len() as i64;
269 let adjusted = exponent + ndigits - 1;
270 let sign = if neg { "-" } else { "" };
271
272 if exponent <= 0 && adjusted >= -6 {
273 if exponent == 0 {
275 return format!("{}{}", sign, digits);
276 }
277 let point = ndigits + exponent;
278 if point <= 0 {
279 return format!("{}0.{}{}", sign, "0".repeat((-point) as usize), digits);
280 }
281 let (i, f) = digits.split_at(point as usize);
282 return format!("{}{}.{}", sign, i, f);
283 }
284 let exp = adjusted;
286 let mantissa = if digits.len() == 1 {
287 digits
288 } else {
289 format!("{}.{}", &digits[..1], &digits[1..])
290 };
291 format!(
292 "{}{}E{}{}",
293 sign,
294 mantissa,
295 if exp >= 0 { "+" } else { "" },
296 exp
297 )
298}