1use crate::base::{set_low_numwords, set_mid_numwords, Cards, Lang, N2WError, Result};
24use crate::currency::CurrencyForms;
25use crate::floatpath::FloatValue;
26use num_bigint::BigInt;
27use num_traits::{One, Signed, Zero};
28use std::collections::HashMap;
29
30pub fn gen_high_numwords(units: &[&str], tens: &[&str], lows: &[&str]) -> Vec<String> {
35 let mut out: Vec<String> = Vec::new();
36 for t in tens {
38 for u in units {
39 out.push(format!("{}{}", u, t));
40 }
41 }
42 out.reverse();
43
44 const REPLACEMENTS: &[(&str, &str)] = &[
45 ("novemn", "noven"),
46 ("novemo", "novo"),
47 ("octoo", "octo"),
48 ("quintd", "quind"),
49 ("quintn", "quin"),
50 ("quintq", "quinq"),
51 ("quints", "quins"),
52 ("quintt", "quint"),
53 ("quintv", "quinv"),
54 ("septenn", "septen"),
55 ("septent", "sept"),
56 ("sexn", "sen"),
57 ("sexs", "ses"),
58 ("tresd", "tred"),
59 ("tresn", "tren"),
60 ("tress", "tres"),
61 ("tresv", "trev"),
62 ("unno", "uno"),
63 ];
64 for (k, v) in REPLACEMENTS {
65 out = out.iter().map(|o| o.replace(k, v)).collect();
66 }
67 out.extend(lows.iter().map(|s| s.to_string()));
68 out
69}
70
71fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
82 const DOLLARS: [&str; 2] = ["dollar", "dollars"];
83 const CENTS: [&str; 2] = ["cent", "cents"];
84
85 let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
86
87 m.insert("AUD", CurrencyForms::new(&DOLLARS, &CENTS));
89 m.insert("BYN", CurrencyForms::new(&["rouble", "roubles"], &["kopek", "kopeks"]));
90 m.insert("CAD", CurrencyForms::new(&DOLLARS, &CENTS));
91 m.insert("EEK", CurrencyForms::new(&["kroon", "kroons"], &["sent", "senti"]));
93 m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &CENTS));
94 m.insert(
95 "GBP",
96 CurrencyForms::new(&["pound sterling", "pounds sterling"], &["penny", "pence"]),
97 );
98 m.insert("LTL", CurrencyForms::new(&["litas", "litas"], &CENTS));
100 m.insert("LVL", CurrencyForms::new(&["lat", "lats"], &["santim", "santims"]));
101 m.insert("USD", CurrencyForms::new(&DOLLARS, &CENTS));
102 m.insert("RUB", CurrencyForms::new(&["rouble", "roubles"], &["kopek", "kopeks"]));
103 m.insert("SEK", CurrencyForms::new(&["krona", "kronor"], &["öre", "öre"]));
104 m.insert("NOK", CurrencyForms::new(&["krone", "kroner"], &["øre", "øre"]));
105 m.insert(
106 "PLN",
107 CurrencyForms::new(&["zloty", "zlotys", "zlotu"], &["grosz", "groszy"]),
108 );
109 m.insert("MXN", CurrencyForms::new(&["peso", "pesos"], &CENTS));
110 m.insert(
111 "RON",
112 CurrencyForms::new(&["leu", "lei", "de lei"], &["ban", "bani", "de bani"]),
113 );
114 m.insert("INR", CurrencyForms::new(&["rupee", "rupees"], &["paisa", "paise"]));
115 m.insert("HUF", CurrencyForms::new(&["forint", "forint"], &["fillér", "fillér"]));
116 m.insert("ISK", CurrencyForms::new(&["króna", "krónur"], &["aur", "aurar"]));
117 m.insert("UZS", CurrencyForms::new(&["sum", "sums"], &["tiyin", "tiyins"]));
118 m.insert(
119 "SAR",
120 CurrencyForms::new(&["saudi riyal", "saudi riyals"], &["halalah", "halalas"]),
121 );
122 m.insert("JPY", CurrencyForms::new(&["yen", "yen"], &["sen", "sen"]));
123 m.insert("KRW", CurrencyForms::new(&["won", "won"], &["jeon", "jeon"]));
124
125 m.insert("EUR", CurrencyForms::new(&["euro", "euros"], &CENTS));
128 m.insert("USD", CurrencyForms::new(&DOLLARS, &CENTS));
129 m.insert("GBP", CurrencyForms::new(&["pound", "pounds"], &["penny", "pence"]));
130 m.insert("NGN", CurrencyForms::new(&["naira", "naira"], &["kobo", "kobo"]));
131 m.insert("AUD", CurrencyForms::new(&DOLLARS, &CENTS));
133 m.insert("CAD", CurrencyForms::new(&DOLLARS, &CENTS));
134 m.insert("NZD", CurrencyForms::new(&DOLLARS, &CENTS));
135 m.insert("HKD", CurrencyForms::new(&DOLLARS, &CENTS));
136 m.insert("SGD", CurrencyForms::new(&DOLLARS, &CENTS));
137 m.insert("CHF", CurrencyForms::new(&["franc", "francs"], &["rappen", "rappen"]));
138 m.insert("AED", CurrencyForms::new(&["dirham", "dirhams"], &["fils", "fils"]));
139 m.insert("JPY", CurrencyForms::new(&["yen", "yen"], &["sen", "sen"]));
140 m.insert("CNY", CurrencyForms::new(&["yuan", "yuan"], &["fen", "fen"]));
141 m.insert("INR", CurrencyForms::new(&["rupee", "rupees"], &["paisa", "paise"]));
142 m.insert("KRW", CurrencyForms::new(&["won", "won"], &["jeon", "jeon"]));
143 m.insert("MXN", CurrencyForms::new(&["peso", "pesos"], &CENTS));
144 m.insert("BRL", CurrencyForms::new(&["real", "reais"], &CENTS));
145 m.insert("ZAR", CurrencyForms::new(&["rand", "rand"], &CENTS));
146 m.insert("SAR", CurrencyForms::new(&["riyal", "riyals"], &["halalah", "halalas"]));
147 m.insert("QAR", CurrencyForms::new(&["riyal", "riyals"], &["dirham", "dirhams"]));
148 m.insert("KWD", CurrencyForms::new(&["dinar", "dinars"], &["fils", "fils"]));
149 m.insert("BHD", CurrencyForms::new(&["dinar", "dinars"], &["fils", "fils"]));
151 m.insert("OMR", CurrencyForms::new(&["rial", "rials"], &["baisa", "baisa"]));
152 m.insert("JOD", CurrencyForms::new(&["dinar", "dinars"], &["fils", "fils"]));
153 m.insert("TND", CurrencyForms::new(&["dinar", "dinars"], &["millime", "millimes"]));
154 m.insert("LYD", CurrencyForms::new(&["dinar", "dinars"], &["dirham", "dirhams"]));
155 m.insert("IQD", CurrencyForms::new(&["dinar", "dinars"], &["fils", "fils"]));
156
157 m
158}
159
160fn build_currency_adjectives() -> HashMap<&'static str, &'static str> {
162 [
163 ("AUD", "Australian"),
164 ("BYN", "Belarusian"),
165 ("CAD", "Canadian"),
166 ("EEK", "Estonian"),
167 ("USD", "US"),
168 ("RUB", "Russian"),
169 ("NOK", "Norwegian"),
170 ("MXN", "Mexican"),
171 ("RON", "Romanian"),
172 ("INR", "Indian"),
173 ("HUF", "Hungarian"),
174 ("ISK", "íslenskar"),
175 ("UZS", "Uzbekistan"),
176 ("SAR", "Saudi"),
177 ("JPY", "Japanese"),
178 ("KRW", "Korean"),
179 ]
180 .into_iter()
181 .collect()
182}
183
184fn build_currency_precision() -> HashMap<&'static str, i64> {
192 [
193 ("BHD", 1000),
194 ("KWD", 1000),
195 ("OMR", 1000),
196 ("JOD", 1000),
197 ("TND", 1000),
198 ("LYD", 1000),
199 ("IQD", 1000),
200 ]
201 .into_iter()
202 .collect()
203}
204
205fn py_float_str(f: f64) -> String {
211 if f.is_nan() {
212 return "nan".to_string();
213 }
214 if f.is_infinite() {
215 return if f.is_sign_positive() {
216 "inf".to_string()
217 } else {
218 "-inf".to_string()
219 };
220 }
221 let a = f.abs();
222 if a != 0.0 && (a >= 1e16 || a < 1e-4) {
223 let s = format!("{:e}", f);
225 if let Some((m, e)) = s.split_once('e') {
226 let (sign, digits) = match e.strip_prefix('-') {
227 Some(d) => ("-", d.to_string()),
228 None => ("+", e.to_string()),
229 };
230 let digits = if digits.len() < 2 {
231 format!("0{}", digits)
232 } else {
233 digits
234 };
235 return format!("{}e{}{}", m, sign, digits);
236 }
237 s
238 } else if f.fract() == 0.0 {
239 format!("{:.1}", f)
241 } else {
242 format!("{}", f)
243 }
244}
245
246fn py_num_str(v: &FloatValue) -> String {
248 match v {
249 FloatValue::Float { value, .. } => py_float_str(*value),
250 FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
251 }
252}
253
254pub struct LangEn {
255 cards: Cards,
256 maxval: BigInt,
257 ords: HashMap<&'static str, &'static str>,
258 exclude_title: Vec<String>,
259 currency_forms: HashMap<&'static str, CurrencyForms>,
260 currency_adjectives: HashMap<&'static str, &'static str>,
261 currency_precision: HashMap<&'static str, i64>,
262}
263
264impl Default for LangEn {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl LangEn {
271 pub fn new() -> Self {
272 let lows = ["non", "oct", "sept", "sext", "quint", "quadr", "tr", "b", "m"];
273 let units = [
274 "", "un", "duo", "tres", "quattuor", "quint", "sex", "septen", "octo", "novem",
275 ];
276 let tens = [
277 "dec",
278 "vigint",
279 "trigint",
280 "quadragint",
281 "quinquagint",
282 "sexagint",
283 "septuagint",
284 "octogint",
285 "nonagint",
286 ];
287 let mut high = vec!["cent".to_string()];
288 high.extend(gen_high_numwords(&units, &tens, &lows));
289
290 let mut cards = Cards::new();
291
292 let max = 3 + 3 * high.len() as i64;
294 let mut n = max;
295 for word in high.iter() {
296 if n <= 3 {
297 break;
298 }
299 cards.insert(BigInt::from(10u8).pow(n as u32), format!("{}illion", word));
300 n -= 3;
301 }
302
303 set_mid_numwords(
304 &mut cards,
305 &[
306 (1000, "thousand"),
307 (100, "hundred"),
308 (90, "ninety"),
309 (80, "eighty"),
310 (70, "seventy"),
311 (60, "sixty"),
312 (50, "fifty"),
313 (40, "forty"),
314 (30, "thirty"),
315 ],
316 );
317 set_low_numwords(
318 &mut cards,
319 &[
320 "twenty", "nineteen", "eighteen", "seventeen", "sixteen", "fifteen", "fourteen",
321 "thirteen", "twelve", "eleven", "ten", "nine", "eight", "seven", "six", "five",
322 "four", "three", "two", "one", "zero",
323 ],
324 );
325
326 let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
328
329 let ords: HashMap<&str, &str> = [
330 ("one", "first"),
331 ("two", "second"),
332 ("three", "third"),
333 ("four", "fourth"),
334 ("five", "fifth"),
335 ("six", "sixth"),
336 ("seven", "seventh"),
337 ("eight", "eighth"),
338 ("nine", "ninth"),
339 ("ten", "tenth"),
340 ("eleven", "eleventh"),
341 ("twelve", "twelfth"),
342 ("twenty", "twentieth"),
343 ("thirty", "thirtieth"),
344 ("forty", "fortieth"),
345 ("fifty", "fiftieth"),
346 ("sixty", "sixtieth"),
347 ("seventy", "seventieth"),
348 ("eighty", "eightieth"),
349 ("ninety", "ninetieth"),
350 ("hundred", "hundredth"),
351 ("thousand", "thousandth"),
352 ("million", "millionth"),
353 ("billion", "billionth"),
354 ]
355 .into_iter()
356 .collect();
357
358 LangEn {
359 cards,
360 maxval,
361 ords,
362 exclude_title: vec!["and".into(), "point".into(), "minus".into()],
363 currency_forms: build_currency_forms(),
367 currency_adjectives: build_currency_adjectives(),
368 currency_precision: build_currency_precision(),
369 }
370 }
371
372 fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
373 if value.sign() == num_bigint::Sign::Minus {
374 return Err(N2WError::Type(format!(
375 "Cannot treat negative num {} as ordinal.",
376 value
377 )));
378 }
379 Ok(())
380 }
381
382 fn verify_ordinal_float(&self, value: &FloatValue) -> Result<BigInt> {
394 match value.as_whole_int() {
395 Some(i) => {
396 if i.sign() == num_bigint::Sign::Minus {
397 Err(N2WError::Type(format!(
398 "Cannot treat negative num {} as ordinal.",
399 py_num_str(value)
400 )))
401 } else {
402 Ok(i)
403 }
404 }
405 None => Err(N2WError::Type(format!(
406 "Cannot treat float {} as ordinal.",
407 py_num_str(value)
408 ))),
409 }
410 }
411}
412
413impl Lang for LangEn {
414 fn default_currency(&self) -> &str {
417 "EUR"
418 }
419
420 fn default_separator(&self) -> &str {
425 ","
426 }
427
428 fn cards(&self) -> &Cards {
429 &self.cards
430 }
431 fn maxval(&self) -> &BigInt {
432 &self.maxval
433 }
434 fn negword(&self) -> &str {
435 "minus "
436 }
437 fn pointword(&self) -> &str {
438 "point"
439 }
440 fn exclude_title(&self) -> &[String] {
441 &self.exclude_title
442 }
443
444 fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
445 let (ltext, lnum) = l;
446 let (rtext, rnum) = r;
447 let hundred = BigInt::from(100);
448
449 if lnum.is_one() && rnum < &hundred {
450 (rtext.to_string(), rnum.clone())
451 } else if &hundred > lnum && lnum > rnum {
452 (format!("{}-{}", ltext, rtext), lnum + rnum)
453 } else if lnum >= &hundred && &hundred > rnum {
454 (format!("{} and {}", ltext, rtext), lnum + rnum)
455 } else if rnum > lnum {
456 (format!("{} {}", ltext, rtext), lnum * rnum)
457 } else {
458 (format!("{}, {}", ltext, rtext), lnum + rnum)
459 }
460 }
461
462 fn to_ordinal(&self, value: &BigInt) -> Result<String> {
463 self.verify_ordinal(value)?;
464 let cardinal = self.to_cardinal(value)?;
465 let mut outwords: Vec<String> = cardinal.split(' ').map(|s| s.to_string()).collect();
466 let last = outwords.last().unwrap().clone();
467 let mut lastwords: Vec<String> = last.split('-').map(|s| s.to_string()).collect();
468 let lastword = lastwords.last().unwrap().to_lowercase();
469
470 let newlast = match self.ords.get(lastword.as_str()) {
471 Some(o) => o.to_string(),
472 None => {
473 if lastword.ends_with('y') {
474 format!("{}ieth", &lastword[..lastword.len() - 1])
475 } else {
476 format!("{}th", lastword)
477 }
478 }
479 };
480 let n = lastwords.len();
481 lastwords[n - 1] = self.title(&newlast);
482 let n2 = outwords.len();
483 outwords[n2 - 1] = lastwords.join("-");
484 Ok(outwords.join(" "))
485 }
486
487 fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
488 self.verify_ordinal(value)?;
489 let ord = self.to_ordinal(value)?;
490 let suffix: String = ord.chars().rev().take(2).collect::<Vec<_>>().into_iter().rev().collect();
492 Ok(format!("{}{}", value, suffix))
493 }
494
495 fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
508 let i = self.verify_ordinal_float(value)?;
509 self.to_ordinal(&i)
510 }
511
512 fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
517 let i = self.verify_ordinal_float(value)?;
518 let ord = self.to_ordinal(&i)?;
519 let suffix: String = {
520 let chars: Vec<char> = ord.chars().collect();
521 let start = chars.len().saturating_sub(2);
522 chars[start..].iter().collect()
523 };
524 Ok(format!("{}{}", repr_str, suffix))
525 }
526
527 fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
533 match value {
534 FloatValue::Float { value: f, .. } => {
535 if !(f.is_finite() && f.fract() == 0.0) {
536 return Err(N2WError::Type(format!(
537 "to='year' expects an integer; got non-integer float {}",
538 py_float_str(*f)
539 )));
540 }
541 let i = value
542 .as_whole_int()
543 .ok_or_else(|| N2WError::Type("unreachable: whole float".into()))?;
544 self.to_year(&i)
545 }
546 FloatValue::Decimal { value: d, .. } => {
547 let i = d.with_scale(0).as_bigint_and_exponent().0;
549 self.to_year(&i)
550 }
551 }
552 }
553
554 fn to_fraction(&self, numerator: &BigInt, denominator: &BigInt) -> Result<String> {
561 if denominator.is_zero() {
562 return Err(N2WError::ZeroDivision(
563 "denominator must not be zero".into(),
564 ));
565 }
566 if denominator.is_one() || numerator.is_zero() {
567 return self.to_cardinal(numerator);
568 }
569 let is_negative = numerator.is_negative() ^ denominator.is_negative();
570 let abs_n = numerator.abs();
571 let abs_d = denominator.abs();
572 let den_word = if abs_d == BigInt::from(2) {
573 (if abs_n.is_one() { "half" } else { "halves" }).to_string()
574 } else if abs_d == BigInt::from(4) {
575 (if abs_n.is_one() { "quarter" } else { "quarters" }).to_string()
576 } else {
577 let mut w = self.to_ordinal(&abs_d)?;
578 if !abs_n.is_one() {
579 w.push('s');
580 }
581 w
582 };
583 let sign = if is_negative { "minus " } else { "" };
584 Ok(format!("{}{} {}", sign, self.to_cardinal(&abs_n)?, den_word))
585 }
586
587 fn lang_name(&self) -> &str {
596 "Num2Word_EN"
597 }
598
599 fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
600 self.currency_forms.get(code)
601 }
602
603 fn currency_adjective(&self, code: &str) -> Option<&str> {
604 self.currency_adjectives.get(code).copied()
605 }
606
607 fn currency_precision(&self, code: &str) -> i64 {
608 self.currency_precision.get(code).copied().unwrap_or(100)
610 }
611
612 fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
619 let form = if n.is_one() { 0 } else { 1 };
620 forms
621 .get(form)
622 .cloned()
623 .ok_or_else(|| N2WError::Index("tuple index out of range".into()))
624 }
625
626 fn to_year(&self, value: &BigInt) -> Result<String> {
627 let mut val = value.clone();
628 let mut suffix: Option<&str> = None;
629 if val.sign() == num_bigint::Sign::Minus {
630 val = -val;
631 suffix = Some("BC");
632 }
633 let hundred = BigInt::from(100);
634 let high = &val / &hundred;
635 let low = &val % &hundred;
636
637 let ten = BigInt::from(10);
639 let valtext = if high.is_zero()
640 || (( &high % &ten).is_zero() && low < ten)
641 || high >= hundred
642 {
643 self.to_cardinal(&val)?
644 } else {
645 let hightext = self.to_cardinal(&high)?;
646 let lowtext = if low.is_zero() {
647 "hundred".to_string()
648 } else if low < ten {
649 format!("oh-{}", self.to_cardinal(&low)?)
650 } else {
651 self.to_cardinal(&low)?
652 };
653 format!("{} {}", hightext, lowtext)
654 };
655 Ok(match suffix {
656 Some(s) => format!("{} {}", valtext, s),
657 None => valtext,
658 })
659 }
660}