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
25const LOOKUP_LO: i64 = -1;
27const LOOKUP_HI: i64 = 10001;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum LookupError {
32 NotImplemented(String),
35}
36
37pub fn normalize_tail(decomposed: &str) -> String {
48 let lowered = decomposed.to_lowercase().replace('_', " ");
49 let chars: Vec<char> = lowered.chars().collect();
50 let mut out = String::with_capacity(lowered.len());
51
52 for (i, &c) in chars.iter().enumerate() {
53 match c {
54 '-' => {
57 let prev_alpha = i > 0 && chars[i - 1].is_ascii_lowercase();
58 let next_alpha = chars.get(i + 1).is_some_and(|n| n.is_ascii_lowercase());
59 out.push(if prev_alpha && next_alpha { ' ' } else { '-' });
60 }
61 ',' | ';' | ':' | '!' | '?' | '"' | '\'' => out.push(' '),
62 '.' => {
64 let next_digit = chars.get(i + 1).is_some_and(|n| n.is_ascii_digit());
65 out.push(if next_digit { '.' } else { ' ' });
66 }
67 _ => out.push(c),
68 }
69 }
70 out.split_whitespace().collect::<Vec<_>>().join(" ")
72}
73
74pub fn normalize(s: &str) -> String {
87 use unicode_normalization::UnicodeNormalization;
88 let nfkd: String = s.nfkd().collect();
89 let stripped: String = nfkd
90 .chars()
91 .filter(|&c| !unicode_normalization::char::is_combining_mark(c))
92 .collect();
93 normalize_tail(&stripped)
94}
95
96type ReverseTable = HashMap<String, i64>;
98type TableCache = RwLock<HashMap<(String, bool), ReverseTable>>;
100
101fn tables() -> &'static TableCache {
103 static T: OnceLock<TableCache> = OnceLock::new();
104 T.get_or_init(|| RwLock::new(HashMap::new()))
105}
106
107fn build_table(lang: &str, ordinal: bool) -> Result<HashMap<String, i64>, LookupError> {
114 let l = num2words2_core::get_lang_by_key(lang)
115 .ok_or_else(|| LookupError::NotImplemented(lang.to_string()))?;
116 let mut table = HashMap::new();
117 for n in LOOKUP_LO..LOOKUP_HI {
118 let v = BigInt::from(n);
119 let words = if ordinal { l.to_ordinal(&v) } else { l.to_cardinal(&v) };
120 let Ok(words) = words else { continue }; let key = normalize(&words);
122 table.entry(key).or_insert(n);
123 }
124 Ok(table)
125}
126
127pub fn lookup(
132 lang: &str,
133 text: &str,
134 ordinal: bool,
135 negative_words: &[String],
136) -> Result<Option<i64>, LookupError> {
137 let mut normalized = normalize(text);
138 if normalized.is_empty() {
139 return Ok(None);
140 }
141
142 let mut sign = 1i64;
143 for neg in negative_words {
144 if normalized == *neg {
145 return Ok(None); }
147 if let Some(rest) = normalized.strip_prefix(&format!("{} ", neg)) {
148 sign = -1;
149 normalized = rest.to_string();
150 break;
151 }
152 }
153
154 let key = (lang.to_string(), ordinal);
155 {
156 let t = tables().read().unwrap();
157 if let Some(tab) = t.get(&key) {
158 return Ok(tab.get(&normalized).map(|v| sign * v));
159 }
160 }
161 let built = build_table(lang, ordinal)?;
162 let got = built.get(&normalized).map(|v| sign * v);
163 tables().write().unwrap().insert(key, built);
164 Ok(got)
165}
166
167pub fn supported_langs() -> Vec<&'static str> {
169 num2words2_core::supported_lang_keys()
170}
171
172pub fn parse_int(s: &str) -> Result<i64, std::num::ParseIntError> {
174 s.parse::<i64>()
175}
176
177pub fn en_to_cardinal(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
183 w2n_lang_en::W2nLangEn::new().to_cardinal(text)
184}
185
186pub fn en_to_ordinal(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
188 w2n_lang_en::W2nLangEn::new().to_ordinal(text)
189}
190
191pub fn en_to_year(text: &str) -> Result<w2n_lang_en::W2nValue, w2n_lang_en::W2nError> {
193 w2n_lang_en::W2nLangEn::new().to_year(text)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn normalize_matches_python_shape() {
202 assert_eq!(normalize("Forty-Two"), "forty two");
204 assert_eq!(normalize(" a_b "), "a b");
205 assert_eq!(normalize("hello."), "hello");
206 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"); }
212
213 #[test]
214 fn en_entry_points() {
215 assert_eq!(
216 en_to_cardinal("forty-two").unwrap(),
217 w2n_lang_en::W2nValue::Int(BigInt::from(42))
218 );
219 assert_eq!(
220 en_to_ordinal("twenty-first").unwrap(),
221 w2n_lang_en::W2nValue::Int(BigInt::from(21))
222 );
223 assert_eq!(
224 en_to_year("nineteen ninety nine").unwrap(),
225 w2n_lang_en::W2nValue::Int(BigInt::from(1999))
226 );
227 assert!(en_to_cardinal("forty zoot").is_err());
228 }
229
230 #[test]
231 fn supported_langs_nonempty() {
232 let langs = supported_langs();
233 assert!(langs.contains(&"en"));
234 assert!(langs.contains(&"fr"));
235 assert!(langs.len() >= 100);
236 }
237}