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().any(|c| unicode_digit(c).is_some() || c.is_numeric())
92}
93
94pub fn python_decimal_parse(s: &str) -> Result<ParsedNumber> {
102 let t = s.trim();
103 if t.is_empty() {
104 return Err(invalid());
105 }
106 let lower = t.to_lowercase();
107 let (sign_neg, rest) = match lower.strip_prefix('-') {
108 Some(r) => (true, r),
109 None => (false, lower.strip_prefix('+').unwrap_or(&lower)),
110 };
111 if rest == "inf" || rest == "infinity" {
112 return Ok(ParsedNumber::Inf { negative: sign_neg });
113 }
114 if rest == "nan" || rest == "snan"
115 || (rest.starts_with("nan") && rest[3..].chars().all(|c| c.is_ascii_digit()) && !rest[3..].is_empty())
116 || (rest.starts_with("snan") && rest[4..].chars().all(|c| c.is_ascii_digit()) && !rest[4..].is_empty())
117 {
118 return Ok(ParsedNumber::NaN);
119 }
120
121 let src = t; let mut out = String::with_capacity(src.len());
125 let mut chars = src.chars().peekable();
126 if let Some(&c) = chars.peek() {
127 if c == '+' || c == '-' {
128 if c == '-' {
129 out.push('-');
130 }
131 chars.next();
132 }
133 }
134 let mut int_digits = 0usize;
135 let mut frac_digits = 0usize;
136 let mut seen_dot = false;
137 let mut exp_part = String::new();
138 let mut prev_was_digit = false;
139 while let Some(c) = chars.next() {
140 if let Some(d) = unicode_digit(c) {
141 out.push(char::from(b'0' + d as u8));
142 if seen_dot {
143 frac_digits += 1;
144 } else {
145 int_digits += 1;
146 }
147 prev_was_digit = true;
148 } else if c == '_' {
149 let next_is_digit = chars.peek().is_some_and(|&n| unicode_digit(n).is_some());
151 if !prev_was_digit || !next_is_digit {
152 return Err(invalid());
153 }
154 prev_was_digit = false;
155 } else if c == '.' {
156 if seen_dot {
157 return Err(invalid());
158 }
159 seen_dot = true;
160 out.push('.');
161 prev_was_digit = false;
162 } else if c == 'e' || c == 'E' {
163 exp_part.push('E');
165 if let Some(&sc) = chars.peek() {
166 if sc == '+' || sc == '-' {
167 exp_part.push(sc);
168 chars.next();
169 }
170 }
171 let mut exp_digits = 0usize;
172 let mut prev_exp_digit = false;
173 for ec in chars.by_ref() {
174 if let Some(d) = unicode_digit(ec) {
175 exp_part.push(char::from(b'0' + d as u8));
176 exp_digits += 1;
177 prev_exp_digit = true;
178 } else if ec == '_' {
179 if !prev_exp_digit {
180 return Err(invalid());
181 }
182 prev_exp_digit = false;
183 } else {
184 return Err(invalid());
185 }
186 }
187 if exp_digits == 0 || !prev_exp_digit {
188 return Err(invalid());
189 }
190 break;
191 } else {
192 return Err(invalid());
193 }
194 }
195 if int_digits == 0 && frac_digits == 0 {
196 return Err(invalid());
197 }
198 if out.ends_with('.') {
200 out.pop();
201 }
202 if out.starts_with('.') {
204 out.insert(0, '0');
205 } else if out.starts_with("-.") {
206 out.insert(1, '0');
207 }
208 out.push_str(&exp_part);
209 BigDecimal::from_str(&out)
210 .map(ParsedNumber::Dec)
211 .map_err(|_| invalid())
212}
213
214pub fn python_int_parse(s: &str) -> Option<BigInt> {
218 let t = s.trim();
219 if t.is_empty() {
220 return None;
221 }
222 let (neg, rest) = match t.strip_prefix('-') {
223 Some(r) => (true, r),
224 None => (false, t.strip_prefix('+').unwrap_or(t)),
225 };
226 if rest.is_empty() {
227 return None;
228 }
229 let mut ascii = String::with_capacity(rest.len());
230 let mut prev_was_digit = false;
231 let mut iter = rest.chars().peekable();
232 while let Some(c) = iter.next() {
233 if let Some(d) = unicode_digit(c) {
234 ascii.push(char::from(b'0' + d as u8));
235 prev_was_digit = true;
236 } else if c == '_' {
237 let next_is_digit = iter.peek().is_some_and(|&n| unicode_digit(n).is_some());
238 if !prev_was_digit || !next_is_digit {
239 return None;
240 }
241 prev_was_digit = false;
242 } else {
243 return None;
244 }
245 }
246 let mut v = BigInt::from_str(&ascii).ok()?;
247 if neg {
248 v = -v;
249 }
250 Some(v)
251}
252
253pub fn python_decimal_str(d: &BigDecimal) -> String {
258 let (mant, scale) = d.as_bigint_and_exponent();
259 let exponent = -scale; let neg = mant.sign() == num_bigint::Sign::Minus;
261 let digits = mant.magnitude().to_string();
262 let ndigits = digits.len() as i64;
263 let adjusted = exponent + ndigits - 1;
264 let sign = if neg { "-" } else { "" };
265
266 if exponent <= 0 && adjusted >= -6 {
267 if exponent == 0 {
269 return format!("{}{}", sign, digits);
270 }
271 let point = ndigits + exponent;
272 if point <= 0 {
273 return format!("{}0.{}{}", sign, "0".repeat((-point) as usize), digits);
274 }
275 let (i, f) = digits.split_at(point as usize);
276 return format!("{}{}.{}", sign, i, f);
277 }
278 let exp = adjusted;
280 let mantissa = if digits.len() == 1 {
281 digits
282 } else {
283 format!("{}.{}", &digits[..1], &digits[1..])
284 };
285 format!("{}{}E{}{}", sign, mantissa, if exp >= 0 { "+" } else { "" }, exp)
286}