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