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}