use num_bigint::BigInt;
use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};
pub mod w2n_formats;
pub mod w2n_lang_en;
pub mod w2n_sentence;
const LOOKUP_LO: i64 = -1;
const LOOKUP_HI: i64 = 10001;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LookupError {
NotImplemented(String),
}
pub fn normalize_tail(decomposed: &str) -> String {
let lowered = decomposed.to_lowercase().replace('_', " ");
let chars: Vec<char> = lowered.chars().collect();
let mut out = String::with_capacity(lowered.len());
for (i, &c) in chars.iter().enumerate() {
match c {
'-' => {
let prev_alpha = i > 0 && chars[i - 1].is_ascii_lowercase();
let next_alpha = chars.get(i + 1).is_some_and(|n| n.is_ascii_lowercase());
out.push(if prev_alpha && next_alpha { ' ' } else { '-' });
}
',' | ';' | ':' | '!' | '?' | '"' | '\'' => out.push(' '),
'.' => {
let next_digit = chars.get(i + 1).is_some_and(|n| n.is_ascii_digit());
out.push(if next_digit { '.' } else { ' ' });
}
_ => out.push(c),
}
}
out.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub fn normalize(s: &str) -> String {
use unicode_normalization::UnicodeNormalization;
let nfkd: String = s.nfkd().collect();
let stripped: String = nfkd
.chars()
.filter(|&c| !unicode_normalization::char::is_combining_mark(c))
.collect();
normalize_tail(&stripped)
}
type ReverseTable = HashMap<String, i64>;
type TableCache = RwLock<HashMap<(String, bool), ReverseTable>>;
fn tables() -> &'static TableCache {
static T: OnceLock<TableCache> = OnceLock::new();
T.get_or_init(|| RwLock::new(HashMap::new()))
}
fn build_table(lang: &str, ordinal: bool) -> Result<HashMap<String, i64>, LookupError> {
let l = num2words2_core::get_lang_by_key(lang)
.ok_or_else(|| LookupError::NotImplemented(lang.to_string()))?;
let mut table = HashMap::new();
for n in LOOKUP_LO..LOOKUP_HI {
let v = BigInt::from(n);
let words = if ordinal { l.to_ordinal(&v) } else { l.to_cardinal(&v) };
let Ok(words) = words else { continue }; let key = normalize(&words);
table.entry(key).or_insert(n);
}
Ok(table)
}
pub fn lookup(
lang: &str,
text: &str,
ordinal: bool,
negative_words: &[String],
) -> Result<Option<i64>, LookupError> {
let mut normalized = normalize(text);
if normalized.is_empty() {
return Ok(None);
}
let mut sign = 1i64;
for neg in negative_words {
if normalized == *neg {
return Ok(None); }
if let Some(rest) = normalized.strip_prefix(&format!("{} ", neg)) {
sign = -1;
normalized = rest.to_string();
break;
}
}
let key = (lang.to_string(), ordinal);
{
let t = tables().read().unwrap();
if let Some(tab) = t.get(&key) {
return Ok(tab.get(&normalized).map(|v| sign * v));
}
}
let built = build_table(lang, ordinal)?;
let got = built.get(&normalized).map(|v| sign * v);
tables().write().unwrap().insert(key, built);
Ok(got)
}
pub fn supported_langs() -> Vec<&'static str> {
num2words2_core::supported_lang_keys()
}
pub fn parse_int(s: &str) -> Result<i64, std::num::ParseIntError> {
s.parse::<i64>()
}
pub fn en_to_cardinal(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
w2n_lang_en::W2nLangEn::new().to_cardinal(text)
}
pub fn en_to_ordinal(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
w2n_lang_en::W2nLangEn::new().to_ordinal(text)
}
pub fn en_to_year(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
w2n_lang_en::W2nLangEn::new().to_year(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_matches_python_shape() {
assert_eq!(normalize("Forty-Two"), "forty two");
assert_eq!(normalize(" a_b "), "a b");
assert_eq!(normalize("hello."), "hello");
assert_eq!(normalize("3.14"), "3.14"); assert_eq!(normalize("-17"), "-17"); assert_eq!(normalize("tr\u{e9}nte"), "trente"); assert_eq!(normalize("f\u{f3}rty"), "forty"); }
#[test]
fn en_entry_points() {
assert_eq!(
en_to_cardinal("forty-two").unwrap(),
w2n_lang_en::W2nValue::Int(BigInt::from(42))
);
assert_eq!(
en_to_ordinal("twenty-first").unwrap(),
w2n_lang_en::W2nValue::Int(BigInt::from(21))
);
assert_eq!(
en_to_year("nineteen ninety nine").unwrap(),
w2n_lang_en::W2nValue::Int(BigInt::from(1999))
);
assert!(en_to_cardinal("forty zoot").is_err());
}
#[test]
fn supported_langs_nonempty() {
let langs = supported_langs();
assert!(langs.contains(&"en"));
assert!(langs.contains(&"fr"));
assert!(langs.len() >= 100);
}
}