1use num_bigint::BigInt;
18use std::collections::HashMap;
19use std::sync::{OnceLock, RwLock};
20
21pub mod w2n_formats;
22pub mod w2n_lang_en;
23pub mod w2n_sentence;
24
25pub use w2n_sentence::words2num;
29
30const LOOKUP_LO: i64 = -1;
32const LOOKUP_HI: i64 = 10001;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum LookupError {
37 NotImplemented(String),
40}
41
42pub fn normalize_tail(decomposed: &str) -> String {
53 let lowered = decomposed.to_lowercase().replace('_', " ");
54 let chars: Vec<char> = lowered.chars().collect();
55 let mut out = String::with_capacity(lowered.len());
56
57 for (i, &c) in chars.iter().enumerate() {
58 match c {
59 '-' => {
62 let prev_alpha = i > 0 && chars[i - 1].is_ascii_lowercase();
63 let next_alpha = chars.get(i + 1).is_some_and(|n| n.is_ascii_lowercase());
64 out.push(if prev_alpha && next_alpha { ' ' } else { '-' });
65 }
66 ',' | ';' | ':' | '!' | '?' | '"' | '\'' => out.push(' '),
67 '.' => {
69 let next_digit = chars.get(i + 1).is_some_and(|n| n.is_ascii_digit());
70 out.push(if next_digit { '.' } else { ' ' });
71 }
72 _ => out.push(c),
73 }
74 }
75 out.split_whitespace().collect::<Vec<_>>().join(" ")
77}
78
79pub fn normalize(s: &str) -> String {
92 use unicode_normalization::UnicodeNormalization;
93 let nfkd: String = s.nfkd().collect();
94 let stripped: String = nfkd
95 .chars()
96 .filter(|&c| !unicode_normalization::char::is_combining_mark(c))
97 .collect();
98 normalize_tail(&stripped)
99}
100
101type ReverseTable = HashMap<String, i64>;
103type TableCache = RwLock<HashMap<(String, bool), ReverseTable>>;
105
106fn tables() -> &'static TableCache {
108 static T: OnceLock<TableCache> = OnceLock::new();
109 T.get_or_init(|| RwLock::new(HashMap::new()))
110}
111
112fn build_table(lang: &str, ordinal: bool) -> Result<HashMap<String, i64>, LookupError> {
119 let l = num2words2_core::get_lang_by_key(lang)
120 .ok_or_else(|| LookupError::NotImplemented(lang.to_string()))?;
121 let mut table = HashMap::new();
122 for n in LOOKUP_LO..LOOKUP_HI {
123 let v = BigInt::from(n);
124 let words = if ordinal { l.to_ordinal(&v) } else { l.to_cardinal(&v) };
125 let Ok(words) = words else { continue }; let key = normalize(&words);
127 table.entry(key).or_insert(n);
128 }
129 Ok(table)
130}
131
132pub fn lookup(
137 lang: &str,
138 text: &str,
139 ordinal: bool,
140 negative_words: &[String],
141) -> Result<Option<i64>, LookupError> {
142 let mut normalized = normalize(text);
143 if normalized.is_empty() {
144 return Ok(None);
145 }
146
147 let mut sign = 1i64;
148 for neg in negative_words {
149 if normalized == *neg {
150 return Ok(None); }
152 if let Some(rest) = normalized.strip_prefix(&format!("{} ", neg)) {
153 sign = -1;
154 normalized = rest.to_string();
155 break;
156 }
157 }
158
159 let key = (lang.to_string(), ordinal);
160 {
161 let t = tables().read().unwrap();
162 if let Some(tab) = t.get(&key) {
163 return Ok(tab.get(&normalized).map(|v| sign * v));
164 }
165 }
166 let built = build_table(lang, ordinal)?;
167 let got = built.get(&normalized).map(|v| sign * v);
168 tables().write().unwrap().insert(key, built);
169 Ok(got)
170}
171
172pub fn supported_langs() -> Vec<&'static str> {
174 num2words2_core::supported_lang_keys()
175}
176
177pub fn parse_int(s: &str) -> Result<i64, std::num::ParseIntError> {
179 s.parse::<i64>()
180}
181
182pub fn en_to_cardinal(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
188 w2n_lang_en::W2nLangEn::new().to_cardinal(text)
189}
190
191pub fn en_to_ordinal(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
193 w2n_lang_en::W2nLangEn::new().to_ordinal(text)
194}
195
196pub fn en_to_year(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
198 w2n_lang_en::W2nLangEn::new().to_year(text)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn normalize_matches_python_shape() {
207 assert_eq!(normalize("Forty-Two"), "forty two");
209 assert_eq!(normalize(" a_b "), "a b");
210 assert_eq!(normalize("hello."), "hello");
211 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"); }
217
218 #[test]
219 fn en_entry_points() {
220 assert_eq!(
221 en_to_cardinal("forty-two").unwrap(),
222 w2n_lang_en::W2nValue::Int(BigInt::from(42))
223 );
224 assert_eq!(
225 en_to_ordinal("twenty-first").unwrap(),
226 w2n_lang_en::W2nValue::Int(BigInt::from(21))
227 );
228 assert_eq!(
229 en_to_year("nineteen ninety nine").unwrap(),
230 w2n_lang_en::W2nValue::Int(BigInt::from(1999))
231 );
232 assert!(en_to_cardinal("forty zoot").is_err());
233 }
234
235 #[test]
236 fn supported_langs_nonempty() {
237 let langs = supported_langs();
238 assert!(langs.contains(&"en"));
239 assert!(langs.contains(&"fr"));
240 assert!(langs.len() >= 100);
241 }
242}