num2words2_core/base.rs
1//! Faithful port of num2words2's `base.py` engine.
2//!
3//! The Python original builds a nested list-of-tuples tree in `splitnum`,
4//! then folds it with `clean`/`merge`. The tree shape is load-bearing:
5//! `clean` branches on whether an element is a tuple or a list, so `Node`
6//! mirrors that distinction exactly rather than flattening it.
7
8use crate::currency::{CurrencyForms, CurrencyValue};
9use crate::floatpath::FloatValue;
10use crate::strnum::{python_decimal_parse, ParsedNumber};
11use bigdecimal::BigDecimal;
12use num_bigint::BigInt;
13use num_integer::Integer;
14use num_traits::{One, Signed, Zero};
15use std::sync::OnceLock;
16
17/// A grammatical-kwargs value (`case=`, `gender=`, `reading=`, ...).
18///
19/// Covers every type the library's converter signatures accept: str, bool,
20/// int, list-of-str, and an explicit `None`. Python bools are ints, so the
21/// binding must try Bool before Int when extracting.
22#[derive(Debug, Clone, PartialEq)]
23pub enum KwVal {
24 Str(String),
25 Bool(bool),
26 Int(i64),
27 List(Vec<String>),
28 None,
29}
30
31/// The kwargs bag handed to the `*_kw` trait hooks. Order-preserving —
32/// insertion order never matters to the converters, but it keeps error
33/// messages deterministic.
34#[derive(Debug, Clone, Default, PartialEq)]
35pub struct Kwargs(pub Vec<(String, KwVal)>);
36
37impl Kwargs {
38 pub fn is_empty(&self) -> bool {
39 self.0.is_empty()
40 }
41 pub fn get(&self, key: &str) -> Option<&KwVal> {
42 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
43 }
44 pub fn str(&self, key: &str) -> Option<&str> {
45 match self.get(key) {
46 Some(KwVal::Str(s)) => Some(s),
47 _ => Option::None,
48 }
49 }
50 pub fn bool(&self, key: &str) -> Option<bool> {
51 match self.get(key) {
52 Some(KwVal::Bool(b)) => Some(*b),
53 _ => Option::None,
54 }
55 }
56 pub fn int(&self, key: &str) -> Option<i64> {
57 match self.get(key) {
58 Some(KwVal::Int(i)) => Some(*i),
59 _ => Option::None,
60 }
61 }
62 pub fn list(&self, key: &str) -> Option<&[String]> {
63 match self.get(key) {
64 Some(KwVal::List(l)) => Some(l),
65 _ => Option::None,
66 }
67 }
68 /// True when every key is in `allowed`. A language's `*_kw` override
69 /// starts with this guard: an unexpected key means the Python signature
70 /// would raise TypeError, so the hook returns NotImplemented and the
71 /// dispatcher falls back to Python, which raises the original error.
72 pub fn only(&self, allowed: &[&str]) -> bool {
73 self.0.iter().all(|(k, _)| allowed.contains(&k.as_str()))
74 }
75}
76
77/// A node in the tree `splitnum` produces.
78///
79/// `Leaf` is Python's `(text, num)` tuple; `List` is a nested list. `clean`
80/// distinguishes the two, so collapsing them would change the output.
81#[derive(Debug, Clone)]
82pub enum Node {
83 Leaf(String, BigInt),
84 List(Vec<Node>),
85}
86
87impl Node {
88 fn leaf(text: impl Into<String>, num: BigInt) -> Node {
89 Node::Leaf(text.into(), num)
90 }
91 fn is_leaf(&self) -> bool {
92 matches!(self, Node::Leaf(..))
93 }
94}
95
96/// The ordered card table: value -> word, in descending key order.
97///
98/// Python uses an `OrderedDict` whose insertion order (high, then mid, then
99/// low) happens to be descending. `splitnum` iterates it in that order, so a
100/// sorted `Vec` reproduces the semantics while allowing binary search.
101#[derive(Debug, Clone, Default)]
102pub struct Cards {
103 entries: Vec<(BigInt, String)>,
104}
105
106impl Cards {
107 pub fn new() -> Self {
108 Cards { entries: Vec::new() }
109 }
110
111 pub fn insert(&mut self, key: BigInt, word: impl Into<String>) {
112 let word = word.into();
113 match self.entries.binary_search_by(|(k, _)| key.cmp(k)) {
114 Ok(i) => self.entries[i] = (key, word),
115 Err(i) => self.entries.insert(i, (key, word)),
116 }
117 }
118
119 pub fn get(&self, key: &BigInt) -> Option<&str> {
120 self.entries
121 .binary_search_by(|(k, _)| key.cmp(k))
122 .ok()
123 .map(|i| self.entries[i].1.as_str())
124 }
125
126 /// Descending iteration, matching the Python insertion order.
127 pub fn iter(&self) -> impl Iterator<Item = &(BigInt, String)> {
128 self.entries.iter()
129 }
130
131 /// Entries from the first card `<= value`, descending.
132 ///
133 /// Python does `for elem in self.cards: if elem > value: continue`, an
134 /// O(n) scan that costs ~100 BigInt comparisons to reach "forty" from
135 /// 10^303. Entries are sorted descending, so the first card `<= value`
136 /// is a binary-search partition point — identical result, O(log n).
137 pub fn iter_from(&self, value: &BigInt) -> impl Iterator<Item = &(BigInt, String)> {
138 let start = self.entries.partition_point(|(k, _)| k > value);
139 self.entries[start..].iter()
140 }
141
142 pub fn highest(&self) -> Option<&BigInt> {
143 self.entries.first().map(|(k, _)| k)
144 }
145
146 pub fn is_empty(&self) -> bool {
147 self.entries.is_empty()
148 }
149}
150
151/// Errors mirroring the exception types the Python original raises.
152///
153/// `Index`, `Key` and `Value` exist because several language modules crash
154/// on edge inputs rather than raising deliberately — e.g. `lang_PL`'s
155/// `to_ordinal(0)` pops its last fragment and then indexes an empty list
156/// (IndexError), `to_ordinal(-1)` feeds "-" to `int()` (ValueError), and
157/// `to_ordinal(10**12)` misses `prefixes_ordinal[4]` (KeyError).
158///
159/// Those are Python bugs, but they are observable behaviour that callers may
160/// catch, so parity requires reproducing the exception *type*. Do not
161/// "improve" them into a tidy TypeError.
162#[derive(Debug)]
163pub enum N2WError {
164 Overflow(String),
165 Type(String),
166 /// Python raised NotImplementedError as a DELIBERATE result — the same
167 /// exception the original produces, so it must propagate to the caller.
168 /// `to_ordinal(>100)` in Welsh, an unknown Japanese counter, and the
169 /// abstract `pluralize` all land here. Distinct from `Fallback`.
170 NotImplemented(String),
171 /// Not a Python exception: the Rust core declines this specific call and
172 /// the binding re-runs the ORIGINAL Python converter, which owns it
173 /// (an unported kwarg whose Python path raises the real TypeError; an
174 /// Inf/NaN string a language handles in Python; a sentence sub-case).
175 /// Kept separate from `NotImplemented` so a genuine NotImplementedError
176 /// raise is served natively instead of being swallowed by the shim's
177 /// fallback catch.
178 Fallback(String),
179 ZeroDivision(String),
180 /// Python raised IndexError (usually an out-of-range list access).
181 Index(String),
182 /// Python raised KeyError (usually a missing dict entry).
183 Key(String),
184 /// Python raised ValueError (usually int()/str parsing of a bad token).
185 Value(String),
186 /// Python raised AttributeError. Usually the converter simply does not
187 /// define the method or attribute at all — `Num2Word_RM` has no
188 /// `to_year`/`to_ordinal_num`, and `Num2Word_IT` has no `errmsg_negord`
189 /// — so the crash happens on attribute lookup, before any conversion.
190 Attribute(String),
191 /// Python raised AssertionError. `lang_AR` asserts its bounds instead of
192 /// raising deliberately, so `to_ordinal(>=10**51)` and
193 /// `to_cardinal(-(10**51))` surface as AssertionError, not OverflowError.
194 Assertion(String),
195 /// Not an error: Python's function fell off the end and returned `None`.
196 ///
197 /// `lang_VI` does this past 10^75 — no raise, no string, just an implicit
198 /// `None` that propagates to the caller. `None` is a *success* value, so
199 /// the binding turns this sentinel into Python `None` rather than an
200 /// exception. Modelled as an Err variant purely to avoid rewriting
201 /// `Result<String>` to `Result<Option<String>>` across 154 language files.
202 ReturnsNone,
203 /// Python raised an exception class defined in a language module rather
204 /// than a builtin: `(module_path, class_name, message)`.
205 ///
206 /// `lang_BN.py` is the only case in the library — it defines
207 /// `NumberTooLargeError(Exception)` and raises it past `MAX_NUMBER`
208 /// (~10^306) instead of `OverflowError`. The binding imports the class
209 /// and raises the real thing, so `except NumberTooLargeError` keeps
210 /// working.
211 Custom {
212 module: &'static str,
213 class: &'static str,
214 msg: String,
215 },
216}
217
218pub type Result<T> = std::result::Result<T, N2WError>;
219
220/// The per-language behaviour that `base.py` leaves abstract or overridable.
221///
222/// Defaults mirror `Num2Word_Base`; each language overrides what it needs,
223/// exactly as the Python subclasses do.
224///
225/// Two shapes exist in the wild, and both must be expressible:
226///
227/// 1. *Engine languages* supply `cards` + `merge` and let the default
228/// `to_cardinal` drive `splitnum`/`clean` (as `Num2Word_Base` does).
229/// 2. *Self-contained languages* override `to_cardinal` outright and never
230/// touch cards or merge — 121 of the 156 Python modules do this.
231///
232/// So `cards`/`maxval`/`merge` carry defaults rather than being required;
233/// a shape-2 language simply ignores them.
234pub trait Lang {
235 fn cards(&self) -> &Cards {
236 static EMPTY: OnceLock<Cards> = OnceLock::new();
237 EMPTY.get_or_init(Cards::new)
238 }
239
240 fn maxval(&self) -> &BigInt {
241 static ZERO: OnceLock<BigInt> = OnceLock::new();
242 ZERO.get_or_init(BigInt::zero)
243 }
244
245 fn negword(&self) -> &str {
246 "(-) "
247 }
248 fn pointword(&self) -> &str {
249 "(.)"
250 }
251 fn is_title(&self) -> bool {
252 false
253 }
254 fn exclude_title(&self) -> &[String] {
255 &[]
256 }
257
258 /// `merge` is abstract in Python (`raise NotImplementedError`).
259 /// Languages that override `to_cardinal` never reach it.
260 fn merge(&self, _l: (&str, &BigInt), _r: (&str, &BigInt)) -> (String, BigInt) {
261 unimplemented!("merge not implemented for this language")
262 }
263
264 fn title(&self, value: &str) -> String {
265 if !self.is_title() {
266 return value.to_string();
267 }
268 let excl = self.exclude_title();
269 value
270 .split_whitespace()
271 .map(|w| {
272 if excl.iter().any(|e| e == w) {
273 w.to_string()
274 } else {
275 // Python does word[0].upper() + word[1:] — uppercase the
276 // first *character*, not the first byte.
277 let mut c = w.chars();
278 match c.next() {
279 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
280 None => String::new(),
281 }
282 }
283 })
284 .collect::<Vec<_>>()
285 .join(" ")
286 }
287
288 fn to_cardinal(&self, value: &BigInt) -> Result<String> {
289 default_to_cardinal(self, value)
290 }
291
292 fn to_ordinal(&self, value: &BigInt) -> Result<String> {
293 self.to_cardinal(value)
294 }
295
296 fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
297 Ok(value.to_string())
298 }
299
300 fn to_year(&self, value: &BigInt) -> Result<String> {
301 self.to_cardinal(value)
302 }
303
304 // ---- currency ----------------------------------------------------
305 //
306 // Every hook Python's currency path can reach is exposed here with a
307 // default. A language overrides only what its Python class overrides.
308
309 /// Class name, for the `'X' not implemented for 'Num2Word_Y'` message.
310 fn lang_name(&self) -> &str {
311 "Num2Word_Base"
312 }
313
314 /// `CURRENCY_FORMS[code]`. `None` -> NotImplementedError, as in Python.
315 fn currency_forms(&self, _code: &str) -> Option<&CurrencyForms> {
316 None
317 }
318
319 /// `CURRENCY_ADJECTIVES[code]`.
320 fn currency_adjective(&self, _code: &str) -> Option<&str> {
321 None
322 }
323
324 /// `CURRENCY_PRECISION.get(code, 100)` — subunits per unit. 1000 for
325 /// 3-decimal currencies (BHD/KWD/...), 1 for no-subunit currencies.
326 fn currency_precision(&self, _code: &str) -> i64 {
327 100
328 }
329
330 /// `pluralize(n, forms)`. Abstract in Python (`raise NotImplementedError`),
331 /// so the default raises rather than guessing a plural rule.
332 fn pluralize(&self, _n: &BigInt, _forms: &[String]) -> Result<String> {
333 Err(N2WError::NotImplemented("pluralize".into()))
334 }
335
336 fn money_verbose(&self, number: &BigInt, _currency: &str) -> Result<String> {
337 self.to_cardinal(number)
338 }
339
340 fn cents_verbose(&self, number: &BigInt, _currency: &str) -> Result<String> {
341 self.to_cardinal(number)
342 }
343
344 fn cents_terse(&self, number: &BigInt, currency: &str) -> Result<String> {
345 Ok(crate::currency::default_cents_terse(
346 number,
347 self.currency_precision(currency),
348 ))
349 }
350
351 /// `Num2Word_Base.to_cardinal_float`. `precision_override` is the
352 /// `precision=` kwarg (issue #580).
353 fn to_cardinal_float(
354 &self,
355 value: &FloatValue,
356 precision_override: Option<u32>,
357 ) -> Result<String> {
358 crate::floatpath::default_to_cardinal_float(self, value, precision_override)
359 }
360
361 /// Render a non-integral value, for the fractional-cents branch. Python
362 /// reaches this as `self.to_cardinal(float(right))`, i.e. through the
363 /// float path, so the default routes there rather than raising.
364 fn cardinal_from_decimal(&self, value: &BigDecimal) -> Result<String> {
365 crate::floatpath::cardinal_from_bigdecimal(self, value)
366 }
367
368 /// `separator: None` means the caller omitted the kwarg, so the
369 /// language's own default applies. Python expresses this as a per-method
370 /// default (`Num2Word_CA.to_currency(..., separator=" amb")`), which a
371 /// plain `&str` cannot represent: once the shim substitutes Base's ",",
372 /// "caller omitted it" and "caller passed a comma" are indistinguishable.
373 fn to_currency(
374 &self,
375 val: &CurrencyValue,
376 currency: &str,
377 cents: bool,
378 separator: Option<&str>,
379 adjective: bool,
380 ) -> Result<String> {
381 crate::currency::default_to_currency(
382 self,
383 val,
384 currency,
385 cents,
386 separator.unwrap_or(self.default_separator()),
387 adjective,
388 )
389 }
390
391 /// This language's own `separator=` default. Base's is ",".
392 fn default_separator(&self) -> &str {
393 ","
394 }
395
396 /// This language's own `to_currency(adjective=...)` default. Base's is
397 /// False; only Mongolian defaults to True.
398 fn default_adjective(&self) -> bool {
399 false
400 }
401
402 /// This language's own `to_currency(currency=...)` default. Base's is
403 /// "EUR", but only 44 of 156 languages actually use it — en_IN defaults
404 /// to INR, id to IDR, af to ZAR, and so on. Hardcoding EUR in the
405 /// dispatcher silently converts the wrong currency.
406 fn default_currency(&self) -> &str {
407 "EUR"
408 }
409
410 fn to_cheque(&self, val: &BigDecimal, currency: &str) -> Result<String> {
411 crate::currency::default_to_cheque(self, val, currency)
412 }
413
414 // ---- fractions -----------------------------------------------------
415
416 /// `Num2Word_Base.to_fraction` (issue #584). EN/DE/ES/FR/IT/PT and the
417 /// aero profiles override with idiomatic forms (half/quarter/Drittel).
418 fn to_fraction(&self, numerator: &BigInt, denominator: &BigInt) -> Result<String> {
419 default_to_fraction(self, numerator, denominator)
420 }
421
422 // ---- float/Decimal routing ------------------------------------------
423 //
424 // Python's dispatcher hands a float/Decimal straight to the converter
425 // method; where it lands is the *language's* decision. Base semantics:
426 // `assert int(value) == value` succeeds for a whole value -> integer
427 // path ("one"); fails -> to_cardinal_float. Languages overriding
428 // Python's to_cardinal (ru "пять целых ноль десятых", cs, be, ...) make
429 // their own call and override these hooks.
430
431 /// `to_cardinal(float/Decimal)` — the full entry, whole values included.
432 fn cardinal_float_entry(
433 &self,
434 value: &FloatValue,
435 precision_override: Option<u32>,
436 ) -> Result<String> {
437 if let Some(i) = value.as_whole_int() {
438 return self.to_cardinal(&i);
439 }
440 self.to_cardinal_float(value, precision_override)
441 }
442
443 /// `to_ordinal(float/Decimal)`. Base's to_ordinal is to_cardinal, so the
444 /// default routes identically; languages with verify_ordinal-style type
445 /// checks (TypeError on non-int) override.
446 fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
447 self.cardinal_float_entry(value, None)
448 }
449
450 /// `to_ordinal_num(float/Decimal)`. Base returns the value unchanged and
451 /// the dispatcher str()s it, so the default echoes the Python repr the
452 /// binding computed.
453 fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
454 Ok(repr_str.to_string())
455 }
456
457 /// `to_year(float/Decimal)`. Base's to_year is to_cardinal.
458 fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
459 self.cardinal_float_entry(value, None)
460 }
461
462 // ---- string inputs ---------------------------------------------------
463
464 /// `converter.str_to_number`. Base is `Decimal(value)`; ES ("1ro" ->
465 /// ordinal handshake), PT_BR ("1.50" -> 'ponto'), DV (TypeError instead
466 /// of InvalidOperation), BN (abs()) and ID override.
467 fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
468 python_decimal_parse(s)
469 }
470
471 /// The pt_BR 'ponto' rendering: `to_cardinal(value)` with the pointword
472 /// swapped for this one call. Only reachable through
473 /// `ParsedNumber::DecPoint`, so only PT_BR implements it.
474 fn cardinal_with_pointword(
475 &self,
476 _value: &FloatValue,
477 _pointword: &str,
478 _precision_override: Option<u32>,
479 ) -> Result<String> {
480 Err(N2WError::Fallback("cardinal_with_pointword".into()))
481 }
482
483 // ---- grammatical kwargs ----------------------------------------------
484 //
485 // The `*_kw` hooks receive the caller's kwargs bag. The default accepts
486 // only an empty bag; any actual kwarg is NotImplemented, which the
487 // binding surfaces as NotImplementedError so the dispatcher falls back
488 // to Python — either the original handles the kwarg (port not landed) or
489 // it raises the original TypeError (converter doesn't accept it).
490
491 fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
492 if kw.is_empty() {
493 return self.to_cardinal(value);
494 }
495 Err(N2WError::Fallback("kwargs".into()))
496 }
497
498 fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
499 if kw.is_empty() {
500 return self.to_ordinal(value);
501 }
502 Err(N2WError::Fallback("kwargs".into()))
503 }
504
505 fn to_ordinal_num_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
506 if kw.is_empty() {
507 return self.to_ordinal_num(value);
508 }
509 Err(N2WError::Fallback("kwargs".into()))
510 }
511
512 fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
513 if kw.is_empty() {
514 return self.to_year(value);
515 }
516 Err(N2WError::Fallback("kwargs".into()))
517 }
518
519 #[allow(clippy::too_many_arguments)]
520 fn to_currency_kw(
521 &self,
522 val: &CurrencyValue,
523 currency: &str,
524 cents: bool,
525 separator: Option<&str>,
526 adjective: bool,
527 kw: &Kwargs,
528 ) -> Result<String> {
529 if kw.is_empty() {
530 return self.to_currency(val, currency, cents, separator, adjective);
531 }
532 Err(N2WError::Fallback("kwargs".into()))
533 }
534
535 fn to_cardinal_float_kw(
536 &self,
537 value: &FloatValue,
538 precision_override: Option<u32>,
539 kw: &Kwargs,
540 ) -> Result<String> {
541 if kw.is_empty() {
542 return self.cardinal_float_entry(value, precision_override);
543 }
544 Err(N2WError::Fallback("kwargs".into()))
545 }
546
547 /// Spanish "1ro" currency: does the stashed ordinal fire? `Num2Word_ES`
548 /// apocopates 1 → "un euro" in `to_currency`, bypassing `to_cardinal`, so
549 /// the stash never fires (false, the default). The regional `es_XX`
550 /// classes inherit `Base.to_currency`, whose `_money_verbose` calls
551 /// `to_cardinal(1)` → the stash fires → "primero córdoba" (true).
552 fn es_currency_ordinal_fires(&self) -> bool {
553 false
554 }
555
556 /// Render `Decimal('-0.0')` for mode `to`, which BigDecimal cannot
557 /// represent (it has no signed zero). `None` — the default — means the
558 /// value coincides with float `-0.0`, so the binding's `Float{-0.0}`
559 /// demotion is exact. The ~10 languages that render the Decimal form
560 /// differently (ce/cy "dim"/"ноль" via the integer path, pl its float
561 /// grammar with the negword, rm a TypeError) override this to serve it
562 /// natively instead of leaning on a Python fallback.
563 fn neg_zero_decimal(&self, _to: &str) -> Option<Result<String>> {
564 None
565 }
566
567 /// `str_to_number` produced `Decimal('Infinity')` / `-Infinity`. Base's
568 /// integer path does `int(Decimal('Infinity'))` → OverflowError. The many
569 /// self-contained converters that feed the raw token to `int("Infinity")`
570 /// get ValueError instead and override this. `to` is the requested mode.
571 fn inf_result(&self, _negative: bool, _to: &str) -> Result<String> {
572 Err(N2WError::Overflow(
573 "cannot convert Infinity to integer".into(),
574 ))
575 }
576
577 /// `str_to_number` produced `Decimal('NaN')`. Base's `int(NaN)` raises
578 /// ValueError (caught into the float path, which raises ValueError again).
579 /// Languages whose parse raises `decimal.InvalidOperation` first override.
580 fn nan_result(&self, _to: &str) -> Result<String> {
581 Err(N2WError::Value("cannot convert NaN to integer".into()))
582 }
583
584 // ---- introspection ---------------------------------------------------
585
586 /// Python's `getattr(converter, "MAXVAL", None)` — the `maxval()` API.
587 /// Engine languages get it from their card table (Base sets
588 /// `MAXVAL = 1000 * top_card`); self-contained ones mostly lack the
589 /// attribute (None). The zero sentinel in `maxval()` means "unset".
590 fn python_maxval(&self) -> Option<BigInt> {
591 let m = self.maxval();
592 if m.is_zero() {
593 None
594 } else {
595 Some(m.clone())
596 }
597 }
598}
599
600/// Python's `Num2Word_Base.to_fraction`.
601pub fn default_to_fraction<L: Lang + ?Sized>(
602 lang: &L,
603 numerator: &BigInt,
604 denominator: &BigInt,
605) -> Result<String> {
606 if denominator.is_zero() {
607 return Err(N2WError::ZeroDivision(
608 "denominator must not be zero".into(),
609 ));
610 }
611 if denominator.is_one() || numerator.is_zero() {
612 return lang.to_cardinal(numerator);
613 }
614 let is_negative = numerator.is_negative() ^ denominator.is_negative();
615 let abs_n = numerator.abs();
616 let abs_d = denominator.abs();
617 let sign = if is_negative {
618 format!("{} ", lang.negword().trim())
619 } else {
620 String::new()
621 };
622 let num_word = lang.to_cardinal(&abs_n)?;
623 let mut den_word = lang.to_ordinal(&abs_d)?;
624 if !abs_n.is_one() {
625 // Python appends a bare "s"; languages override for real plurals.
626 den_word.push('s');
627 }
628 Ok(format!("{}{} {}", sign, num_word, den_word))
629}
630
631/// Python's `splitnum`. Returns `None` where Python falls off the loop and
632/// implicitly returns `None` (value larger than every card).
633pub fn splitnum<L: Lang + ?Sized>(lang: &L, value: &BigInt) -> Option<Vec<Node>> {
634 let cards = lang.cards();
635 for (elem, word) in cards.iter_from(value) {
636 let mut out: Vec<Node> = Vec::new();
637 let (div, mod_) = if value.is_zero() {
638 (BigInt::one(), BigInt::zero())
639 } else {
640 value.div_mod_floor(elem)
641 };
642
643 if div.is_one() {
644 let one = BigInt::one();
645 let w = cards.get(&one).unwrap_or("").to_string();
646 out.push(Node::leaf(w, one));
647 } else {
648 if &div == value {
649 // Tally systems (e.g. Roman numerals): repeat the word `div`
650 // times rather than recursing, matching Python's `div * word`.
651 let reps = div.to_string().parse::<usize>().unwrap_or(0);
652 return Some(vec![Node::leaf(word.repeat(reps), &div * elem)]);
653 }
654 out.push(Node::List(splitnum(lang, &div)?));
655 }
656
657 out.push(Node::leaf(word.clone(), elem.clone()));
658
659 if !mod_.is_zero() {
660 out.push(Node::List(splitnum(lang, &mod_)?));
661 }
662
663 return Some(out);
664 }
665 None
666}
667
668/// Python's `clean`: fold the tree down to a single `(text, num)` leaf.
669///
670/// Mirrors the original's quirk where the tail `val[2:]` is appended as a
671/// *nested list* rather than spliced, which changes how the next iteration
672/// sees it.
673pub fn clean<L: Lang + ?Sized>(lang: &L, val: Vec<Node>) -> Node {
674 let mut val = val;
675 while val.len() != 1 {
676 let mut out: Vec<Node> = Vec::new();
677 if val.len() >= 2 && val[0].is_leaf() && val[1].is_leaf() {
678 let (lt, ln) = match &val[0] {
679 Node::Leaf(t, n) => (t.clone(), n.clone()),
680 _ => unreachable!(),
681 };
682 let (rt, rn) = match &val[1] {
683 Node::Leaf(t, n) => (t.clone(), n.clone()),
684 _ => unreachable!(),
685 };
686 let (mt, mn) = lang.merge((<, &ln), (&rt, &rn));
687 out.push(Node::Leaf(mt, mn));
688 if val.len() > 2 {
689 out.push(Node::List(val[2..].to_vec()));
690 }
691 } else {
692 for elem in val.into_iter() {
693 match elem {
694 Node::List(inner) => {
695 if inner.len() == 1 {
696 out.push(inner.into_iter().next().unwrap());
697 } else {
698 out.push(clean(lang, inner));
699 }
700 }
701 leaf => out.push(leaf),
702 }
703 }
704 }
705 val = out;
706 }
707 val.into_iter().next().unwrap()
708}
709
710/// Python's `Num2Word_Base.to_cardinal` for integral input.
711pub fn default_to_cardinal<L: Lang + ?Sized>(lang: &L, value: &BigInt) -> Result<String> {
712 let mut out = String::new();
713 let mut v = value.clone();
714 if v.is_negative() {
715 v = v.abs();
716 out = format!("{} ", lang.negword().trim());
717 }
718
719 if &v >= lang.maxval() {
720 return Err(N2WError::Overflow(format!(
721 "abs({}) must be less than {}.",
722 v,
723 lang.maxval()
724 )));
725 }
726
727 let tree = splitnum(lang, &v).ok_or_else(|| {
728 N2WError::Overflow(format!("abs({}) must be less than {}.", v, lang.maxval()))
729 })?;
730 let node = clean(lang, tree);
731 let words = match node {
732 Node::Leaf(t, _) => t,
733 Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
734 };
735 Ok(lang.title(&format!("{}{}", out, words)))
736}
737
738/// Python's `set_low_numwords`: words map to descending values ending at 0.
739pub fn set_low_numwords(cards: &mut Cards, numwords: &[&str]) {
740 let n = numwords.len();
741 for (i, word) in numwords.iter().enumerate() {
742 cards.insert(BigInt::from(n - 1 - i), *word);
743 }
744}
745
746pub fn set_mid_numwords(cards: &mut Cards, mid: &[(i64, &str)]) {
747 for (k, v) in mid {
748 cards.insert(BigInt::from(*k), *v);
749 }
750}