use bigdecimal::BigDecimal;
use num_bigint::{BigInt, Sign};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct W2nError {
pub msg: String,
}
impl W2nError {
fn new(msg: impl Into<String>) -> Self {
W2nError { msg: msg.into() }
}
}
impl fmt::Display for W2nError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.msg)
}
}
impl std::error::Error for W2nError {}
pub type W2nResult<T> = Result<T, W2nError>;
#[derive(Debug, Clone, PartialEq)]
pub enum W2nValue {
Int(BigInt),
Float(f64),
Dec(PyDec),
}
const ND_RUNS: [u32; 65] = [
0x0030, 0x0660, 0x06F0, 0x07C0, 0x0966, 0x09E6, 0x0A66, 0x0AE6, 0x0B66, 0x0BE6, 0x0C66, 0x0CE6,
0x0D66, 0x0DE6, 0x0E50, 0x0ED0, 0x0F20, 0x1040, 0x1090, 0x17E0, 0x1810, 0x1946, 0x19D0, 0x1A80,
0x1A90, 0x1B50, 0x1BB0, 0x1C40, 0x1C50, 0xA620, 0xA8D0, 0xA900, 0xA9D0, 0xA9F0, 0xAA50, 0xABF0,
0xFF10, 0x104A0, 0x10D30, 0x11066, 0x110F0, 0x11136, 0x111D0, 0x112F0, 0x11450, 0x114D0,
0x11650, 0x116C0, 0x11730, 0x118E0, 0x11950, 0x11C50, 0x11D50, 0x11DA0, 0x16A60, 0x16B50,
0x1D7CE, 0x1D7D8, 0x1D7E2, 0x1D7EC, 0x1D7F6, 0x1E140, 0x1E2F0, 0x1E950, 0x1FBF0,
];
fn nd_value(c: char) -> Option<u32> {
let cp = c as u32;
ND_RUNS
.iter()
.find(|&&start| cp >= start && cp < start + 10)
.map(|&start| cp - start)
}
fn nd_to_ascii(s: &str) -> String {
s.chars()
.map(|c| match nd_value(c) {
Some(v) => char::from_digit(v, 10).unwrap_or(c),
None => c,
})
.collect()
}
fn is_digits(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| nd_value(c).is_some())
}
fn is_single_digit(s: &str) -> bool {
let mut it = s.chars();
matches!((it.next().map(nd_value), it.next()), (Some(Some(_)), None))
}
fn is_signed_int(s: &str) -> bool {
is_digits(s.strip_prefix('-').unwrap_or(s))
}
fn is_signed_float(s: &str) -> bool {
let body = s.strip_prefix('-').unwrap_or(s);
match body.split_once('.') {
Some((int, frac)) => is_digits(int) && is_digits(frac),
None => false,
}
}
fn py_repr(s: &str) -> String {
let quote = if s.contains('\'') && !s.contains('"') {
'"'
} else {
'\''
};
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c == quote => {
out.push('\\');
out.push(c);
}
c if (c as u32) < 0x20 || (0x7f..0xa0).contains(&(c as u32)) => {
out.push_str(&format!("\\x{:02x}", c as u32));
}
c => out.push(c),
}
}
out.push(quote);
out
}
const PREC: i64 = 28;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PyDec {
neg: bool,
coeff: BigInt,
exp: i64,
}
impl PyDec {
fn from_bigint(v: &BigInt) -> PyDec {
PyDec {
neg: v.sign() == Sign::Minus,
coeff: if v.sign() == Sign::Minus { -v } else { v.clone() },
exp: 0,
}
}
fn zero() -> PyDec {
PyDec {
neg: false,
coeff: BigInt::from(0),
exp: 0,
}
}
fn from_frac_digits(digits: &str) -> Option<PyDec> {
Some(PyDec {
neg: false,
coeff: BigInt::from_str(digits).ok()?,
exp: -(digits.len() as i64),
})
}
fn is_zero(&self) -> bool {
self.coeff.sign() == Sign::NoSign
}
fn int_str(&self) -> String {
self.coeff.to_str_radix(10)
}
fn ndigits(&self) -> i64 {
self.int_str().len() as i64
}
fn fix(self) -> PyDec {
if self.is_zero() {
return self;
}
let int_str = self.int_str();
let len = int_str.len() as i64;
let mut exp_min = len + self.exp - PREC;
if self.exp >= exp_min {
return self; }
let digits = PREC as usize;
let changed = round_half_even(&int_str, digits);
let mut coeff = int_str.get(..digits).unwrap_or("0").to_string();
if coeff.is_empty() {
coeff = "0".to_string(); }
let mut value = BigInt::from_str(&coeff).unwrap_or_else(|_| BigInt::from(0));
if changed > 0 {
value += 1;
if value.to_str_radix(10).len() as i64 > PREC {
value /= 10;
exp_min += 1;
}
}
PyDec {
neg: self.neg,
coeff: value,
exp: exp_min,
}
}
fn rescale(&self, exp: i64) -> PyDec {
if self.is_zero() {
return PyDec {
neg: self.neg,
coeff: BigInt::from(0),
exp,
};
}
if self.exp >= exp {
let pad = (self.exp - exp) as u32;
return PyDec {
neg: self.neg,
coeff: &self.coeff * BigInt::from(10).pow(pad),
exp,
};
}
let int_str = self.int_str();
let len = int_str.len() as i64;
let digits = len + self.exp - exp;
if digits < 0 {
return PyDec {
neg: self.neg,
coeff: BigInt::from(1),
exp,
};
}
let digits = digits as usize;
let changed = round_half_even(&int_str, digits);
let coeff = int_str.get(..digits).unwrap_or("0");
let coeff = if coeff.is_empty() { "0" } else { coeff };
let mut value = BigInt::from_str(coeff).unwrap_or_else(|_| BigInt::from(0));
if changed == 1 {
value += 1;
}
PyDec {
neg: self.neg,
coeff: value,
exp,
}
}
fn add(&self, other: &PyDec) -> PyDec {
let exp = self.exp.min(other.exp);
if self.is_zero() && other.is_zero() {
return PyDec {
neg: self.neg && other.neg,
coeff: BigInt::from(0),
exp,
}
.fix();
}
if self.is_zero() {
let e = exp.max(other.exp - PREC - 1);
return other.rescale(e).fix();
}
if other.is_zero() {
let e = exp.max(self.exp - PREC - 1);
return self.rescale(e).fix();
}
let (op1, op2) = normalize_pair(self, other);
PyDec {
neg: self.neg,
coeff: op1.coeff + op2.coeff,
exp: op1.exp,
}
.fix()
}
fn mul_sign(&self, neg: bool) -> PyDec {
PyDec {
neg: self.neg != neg,
coeff: self.coeff.clone(),
exp: self.exp,
}
.fix()
}
pub fn to_bigdecimal(&self) -> BigDecimal {
let signed = if self.neg { -&self.coeff } else { self.coeff.clone() };
BigDecimal::new(signed, -self.exp)
}
pub fn from_bigdecimal(d: &BigDecimal) -> PyDec {
let (coeff, scale) = d.as_bigint_and_exponent();
let neg = coeff.sign() == Sign::Minus;
let coeff = if neg { -coeff } else { coeff };
PyDec {
neg,
coeff,
exp: -scale,
}
}
}
impl fmt::Display for PyDec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sign = if self.neg { "-" } else { "" };
let int_str = self.int_str(); let n = int_str.len() as i64;
let leftdigits = self.exp + n;
let dotplace = if self.exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat((-dotplace) as usize), int_str),
)
} else if dotplace >= n {
(
format!("{}{}", int_str, "0".repeat((dotplace - n) as usize)),
String::new(),
)
} else {
let cut = dotplace as usize;
(
int_str.get(..cut).unwrap_or("").to_string(),
format!(".{}", int_str.get(cut..).unwrap_or("")),
)
};
let exp = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
write!(f, "{}{}{}{}", sign, intpart, fracpart, exp)
}
}
fn all_zeros(s: &str, i: usize) -> bool {
s.get(i..).unwrap_or("").chars().all(|c| c == '0')
}
fn exact_half(s: &str, i: usize) -> bool {
s.as_bytes().get(i) == Some(&b'5') && all_zeros(s, i + 1)
}
fn round_half_up(s: &str, prec: usize) -> i32 {
match s.as_bytes().get(prec) {
Some(d) if (b'5'..=b'9').contains(d) => 1,
_ if all_zeros(s, prec) => 0,
_ => -1,
}
}
fn round_half_even(s: &str, prec: usize) -> i32 {
let prev_even = prec == 0
|| matches!(
s.as_bytes().get(prec - 1),
Some(b'0') | Some(b'2') | Some(b'4') | Some(b'6') | Some(b'8')
);
if exact_half(s, prec) && prev_even {
-1
} else {
round_half_up(s, prec)
}
}
fn normalize_pair(op1: &PyDec, op2: &PyDec) -> (PyDec, PyDec) {
let swapped = op1.exp < op2.exp;
let (mut tmp, mut other) = if swapped {
(op2.clone(), op1.clone())
} else {
(op1.clone(), op2.clone())
};
let tmp_len = tmp.ndigits();
let other_len = other.ndigits();
let exp = tmp.exp + (-1).min(tmp_len - PREC - 2);
if other_len + other.exp - 1 < exp {
other.coeff = BigInt::from(1);
other.exp = exp;
}
if let Ok(pad) = u32::try_from(tmp.exp - other.exp) {
tmp.coeff *= BigInt::from(10).pow(pad);
}
tmp.exp = other.exp;
if swapped {
(other, tmp)
} else {
(tmp, other)
}
}
const UNITS: [(&str, i64); 23] = [
("zero", 0),
("oh", 0),
("nought", 0),
("naught", 0),
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
("ten", 10),
("eleven", 11),
("twelve", 12),
("thirteen", 13),
("fourteen", 14),
("fifteen", 15),
("sixteen", 16),
("seventeen", 17),
("eighteen", 18),
("nineteen", 19),
];
const TENS: [(&str, i64); 8] = [
("twenty", 20),
("thirty", 30),
("forty", 40),
("fifty", 50),
("sixty", 60),
("seventy", 70),
("eighty", 80),
("ninety", 90),
];
const SCALES: [(&str, u32); 23] = [
("hundred", 2),
("thousand", 3),
("million", 6),
("billion", 9),
("trillion", 12),
("quadrillion", 15),
("quintillion", 18),
("sextillion", 21),
("septillion", 24),
("octillion", 27),
("nonillion", 30),
("decillion", 33),
("undecillion", 36),
("duodecillion", 39),
("tredecillion", 42),
("quattuordecillion", 45),
("quindecillion", 48),
("sexdecillion", 51),
("septendecillion", 54),
("octodecillion", 57),
("novemdecillion", 60),
("vigintillion", 63),
("centillion", 303),
];
const ORDINAL_TO_CARDINAL: [(&str, &str); 40] = [
("zeroth", "zero"),
("first", "one"),
("second", "two"),
("third", "three"),
("fourth", "four"),
("fifth", "five"),
("sixth", "six"),
("seventh", "seven"),
("eighth", "eight"),
("ninth", "nine"),
("tenth", "ten"),
("eleventh", "eleven"),
("twelfth", "twelve"),
("thirteenth", "thirteen"),
("fourteenth", "fourteen"),
("fifteenth", "fifteen"),
("sixteenth", "sixteen"),
("seventeenth", "seventeen"),
("eighteenth", "eighteen"),
("nineteenth", "nineteen"),
("twentieth", "twenty"),
("thirtieth", "thirty"),
("fortieth", "forty"),
("fiftieth", "fifty"),
("sixtieth", "sixty"),
("seventieth", "seventy"),
("eightieth", "eighty"),
("ninetieth", "ninety"),
("hundredth", "hundred"),
("thousandth", "thousand"),
("millionth", "million"),
("billionth", "billion"),
("trillionth", "trillion"),
("quadrillionth", "quadrillion"),
("quintillionth", "quintillion"),
("sextillionth", "sextillion"),
("septillionth", "septillion"),
("octillionth", "octillion"),
("nonillionth", "nonillion"),
("decillionth", "decillion"),
];
const DECIMAL_WORDS: [&str; 2] = ["point", "dot"];
const NEGATIVE_WORDS: [&str; 2] = ["minus", "negative"];
const AND_WORDS: [&str; 1] = ["and"];
const FILLER: [&str; 2] = ["a", "an"];
pub struct W2nLangEn {
units: HashMap<&'static str, i64>,
tens: HashMap<&'static str, i64>,
scales: HashMap<&'static str, BigInt>,
ordinal_to_cardinal: HashMap<&'static str, &'static str>,
decimal_words: HashSet<&'static str>,
negative_words: HashSet<&'static str>,
and_words: HashSet<&'static str>,
filler: HashSet<&'static str>,
}
impl Default for W2nLangEn {
fn default() -> Self {
Self::new()
}
}
impl W2nLangEn {
pub const LANG: &'static str = "en";
pub const NEGATIVE_WORDS: [&'static str; 2] = NEGATIVE_WORDS;
pub fn new() -> Self {
W2nLangEn {
units: UNITS.iter().copied().collect(),
tens: TENS.iter().copied().collect(),
scales: SCALES
.iter()
.map(|&(w, p)| (w, BigInt::from(10).pow(p)))
.collect(),
ordinal_to_cardinal: ORDINAL_TO_CARDINAL.iter().copied().collect(),
decimal_words: DECIMAL_WORDS.iter().copied().collect(),
negative_words: NEGATIVE_WORDS.iter().copied().collect(),
and_words: AND_WORDS.iter().copied().collect(),
filler: FILLER.iter().copied().collect(),
}
}
pub fn to_cardinal(&self, text: &str) -> W2nResult<W2nValue> {
self.parse(text, false, false)
}
pub fn to_ordinal(&self, text: &str) -> W2nResult<W2nValue> {
self.parse(text, true, false)
}
pub fn to_year(&self, text: &str) -> W2nResult<W2nValue> {
self.parse(text, false, true)
}
pub fn parse(&self, text: &str, ordinal: bool, year_mode: bool) -> W2nResult<W2nValue> {
let norm = normalize(text);
if norm.is_empty() {
return Err(W2nError::new("empty input"));
}
if is_signed_int(&norm) {
let ascii = nd_to_ascii(&norm);
return match BigInt::from_str(&ascii) {
Ok(v) => Ok(W2nValue::Int(v)),
Err(e) => Err(W2nError::new(e.to_string())),
};
}
if is_signed_float(&norm) {
let ascii = nd_to_ascii(&norm);
return match f64::from_str(&ascii) {
Ok(v) => Ok(W2nValue::Float(v)),
Err(e) => Err(W2nError::new(e.to_string())),
};
}
let mut toks: Vec<&str> = norm.split_whitespace().collect();
let mut sign = 1i32;
if let Some(first) = toks.first() {
if self.negative_words.contains(*first) {
sign = -1;
toks.remove(0);
}
}
if toks.is_empty() {
return Err(W2nError::new("empty input after sign"));
}
toks.retain(|t| !self.and_words.contains(*t) && !self.filler.contains(*t));
if toks.is_empty() {
return Err(W2nError::new("empty input after filler removal"));
}
let last = *toks.last().unwrap_or(&"");
let was_ordinal = self.ordinal_to_cardinal.contains_key(last);
if was_ordinal {
let n = toks.len();
toks[n - 1] = self.ordinal_to_cardinal[last];
}
if ordinal && !was_ordinal {
}
let decimal_idx = toks.iter().position(|t| self.decimal_words.contains(*t));
if let Some(idx) = decimal_idx {
let int_toks = &toks[..idx];
let frac_toks = &toks[idx + 1..];
let int_part = if int_toks.is_empty() {
BigInt::from(0)
} else {
self.cardinal_value(int_toks)?
};
let frac_part = self.fractional_value(frac_toks)?;
let sum = PyDec::from_bigint(&int_part).add(&frac_part);
return Ok(W2nValue::Dec(sum.mul_sign(sign < 0)));
}
if year_mode && self.looks_like_year(&toks) {
return Ok(W2nValue::Int(self.year_value(&toks)? * sign));
}
Ok(W2nValue::Int(self.cardinal_value(&toks)? * sign))
}
fn cardinal_value(&self, toks: &[&str]) -> W2nResult<BigInt> {
if toks.is_empty() {
return Err(W2nError::new("empty token list"));
}
let mut total = BigInt::from(0);
let mut current = BigInt::from(0);
let mut seen_any = false;
for tok in toks {
if let Some(&v) = self.units.get(*tok) {
current += v;
seen_any = true;
} else if let Some(&v) = self.tens.get(*tok) {
current += v;
seen_any = true;
} else if *tok == "hundred" {
if current.sign() == Sign::NoSign {
current = BigInt::from(1);
}
current *= 100;
seen_any = true;
} else if let Some(scale) = self.scales.get(*tok) {
if current.sign() == Sign::NoSign {
current = BigInt::from(1);
}
total += ¤t * scale;
current = BigInt::from(0);
seen_any = true;
} else if is_digits(tok) {
match BigInt::from_str(&nd_to_ascii(tok)) {
Ok(v) => current += v,
Err(e) => return Err(W2nError::new(e.to_string())),
}
seen_any = true;
} else {
return Err(W2nError::new(format!(
"unrecognized token {} in {}",
py_repr(tok),
py_repr(&toks.join(" "))
)));
}
}
if !seen_any {
return Err(W2nError::new("no number tokens in input"));
}
Ok(total + current)
}
fn fractional_value(&self, toks: &[&str]) -> W2nResult<PyDec> {
if toks.is_empty() {
return Ok(PyDec::zero());
}
let mut digits = String::with_capacity(toks.len());
for tok in toks {
match self.units.get(*tok) {
Some(&v) if v < 10 => digits.push_str(&v.to_string()),
_ if is_single_digit(tok) => digits.push_str(&nd_to_ascii(tok)),
_ => {
return Err(W2nError::new(format!(
"unrecognized fractional token {}",
py_repr(tok)
)))
}
}
}
PyDec::from_frac_digits(&digits)
.ok_or_else(|| W2nError::new(format!("invalid fractional digits {}", py_repr(&digits))))
}
fn looks_like_year(&self, toks: &[&str]) -> bool {
(2..=5).contains(&toks.len())
&& !toks.contains(&"hundred")
&& !toks.contains(&"thousand")
}
fn year_value(&self, toks: &[&str]) -> W2nResult<BigInt> {
if toks.len() < 2 {
return self.cardinal_value(toks);
}
for split in 1..toks.len() {
let (high, low) = match (
self.cardinal_value(&toks[..split]),
self.cardinal_value(&toks[split..]),
) {
(Ok(h), Ok(l)) => (h, l),
_ => continue, };
let in_high = high >= BigInt::from(10) && high <= BigInt::from(99);
let in_low = low >= BigInt::from(0) && low <= BigInt::from(99);
if in_high && in_low {
return Ok(high * 100 + low);
}
}
self.cardinal_value(toks)
}
}
fn normalize(text: &str) -> String {
crate::normalize(text)
}