Skip to main content

num2words2_core/
lang_kn.rs

1//! Port of `lang_KN.py` (Kannada), via its `lang_EUR` -> `Num2Word_Base` ancestry.
2//!
3//! Shape: **engine**. `Num2Word_KN` defines `low_numwords`/`mid_numwords`/
4//! `high_numwords` plus `merge`, and lets `Num2Word_Base.to_cardinal` drive
5//! `splitnum`/`clean`. So this file builds a `Cards` table in `new()`,
6//! implements `merge`, and leaves `to_cardinal` at the trait default.
7//!
8//! # Inheritance notes
9//!
10//! `Num2Word_KN.setup()` does **not** call `super().setup()`, so none of
11//! `Num2Word_EUR`'s Latin-prefix illion machinery (`gen_high_numwords`, the
12//! `cent`/`m`/`b`/`tr` stems) is reachable. KN also overrides
13//! `set_high_numwords` to a completely different contract:
14//!
15//! ```python
16//! def set_high_numwords(self, high):
17//!     for n, word in self.high_numwords:
18//!         self.cards[10**n] = word
19//! ```
20//!
21//! i.e. `high_numwords` holds `(exponent, word)` pairs -- `[(7, crore),
22//! (5, lakh), (3, thousand)]` -- rather than EUR's bare word list zipped
23//! against a computed exponent range. Note it ignores its `high` parameter and
24//! reads `self.high_numwords` directly; same object, so no behavioural
25//! difference. The result is the Indian numbering system: crore (10^7), lakh
26//! (10^5), thousand (10^3), hundred (10^2), then 0..=99 as literal cards.
27//!
28//! Inherited unchanged from `Num2Word_Base` (trait defaults are correct):
29//!   * `negword` = `"(-) "` -- KN never overrides it, so negative cardinals
30//!     read "(-) <word>" (base does `"%s " % negword.strip()`).
31//!   * `is_title` = False -> `title()` is the identity.
32//!   * `to_year(value)` -> `to_cardinal(value)`, including for negative years;
33//!     there is no BC/CE suffix, so `to_year(-500) == to_cardinal(-500)`.
34//!
35//! # MAXVAL
36//!
37//! `Num2Word_Base.__init__` sets `MAXVAL = 1000 * list(self.cards.keys())[0]`.
38//! The OrderedDict is filled high -> mid -> low, so key[0] is 10^7 and
39//! **MAXVAL == 10^10**. Anything `>= 10^10` raises `OverflowError`. Values are
40//! therefore bounded in practice, but `BigInt` is kept throughout because the
41//! overflow check runs against the caller's arbitrary-precision input.
42//!
43//! # Unicode: the source is NOT NFC-normalized
44//!
45//! This is the single biggest hazard in this port. `lang_KN.py` ships a **mix**
46//! of precomposed and decomposed Kannada vowel signs, and the decomposed spellings
47//! are load-bearing for byte-for-byte output parity. Eight card words are
48//! decomposed in the source: 11, 15, 17, 41, 45, 81, 90, and -- most visibly --
49//! 10^7 (crore), whose second vowel is stored as U+0CCA U+0CD5 rather than the
50//! precomposed U+0CCB. Crore appears in every 10^7+ result, so normalizing it
51//! would silently break a large slice of the corpus while still *rendering*
52//! identically on screen.
53//!
54//! Every Kannada string below is therefore written as explicit `\u{...}`
55//! escapes, generated mechanically from the Python source. Escapes are pure
56//! ASCII and survive editors/tools that would otherwise silently NFC-normalize
57//! literal Kannada text (which is exactly what happened on the first draft of
58//! this file). The human-readable spelling of each entry lives in the trailing
59//! comment, where it is inert. **Do not "clean up" these escapes into literals.**
60//!
61//! Reassuringly, NFC does not change the *final* character of any card word, so
62//! the `merge`/`to_ordinal` modifier tests below behave the same either way --
63//! only the emitted bytes differ.
64//!
65//! # Faithfully reproduced Python quirks
66//!
67//! This is a port, not a rewrite. All of the following are exactly what Python
68//! emits, verified against the interpreter over every card boundary in 0..10^10:
69//!
70//! 1. **`modifiers[3]` is dead code.** The list has 15 entries, but entry 3 is
71//!    the *decomposed* two-codepoint sequence U+0CBF U+0CD5 rather than the
72//!    precomposed U+0CC0 that entry 4 already holds. Both `merge` and
73//!    `to_ordinal` test membership with a **single character**
74//!    (`ltext[-1] in self.modifiers` / `outwords[-1] in ...`), and a one-char
75//!    string can never equal a two-char one, so entry 3 can never match. It is
76//!    preserved verbatim anyway: dropping it would be behaviourally identical,
77//!    but keeping it documents the source. This is why [`MODIFIERS`] is
78//!    `[&str; 15]` and not `[char; 15]`.
79//!
80//! 2. **`to_ordinal` truncates a real letter, not just a vowel sign.** Stripping
81//!    the trailing modifier from the cardinal for 10^7 removes the final vowel
82//!    of "koti", yielding "...kota-ne" instead of "...koti-ne". Same for every
83//!    crore value. Wrong-looking, but it is the spec.
84//!
85//! 3. **`to_ordinal_num` prepends the digits to the full ordinal *words***:
86//!    `"%s%s" % (value, self.to_ordinal(value))`, so `to_ordinal_num(100)` is
87//!    the numeral "100" glued directly onto the entire spelled-out ordinal with
88//!    no separator. Most languages return a short suffix here (EN gives
89//!    "100th"); KN does not.
90//!
91//! 4. **Typos and inconsistent spacing in `low_numwords`, kept verbatim.** The
92//!    30s are especially ragged: 36/37/38 are missing the second "ta" that every
93//!    other thirty-something carries. Spacing is arbitrary across the table --
94//!    93/94 use a bare consonant plus a space (no virama), 83/84 use virama plus
95//!    space, 53/54 run the words together with no space at all, while 22..=25 use
96//!    virama plus space. Consequently `to_cardinal(31)` is joined but
97//!    `to_cardinal(21)` is spaced. Do not normalise any of it.
98//!
99//! 5. **The `"%s-%s"` hyphen branch of `merge` is unreachable.** Every value
100//!    below 100 has its own card, so `splitnum` always resolves it with `div == 1`
101//!    and `merge` takes the first branch instead. The branch is ported anyway to
102//!    keep the arm structure identical to Python.
103//!
104//! 6. **KN's currency table is whatever `lang_EN` last mutated it into.**
105//!    See "Currency" below. This is cross-*language* state leakage in the
106//!    Python original and it is observable, so it is reproduced.
107//!
108//! 7. **EN's currency *precisions* do NOT leak, but its *forms* do.** The two
109//!    sit in adjacent lines of the same `__init__` and behave differently. See
110//!    "Currency" below.
111//!
112//! # Currency
113//!
114//! `Num2Word_KN` defines nothing currency-related. It inherits `pluralize` from
115//! `Num2Word_EUR` and the whole `to_currency`/`to_cheque` machinery from
116//! `Num2Word_Base`. The tables, however, are not what `lang_EUR.py` reads like.
117//!
118//! `Num2Word_EN.__init__` runs this:
119//!
120//! ```python
121//! self.CURRENCY_FORMS["GBP"] = (("pound", "pounds"), ("penny", "pence"))
122//! self.CURRENCY_FORMS["CHF"] = (("franc", "francs"), ("rappen", "rappen"))
123//! ...
124//! self.CURRENCY_PRECISION = {"BHD": 1000, "KWD": 1000, ...}
125//! ```
126//!
127//! `Num2Word_EN` declares no `CURRENCY_FORMS` of its own, so `self.CURRENCY_FORMS`
128//! resolves up the MRO to **`Num2Word_EUR`'s dict object** and `__setitem__`
129//! mutates it **in place**. Every `Num2Word_EUR` subclass -- KN included --
130//! shares that one dict, so EN's edits are permanently visible to KN. Because
131//! `num2words2/__init__.py` instantiates every converter at import time,
132//! `Num2Word_EN()` has always run before any KN call, and the table is fully
133//! mutated by the time `to_currency` reads it. Net effect for KN:
134//!
135//!   * 39 codes instead of EUR's 23 (CHF/CNY/KWD/BHD/AED/... arrive from EN).
136//!   * `EUR` is `("euro", "euros")`, not EUR's own `("euro", "euro")`.
137//!   * `GBP` is `("pound", "pounds")`, not `("pound sterling", "pounds sterling")`.
138//!   * `SAR` is `("riyal", "riyals")`, not `("saudi riyal", "saudi riyals")`.
139//!
140//! The **last** line is different in kind: `self.CURRENCY_PRECISION = {...}` is
141//! an *assignment*, not a mutation, so it binds an instance attribute on the EN
142//! object alone and never touches the class. KN therefore keeps
143//! `Num2Word_Base.CURRENCY_PRECISION == {}` and gets divisor 100 for every
144//! code -- including the 3-decimal (KWD/BHD) and 0-decimal (JPY) currencies.
145//! Both halves of this asymmetry are confirmed against the frozen corpus.
146//!
147//! [`CURRENCY_FORMS`] and [`CURRENCY_ADJECTIVES`] below were therefore dumped
148//! from a live interpreter after a full `import num2words2`, not transcribed
149//! from `lang_EUR.py`. Re-deriving them from the source file would silently
150//! produce a different (and wrong) table. `CURRENCY_ADJECTIVES` happens to be
151//! unmutated, but is dumped the same way so the two cannot drift apart.
152//!
153//! # Errors
154//!
155//! KN raises only what the base class raises: `OverflowError` for
156//! `abs(value) >= 10^10` (from `to_cardinal`), and `TypeError` from
157//! `verify_ordinal` for negative ordinals. There are no crash-style
158//! Index/Key/Value paths in this module.
159
160use crate::base::{set_low_numwords, set_mid_numwords, Cards, Lang, N2WError, Result};
161use crate::currency::CurrencyForms;
162use crate::floatpath::FloatValue;
163use num_bigint::BigInt;
164use num_traits::{One, Signed, Zero};
165use std::collections::HashMap;
166
167// --- Data tables ---------------------------------------------------------
168//
169// Generated mechanically from lang_KN.py. Escapes, not literals -- see the
170// "Unicode" section in the module docs above before touching these.
171
172const LOW_NUMWORDS: [&str; 100] = [
173    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 99 = ತೊಂಬತ್ತೊಂಬತ್ತು
174    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 98 = ತೊಂಬತ್ತೆಂಟು
175    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc7}\u{0cb3}\u{0cc1}", // 97 = ತೊಂಬತ್ತೇಳು
176    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cbe}\u{0cb0}\u{0cc1}", // 96 = ತೊಂಬತ್ತಾರು
177    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc8}\u{0ca6}\u{0cc1}", // 95 = ತೊಂಬತ್ತೈದು
178    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 94 = ತೊಂಬತ್ತ ನಾಲ್ಕು
179    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 93 = ತೊಂಬತ್ತ ಮೂರು
180    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cb0}\u{0ca1}\u{0cc1}", // 92 = ತೊಂಬತ್ತೆರಡು
181    "\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0ca6}\u{0cc1}", // 91 = ತೊಂಬತ್ತೊಂದು
182    "\u{0ca4}\u{0cc6}\u{0cc2}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 90 = ತೊಂಬತ್ತು (decomposed)
183    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 89 = ಎಂಬತ್ತೊಂಬತ್ತು
184    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 88 = ಎಂಬತ್ತೆಂಟು
185    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc7}\u{0cb3}\u{0cc1}", // 87 = ಎಂಬತ್ತೇಳು
186    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cbe}\u{0cb0}\u{0cc1}", // 86 = ಎಂಬತ್ತಾರು
187    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc8}\u{0ca6}\u{0cc1}", // 85 = ಎಂಬತ್ತೈದು
188    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 84 = ಎಂಬತ್ತ್ ನಾಲ್ಕು
189    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 83 = ಎಂಬತ್ತ್ ಮೂರು
190    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cb0}\u{0ca1}\u{0cc1}", // 82 = ಎಂಬತ್ತೆರಡು
191    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cc2}\u{0c82}\u{0ca6}\u{0cc1}", // 81 = ಎಂಬತ್ತೊಂದು (decomposed)
192    "\u{0c8e}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 80 = ಎಂಬತ್ತು
193    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 79 = ಎಪ್ಪತ್ತೊಂಬತ್ತು
194    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 78 = ಎಪ್ಪತ್ತೆಂಟು
195    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc7}\u{0cb3}\u{0cc1}", // 77 = ಎಪ್ಪತ್ತೇಳು
196    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cbe}\u{0cb0}\u{0cc1}", // 76 = ಎಪ್ಪತ್ತಾರು
197    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc8}\u{0ca6}\u{0cc1}", // 75 = ಎಪ್ಪತ್ತೈದು
198    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 74 = ಎಪ್ಪತ್ತ್ ನಾಲ್ಕು
199    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 73 = ಎಪ್ಪತ್ತ್ ಮೂರು
200    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cb0}\u{0ca1}\u{0cc1}", // 72 = ಎಪ್ಪತ್ತೆರಡು
201    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0ca6}\u{0cc1}", // 71 = ಎಪ್ಪತ್ತೊಂದು
202    "\u{0c8e}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 70 = ಎಪ್ಪತ್ತು
203    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 69 = ಅರವತ್ತೊಂಬತ್ತು
204    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 68 = ಅರವತ್ತೆಂಟು
205    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc7}\u{0cb3}\u{0cc1}", // 67 = ಅರವತ್ತೇಳು
206    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cbe}\u{0cb0}\u{0cc1}", // 66 = ಅರವತ್ತಾರು
207    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc8}\u{0ca6}\u{0cc1}", // 65 = ಅರವತ್ತೈದು
208    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 64 = ಅರವತ್ತ್ ನಾಲ್ಕು
209    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 63 = ಅರವತ್ತ್ ಮೂರು
210    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cb0}\u{0ca1}\u{0cc1}", // 62 = ಅರವತ್ತೆರಡು
211    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0ca6}\u{0cc1}", // 61 = ಅರವತ್ತೊಂದು
212    "\u{0c85}\u{0cb0}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 60 = ಅರವತ್ತು
213    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 59 = ಐವತ್ತೊಂಬತ್ತು
214    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 58 = ಐವತ್ತೆಂಟು
215    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc7}\u{0cb3}\u{0cc1}", // 57 = ಐವತ್ತೇಳು
216    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cbe}\u{0cb0}\u{0cc1}", // 56 = ಐವತ್ತಾರು
217    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc8}\u{0ca6}\u{0cc1}", // 55 = ಐವತ್ತೈದು
218    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 54 = ಐವತ್ತ್ನಾಲ್ಕು
219    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 53 = ಐವತ್ತಮೂರು
220    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cb0}\u{0ca1}\u{0cc1}", // 52 = ಐವತ್ತೆರಡು
221    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0ca6}\u{0cc1}", // 51 = ಐವತ್ತೊಂದು
222    "\u{0c90}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 50 = ಐವತ್ತು
223    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 49 = ನಲವತ್ತೊಂಬತ್ತು
224    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 48 = ನಲವತ್ತೆಂಟು
225    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc7}\u{0cb3}\u{0cc1}", // 47 = ನಲವತ್ತೇಳು
226    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cbe}\u{0cb0}\u{0cc1}", // 46 = ನಲವತ್ತಾರು
227    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cd6}\u{0ca6}\u{0cc1}", // 45 = ನಲವತ್ತೈದು (decomposed)
228    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 44 = ನಲವತ್ತ್ ನಾಲ್ಕು
229    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 43 = ನಲವತ್ತ್ ಮೂರು
230    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0c8e}\u{0cb0}\u{0ca1}\u{0cc1}", // 42 = ನಲವತ್ತ್ ಎರಡು
231    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc6}\u{0cc2}\u{0c82}\u{0ca6}\u{0cc1}", // 41 = ನಲವತ್ತೊಂದು (decomposed)
232    "\u{0ca8}\u{0cb2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 40 = ನಲವತ್ತು
233    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0c92}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 39 = ಮೂವತ್ತ್ ಒಂಬತ್ತು
234    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0c8e}\u{0c82}\u{0c9f}\u{0cc1}", // 38 = ಮೂವತ್ಎಂಟು
235    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0c8f}\u{0cb3}\u{0cc1}", // 37 = ಮೂವತ್ಏಳು
236    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0c86}\u{0cb0}\u{0cc1}", // 36 = ಮೂವತ್ಆರು
237    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0c90}\u{0ca6}\u{0cc1}", // 35 = ಮೂವತ್ತ್ ಐದು
238    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 34 = ಮೂವತ್ತ್ ನಾಲ್ಕು
239    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 33 = ಮೂವತ್ತ್ ಮೂರು
240    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0c8e}\u{0cb0}\u{0ca1}\u{0cc1}", // 32 = ಮೂವತ್ತ್ಎರಡು
241    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0c92}\u{0c82}\u{0ca6}\u{0cc1}", // 31 = ಮೂವತ್ತ್ಒಂದು
242    "\u{0cae}\u{0cc2}\u{0cb5}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 30 = ಮೂವತ್ತು
243    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0c92}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 29 = ಇಪ್ಪತ್ತ್ಒಂಬತ್ತು
244    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0c8e}\u{0c82}\u{0c9f}\u{0cc1}", // 28 = ಇಪ್ಪತ್ತ್ಎಂಟು
245    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0c8f}\u{0cb3}\u{0cc1}", // 27 = ಇಪ್ಪತ್ತ್ಏಳು
246    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0c86}\u{0cb0}\u{0cc1}", // 26 = ಇಪ್ಪತ್ತ್ಆರು
247    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0c90}\u{0ca6}\u{0cc1}", // 25 = ಇಪ್ಪತ್ತ್ ಐದು
248    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 24 = ಇಪ್ಪತ್ತ್ ನಾಲ್ಕು
249    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 23 = ಇಪ್ಪತ್ತ್ ಮೂರು
250    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0c8e}\u{0cb0}\u{0ca1}\u{0cc1}", // 22 = ಇಪ್ಪತ್ತ್ ಎರಡು
251    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0ccd}\u{0020}\u{0c92}\u{0c82}\u{0ca6}\u{0cc1}", // 21 = ಇಪ್ಪತ್ತ್ ಒಂದು
252    "\u{0c87}\u{0caa}\u{0ccd}\u{0caa}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 20 = ಇಪ್ಪತ್ತು
253    "\u{0cb9}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cca}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 19 = ಹತ್ತೊಂಬತ್ತು
254    "\u{0cb9}\u{0ca6}\u{0cbf}\u{0ca8}\u{0cc6}\u{0c82}\u{0c9f}\u{0cc1}", // 18 = ಹದಿನೆಂಟು
255    "\u{0cb9}\u{0ca6}\u{0cbf}\u{0ca8}\u{0cc6}\u{0cd5}\u{0cb3}\u{0cc1}", // 17 = ಹದಿನೇಳು (decomposed)
256    "\u{0cb9}\u{0ca6}\u{0cbf}\u{0ca8}\u{0cbe}\u{0cb0}\u{0cc1}", // 16 = ಹದಿನಾರು
257    "\u{0cb9}\u{0ca6}\u{0cbf}\u{0ca8}\u{0cc6}\u{0cd6}\u{0ca6}\u{0cc1}", // 15 = ಹದಿನೈದು (decomposed)
258    "\u{0cb9}\u{0ca6}\u{0cbf}\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 14 = ಹದಿನಾಲ್ಕು
259    "\u{0cb9}\u{0ca6}\u{0cbf}\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 13 = ಹದಿಮೂರು
260    "\u{0cb9}\u{0ca8}\u{0ccd}\u{0ca8}\u{0cc6}\u{0cb0}\u{0ca1}\u{0cc1}", // 12 = ಹನ್ನೆರಡು
261    "\u{0cb9}\u{0ca8}\u{0ccd}\u{0ca8}\u{0cc6}\u{0cc2}\u{0c82}\u{0ca6}\u{0cc1}", // 11 = ಹನ್ನೊಂದು (decomposed)
262    "\u{0cb9}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 10 = ಹತ್ತು
263    "\u{0c92}\u{0c82}\u{0cac}\u{0ca4}\u{0ccd}\u{0ca4}\u{0cc1}", // 9 = ಒಂಬತ್ತು
264    "\u{0c8e}\u{0c82}\u{0c9f}\u{0cc1}", // 8 = ಎಂಟು
265    "\u{0c8f}\u{0cb3}\u{0cc1}", // 7 = ಏಳು
266    "\u{0c86}\u{0cb0}\u{0cc1}", // 6 = ಆರು
267    "\u{0c90}\u{0ca6}\u{0cc1}", // 5 = ಐದು
268    "\u{0ca8}\u{0cbe}\u{0cb2}\u{0ccd}\u{0c95}\u{0cc1}", // 4 = ನಾಲ್ಕು
269    "\u{0cae}\u{0cc2}\u{0cb0}\u{0cc1}", // 3 = ಮೂರು
270    "\u{0c8e}\u{0cb0}\u{0ca1}\u{0cc1}", // 2 = ಎರಡು
271    "\u{0c92}\u{0c82}\u{0ca6}\u{0cc1}", // 1 = ಒಂದು
272    "\u{0cb8}\u{0cca}\u{0ca8}\u{0ccd}\u{0ca8}\u{0cc6}", // 0 = ಸೊನ್ನೆ
273];
274
275const MODIFIERS: [&str; 15] = [
276    "\u{0ccd}", // [0] ್
277    "\u{0cbe}", // [1] ಾ
278    "\u{0cbf}", // [2] ಿ
279    "\u{0cbf}\u{0cd5}", // [3] ೀ  <- two codepoints: never matches a single-char test
280    "\u{0cc0}", // [4] ೀ
281    "\u{0cc1}", // [5] ು
282    "\u{0cc2}", // [6] ೂ
283    "\u{0cc3}", // [7] ೃ
284    "\u{0cc6}", // [8] ೆ
285    "\u{0cc7}", // [9] ೇ
286    "\u{0cc8}", // [10] ೈ
287    "\u{0cca}", // [11] ೊ
288    "\u{0ccb}", // [12] ೋ
289    "\u{0ccc}", // [13] ೌ
290    "\u{0cd5}", // [14] ೕ
291];
292
293const ORDINAL_SUFFIX: &str = "\u{0ca8}\u{0cc7}"; // ನೇ
294const GENITIVE: &str = "\u{0ca6}"; // ದ
295const POINTWORD: &str = "\u{0cac}\u{0cbf}\u{0c82}\u{0ca6}\u{0cc1}"; // ಬಿಂದು
296const HUNDRED: &str = "\u{0ca8}\u{0cc2}\u{0cb0}\u{0cc1}"; // ನೂರು
297const THOUSAND: &str = "\u{0cb8}\u{0cbe}\u{0cb5}\u{0cbf}\u{0cb0}"; // ಸಾವಿರ
298const LAKH: &str = "\u{0cb2}\u{0c95}\u{0ccd}\u{0cb7}"; // ಲಕ್ಷ
299const CRORE: &str = "\u{0c95}\u{0cca}\u{0cd5}\u{0c9f}\u{0cbf}"; // ಕೋಟಿ (decomposed)
300
301// --- Currency ------------------------------------------------------------
302//
303// See the "Currency" section of the module docs: this is `Num2Word_EUR`'s
304// CURRENCY_FORMS *as mutated in place by `Num2Word_EN.__init__`*, which is the
305// dict KN actually reads at call time. Dumped from the live interpreter
306// (`CONVERTER_CLASSES["kn"].CURRENCY_FORMS`) rather than transcribed from
307// lang_EUR.py, because lang_EUR.py is not what KN ends up seeing.
308//
309// Non-ASCII is escaped for the same reason the card tables are (see "Unicode"
310// above): the escapes survive tools that would NFC-normalize literals.
311
312/// `(code, unit_forms, subunit_forms)` -- 39 entries.
313///
314/// Arity is load-bearing: `pluralize` indexes into these, and PLN/RON carry a
315/// third form that must not be dropped even though EUR's `pluralize` never
316/// reaches index 2.
317const CURRENCY_FORMS: [(&str, &[&str], &[&str]); 39] = [
318    ("AED", &["dirham", "dirhams"], &["fils", "fils"]),
319    ("AUD", &["dollar", "dollars"], &["cent", "cents"]),
320    ("BHD", &["dinar", "dinars"], &["fils", "fils"]),
321    ("BRL", &["real", "reais"], &["cent", "cents"]),
322    ("BYN", &["rouble", "roubles"], &["kopek", "kopeks"]),
323    ("CAD", &["dollar", "dollars"], &["cent", "cents"]),
324    ("CHF", &["franc", "francs"], &["rappen", "rappen"]),
325    ("CNY", &["yuan", "yuan"], &["fen", "fen"]),
326    ("EEK", &["kroon", "kroons"], &["sent", "senti"]),
327    ("EUR", &["euro", "euros"], &["cent", "cents"]),
328    ("GBP", &["pound", "pounds"], &["penny", "pence"]),
329    ("HKD", &["dollar", "dollars"], &["cent", "cents"]),
330    ("HUF", &["forint", "forint"], &["fill\u{00e9}r", "fill\u{00e9}r"]), // filler
331    ("INR", &["rupee", "rupees"], &["paisa", "paise"]),
332    ("IQD", &["dinar", "dinars"], &["fils", "fils"]),
333    ("ISK", &["kr\u{00f3}na", "kr\u{00f3}nur"], &["aur", "aurar"]), // krona/kronur
334    ("JOD", &["dinar", "dinars"], &["fils", "fils"]),
335    ("JPY", &["yen", "yen"], &["sen", "sen"]),
336    ("KRW", &["won", "won"], &["jeon", "jeon"]),
337    ("KWD", &["dinar", "dinars"], &["fils", "fils"]),
338    ("LTL", &["litas", "litas"], &["cent", "cents"]),
339    ("LVL", &["lat", "lats"], &["santim", "santims"]),
340    ("LYD", &["dinar", "dinars"], &["dirham", "dirhams"]),
341    ("MXN", &["peso", "pesos"], &["cent", "cents"]),
342    ("NGN", &["naira", "naira"], &["kobo", "kobo"]),
343    ("NOK", &["krone", "kroner"], &["\u{00f8}re", "\u{00f8}re"]), // ore
344    ("NZD", &["dollar", "dollars"], &["cent", "cents"]),
345    ("OMR", &["rial", "rials"], &["baisa", "baisa"]),
346    ("PLN", &["zloty", "zlotys", "zlotu"], &["grosz", "groszy"]),
347    ("QAR", &["riyal", "riyals"], &["dirham", "dirhams"]),
348    ("RON", &["leu", "lei", "de lei"], &["ban", "bani", "de bani"]),
349    ("RUB", &["rouble", "roubles"], &["kopek", "kopeks"]),
350    ("SAR", &["riyal", "riyals"], &["halalah", "halalas"]),
351    ("SEK", &["krona", "kronor"], &["\u{00f6}re", "\u{00f6}re"]), // ore
352    ("SGD", &["dollar", "dollars"], &["cent", "cents"]),
353    ("TND", &["dinar", "dinars"], &["millime", "millimes"]),
354    ("USD", &["dollar", "dollars"], &["cent", "cents"]),
355    ("UZS", &["sum", "sums"], &["tiyin", "tiyins"]),
356    ("ZAR", &["rand", "rand"], &["cent", "cents"]),
357];
358
359/// `Num2Word_EUR.CURRENCY_ADJECTIVES` -- 16 entries, **not** mutated by anyone
360/// (verified against the live interpreter: it is still EUR's dict, unchanged).
361///
362/// A `const` slice rather than a map: it is consulted only when
363/// `adjective=true`, and 16 entries make a linear scan cheaper than hashing.
364/// Being `const` it also costs nothing to construct.
365const CURRENCY_ADJECTIVES: [(&str, &str); 16] = [
366    ("AUD", "Australian"),
367    ("BYN", "Belarusian"),
368    ("CAD", "Canadian"),
369    ("EEK", "Estonian"),
370    ("HUF", "Hungarian"),
371    ("INR", "Indian"),
372    ("ISK", "\u{00ed}slenskar"), // islenskar
373    ("JPY", "Japanese"),
374    ("KRW", "Korean"),
375    ("MXN", "Mexican"),
376    ("NOK", "Norwegian"),
377    ("RON", "Romanian"),
378    ("RUB", "Russian"),
379    ("SAR", "Saudi"),
380    ("USD", "US"),
381    ("UZS", "Uzbekistan"),
382];
383
384// --- Helpers -------------------------------------------------------------
385
386/// Python's `x[-1] in self.modifiers`.
387///
388/// Python indexes by *character*, so this takes the final `char` and tests it
389/// against the table as a one-character string. An empty `s` would be an
390/// `IndexError` in Python; neither call site can produce one (every card word is
391/// non-empty and `merge` only ever concatenates non-empty text), so `false` here
392/// is unreachable rather than a papered-over divergence.
393fn ends_with_modifier(s: &str) -> bool {
394    match s.chars().next_back() {
395        Some(c) => {
396            let mut buf = [0u8; 4];
397            let last: &str = c.encode_utf8(&mut buf);
398            MODIFIERS.contains(&last)
399        }
400        None => false,
401    }
402}
403
404/// Python's `x[:-1]` -- drop the final *character*, not the final byte.
405fn strip_last_char(s: &str) -> String {
406    let mut it = s.chars();
407    it.next_back();
408    it.as_str().to_string()
409}
410
411// --- Language ------------------------------------------------------------
412
413pub struct LangKn {
414    cards: Cards,
415    maxval: BigInt,
416    /// Built once in `new()`. Python resolves `self.CURRENCY_FORMS[code]` via a
417    /// dict lookup on a table that already exists; rebuilding this per call
418    /// would be pure overhead (and made an earlier revision of this port an
419    /// order of magnitude slower than the Python it replaces).
420    currency_forms: HashMap<&'static str, CurrencyForms>,
421}
422
423impl Default for LangKn {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429impl LangKn {
430    pub fn new() -> Self {
431        let mut cards = Cards::new();
432
433        // KN's set_high_numwords:
434        //     for n, word in self.high_numwords: self.cards[10**n] = word
435        // with high_numwords = [(7, crore), (5, lakh), (3, thousand)].
436        for (n, word) in [(7u32, CRORE), (5, LAKH), (3, THOUSAND)] {
437            cards.insert(BigInt::from(10u8).pow(n), word);
438        }
439
440        set_mid_numwords(&mut cards, &[(100, HUNDRED)]);
441        set_low_numwords(&mut cards, &LOW_NUMWORDS);
442
443        // Num2Word_Base.__init__: MAXVAL = 1000 * list(self.cards.keys())[0].
444        // The OrderedDict is filled high -> mid -> low, so key[0] is the highest
445        // card (10^7) and MAXVAL lands on 10^10.
446        let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
447
448        let currency_forms = CURRENCY_FORMS
449            .iter()
450            .map(|(code, unit, subunit)| (*code, CurrencyForms::new(unit, subunit)))
451            .collect();
452
453        LangKn {
454            cards,
455            maxval,
456            currency_forms,
457        }
458    }
459
460    /// Port of `Num2Word_Base.verify_ordinal`.
461    ///
462    /// The float check (`errmsg_floatord`) cannot fire on integer input, so only
463    /// the negative check is modelled.
464    fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
465        if value.is_negative() {
466            return Err(N2WError::Type(format!(
467                "Cannot treat negative num {} as ordinal.",
468                value
469            )));
470        }
471        Ok(())
472    }
473}
474
475impl Lang for LangKn {
476    /// This language's own `to_currency(currency=...)` default,
477    /// read from the live Python signature. Only 44 of 156 use EUR.
478    fn default_currency(&self) -> &str {
479        "EUR"
480    }
481
482    /// This language's own `to_currency(separator=...)` default,
483    /// read from the live Python signature. Base's is ",", but only
484    /// 36 of 149 languages actually use it — most default to " " or a
485    /// conjunction, so inheriting Base's comma silently corrupts them.
486    fn default_separator(&self) -> &str {
487        ","
488    }
489
490    fn cards(&self) -> &Cards {
491        &self.cards
492    }
493
494    fn maxval(&self) -> &BigInt {
495        &self.maxval
496    }
497
498    // negword: KN does not override Num2Word_Base's "(-) " -> trait default.
499    // is_title / exclude_title: KN does not override -> trait defaults.
500
501    fn pointword(&self) -> &str {
502        "ಬಿಂದು"
503    }
504
505    /// Port of `Num2Word_KN.merge`.
506    ///
507    /// ```python
508    /// if lnum == 1 and rnum < 100:  return (rtext, rnum)
509    /// elif 100 > lnum > rnum:       return ("%s-%s" % (ltext, rtext), lnum + rnum)
510    /// elif lnum >= 100 > rnum:
511    ///     if ltext[-1] in self.modifiers:
512    ///         return ("%s %s" % (ltext[:-1], rtext), lnum + rnum)
513    ///     else:
514    ///         return ("%s %s" % (ltext + GENITIVE, rtext), lnum + rnum)
515    /// elif rnum > lnum:             return ("%s %s" % (ltext, rtext), lnum * rnum)
516    /// return ("%s %s" % (ltext, rtext), lnum + rnum)
517    /// ```
518    ///
519    /// The third arm drives the sandhi: a left word ending in a vowel sign loses
520    /// it (100 + 1 -> "ondu nura ondu"), otherwise the genitive "da" is suffixed
521    /// (2000 + 1 -> "eradu savirada ondu").
522    ///
523    /// Python's `100 > lnum > rnum` and `lnum >= 100 > rnum` are chained
524    /// comparisons; they are expanded here into explicit conjunctions.
525    fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
526        let (ltext, lnum) = l;
527        let (rtext, rnum) = r;
528        let hundred = BigInt::from(100);
529
530        if lnum.is_one() && rnum < &hundred {
531            (rtext.to_string(), rnum.clone())
532        } else if &hundred > lnum && lnum > rnum {
533            // Unreachable for KN (every value < 100 is its own card), but ported
534            // to keep the arm structure identical. See quirk 5 in the module docs.
535            (format!("{}-{}", ltext, rtext), lnum + rnum)
536        } else if lnum >= &hundred && &hundred > rnum {
537            if ends_with_modifier(ltext) {
538                (format!("{} {}", strip_last_char(ltext), rtext), lnum + rnum)
539            } else {
540                (format!("{}{} {}", ltext, GENITIVE, rtext), lnum + rnum)
541            }
542        } else if rnum > lnum {
543            (format!("{} {}", ltext, rtext), lnum * rnum)
544        } else {
545            (format!("{} {}", ltext, rtext), lnum + rnum)
546        }
547    }
548
549    /// Port of `Num2Word_KN.to_ordinal`.
550    ///
551    /// ```python
552    /// self.verify_ordinal(value)
553    /// outwords = self.to_cardinal(value)
554    /// if outwords[-1] in self.modifiers:
555    ///     outwords = outwords[:-1]
556    /// return outwords + ORDINAL_SUFFIX
557    /// ```
558    ///
559    /// Strips at most **one** character, unconditionally, whenever the cardinal
560    /// ends in a vowel sign -- which is why the crore ordinal is mangled (quirk 2).
561    /// Cardinals ending in a bare consonant (2000, 10^5) are left alone and simply
562    /// take the suffix.
563    fn to_ordinal(&self, value: &BigInt) -> Result<String> {
564        self.verify_ordinal(value)?;
565        let outwords = self.to_cardinal(value)?;
566        let stem = if ends_with_modifier(&outwords) {
567            strip_last_char(&outwords)
568        } else {
569            outwords
570        };
571        Ok(format!("{}{}", stem, ORDINAL_SUFFIX))
572    }
573
574    /// Port of `Num2Word_KN.to_ordinal_num`.
575    ///
576    /// `"%s%s" % (value, self.to_ordinal(value))` -- the numeral glued to the
577    /// *entire* ordinal phrase with no separator (quirk 3).
578    ///
579    /// `verify_ordinal` runs here *and* again inside `to_ordinal`, exactly as in
580    /// Python; the duplicate check is harmless and observationally identical
581    /// (same TypeError, same message).
582    fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
583        self.verify_ordinal(value)?;
584        Ok(format!("{}{}", value, self.to_ordinal(value)?))
585    }
586
587    // to_year: KN does not override Num2Word_Base.to_year, which delegates to
588    // to_cardinal -> the trait default is correct. There is no BC/CE handling.
589
590    /// `to_ordinal(float/Decimal)` — `verify_ordinal` gates the float domain:
591    /// a fractional value raises TypeError ("Cannot treat float %s as
592    /// ordinal.") before the negative check ("Cannot treat negative num %s
593    /// as ordinal."); -0.0 passes both (`abs(-0.0) == -0.0`). A surviving
594    /// whole value takes the integer ordinal path — base `to_cardinal`
595    /// routes a whole float through `int(value)` — so `5.00` is "ಐದನೇ" and
596    /// `1e+12` still overflows inside `to_cardinal` (MAXVAL is 10**10).
597    fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
598        let i = verify_ordinal_float(value, &ordinal_float_repr(value))?;
599        self.to_ordinal(&i)
600    }
601
602    /// `to_ordinal_num(float/Decimal)` — the same gate, then
603    /// `"%s%s" % (value, self.to_ordinal(value))`: the repr glued to the
604    /// *entire* ordinal phrase ("5.0ಐದನೇ"). `to_ordinal` runs for real, so a
605    /// whole value past MAXVAL ("1E+20") raises OverflowError here, unlike
606    /// suffix-only languages.
607    fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
608        let i = verify_ordinal_float(value, repr_str)?;
609        Ok(format!("{}{}", repr_str, self.to_ordinal(&i)?))
610    }
611
612    // ---- currency --------------------------------------------------------
613    //
614    // `Num2Word_KN` defines *nothing* currency-related itself. Everything is
615    // inherited: the tables from `Num2Word_EUR` (as mutated at import time --
616    // see the module docs), `pluralize` from `Num2Word_EUR`, and
617    // `to_currency`/`to_cheque`/`_money_verbose`/`_cents_verbose`/
618    // `_cents_terse` from `Num2Word_Base`. So only the three data hooks plus
619    // `pluralize` and `lang_name` are overridden here; the rest of the trait
620    // defaults already mirror `Num2Word_Base` exactly.
621
622    /// Drives the NotImplementedError text:
623    /// `Currency code "XXX" not implemented for "Num2Word_KN"`.
624    fn lang_name(&self) -> &str {
625        "Num2Word_KN"
626    }
627
628    fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
629        self.currency_forms.get(code)
630    }
631
632    fn currency_adjective(&self, code: &str) -> Option<&str> {
633        CURRENCY_ADJECTIVES
634            .iter()
635            .find(|(k, _)| *k == code)
636            .map(|(_, v)| *v)
637    }
638
639    // currency_precision: deliberately NOT overridden. `Num2Word_KN` inherits
640    // `Num2Word_Base.CURRENCY_PRECISION`, which is `{}` -- so
641    // `CURRENCY_PRECISION.get(code, 100)` is 100 for *every* code and the trait
642    // default (100) is already exact.
643    //
644    // This looks wrong for KWD/BHD (3-decimal) and JPY (0-decimal), and it is
645    // -- but it is what Python does, and the corpus agrees. The reason is quirk
646    // 7 in the module docs: `Num2Word_EN.__init__` *assigns*
647    // `self.CURRENCY_PRECISION = {...}`, which binds an INSTANCE attribute on
648    // the EN object only, whereas its sibling `self.CURRENCY_FORMS[k] = v`
649    // lines MUTATE the shared class dict. So EN's forms leak into KN but EN's
650    // precisions do not. Consequently KN renders KWD 12.34 as
651    // "<12> dinars, <34> fils" (divisor 100, not 1000) and JPY 12.34 with a
652    // sen segment instead of rounding to a whole yen. Verified against the
653    // frozen corpus for all of JPY/KWD/BHD.
654
655    /// Port of `Num2Word_EUR.pluralize`.
656    ///
657    /// ```python
658    /// def pluralize(self, n, forms):
659    ///     form = 0 if n == 1 else 1
660    ///     return forms[form]
661    /// ```
662    ///
663    /// Index 2 is never reached, so PLN's "zlotu" and RON's "de lei"/"de bani"
664    /// are dead weight for KN -- but the arity is kept (see [`CURRENCY_FORMS`]).
665    ///
666    /// The out-of-range arm mirrors Python's `IndexError`, message included --
667    /// `forms` is a tuple in Python, so the text is exactly
668    /// "tuple index out of range". It is unreachable with the tables above
669    /// (every entry has >= 2 forms), but `forms` is a caller-supplied slice, so
670    /// the failure mode is modelled rather than papered over with a panic or a
671    /// silent fallback.
672    fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
673        let form = if n.is_one() { 0 } else { 1 };
674        forms
675            .get(form)
676            .cloned()
677            .ok_or_else(|| N2WError::Index("tuple index out of range".into()))
678    }
679
680    // money_verbose / cents_verbose: Num2Word_Base's versions just delegate to
681    // to_cardinal, which is what the trait defaults do. Not overridden.
682    //
683    // cents_terse: Num2Word_Base._cents_terse zero-pads to
684    // `len(str(divisor)) - 1` digits; with divisor 100 that is "%02d". The
685    // trait default calls default_cents_terse(n, self.currency_precision(c)),
686    // which is the same function. Not overridden.
687    //
688    // cardinal_from_decimal: left at the default (raises NotImplemented). It is
689    // reached only for fractional cents -- e.g. to_currency(12.345, "EUR"),
690    // where Python's base falls through to to_cardinal_float(34.5) and yields
691    // "<34> <pointword> <5> cents". That is the float/Decimal cardinal path,
692    // which PORTING_CURRENCY.md defers to a later phase; no corpus row reaches
693    // it (no corpus currency value has a third decimal). Flagged in the report.
694}
695
696/// Python's `Num2Word_Base.verify_ordinal` over the float/Decimal domain:
697///
698/// ```python
699/// if not value == int(value):
700///     raise TypeError(self.errmsg_floatord % value)
701/// if not abs(value) == value:
702///     raise TypeError(self.errmsg_negord % value)
703/// ```
704///
705/// The float check runs first, so `-1.5` reports the *float* message; the
706/// negative check compares numerically, so `-0.0` (== `abs(-0.0)`) passes.
707/// `repr` is Python's `str(value)`, interpolated verbatim into the message.
708fn verify_ordinal_float(value: &FloatValue, repr: &str) -> Result<BigInt> {
709    match value.as_whole_int() {
710        None => Err(N2WError::Type(format!(
711            "Cannot treat float {} as ordinal.",
712            repr
713        ))),
714        Some(i) if i.is_negative() => Err(N2WError::Type(format!(
715            "Cannot treat negative num {} as ordinal.",
716            repr
717        ))),
718        Some(i) => Ok(i),
719    }
720}
721
722/// Best-effort `str(value)` for the TypeError messages raised by
723/// [`verify_ordinal_float`] when no repr was handed in (the `to_ordinal`
724/// entry). Exact for every reachable message: `str(Decimal)` is the spec
725/// transcription, a fractional float's shortest round-trip matches Rust's
726/// `Display` in the non-exponent range, and a whole float re-gains its
727/// Python ".0" tail. (A whole float >= 1e16 would diverge — Python uses
728/// exponent form — but such values pass verification and never reach a
729/// message.)
730fn ordinal_float_repr(value: &FloatValue) -> String {
731    match value {
732        FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
733        FloatValue::Float { value, .. } => {
734            if value.is_finite() && value.fract() == 0.0 && value.abs() < 1e16 {
735                format!("{:.1}", value)
736            } else {
737                value.to_string()
738            }
739        }
740    }
741}