num2words2_core/lang_dv.rs
1//! Port of `lang_DV.py` (Dhivehi / Maldivian).
2//!
3//! Shape: **self-contained**. `Num2Word_DV` inherits from nothing — it is a
4//! bare `class Num2Word_DV:` with no base class at all, so `base.py`'s engine
5//! (`splitnum`/`clean`/`merge`/`set_high_numwords` on `Num2Word_Base`) is
6//! never involved. Every trait method here is an override; `cards()`,
7//! `maxval()` and `merge()` stay at their `Lang` defaults and are unused.
8//!
9//! Note that `Num2Word_DV` defines its *own* `cards`, `MAXVAL` and
10//! `set_high_numwords` that merely share names with `base.py`'s — they are
11//! unrelated tables driving the module's own `convert_int`. They are modelled
12//! as private fields/consts below rather than through the `Lang` trait, to
13//! avoid the engine accidentally picking them up.
14//!
15//! # The numbering system
16//!
17//! Dhivehi groups digits Indian-style: units take 3 digits, then thousands
18//! take 2 (`ހާސް`), then lakh takes 1 (`ލައްކަ`), and everything above that
19//! reverts to Western 3-digit groups (million, billion, ... nonillion). That
20//! is exactly what `self.grouping` encodes:
21//!
22//! ```text
23//! [-3, -5, -6, -9, -12, -15, -18, -21, -24, -27, -30, -33, -36]
24//! ```
25//!
26//! `convert_int` pairs `[len(digits)] + grouping` pairwise into `(end, start)`
27//! and slices `digits_str[start:end]`, yielding
28//! `parts[0] = last 3 digits`, `parts[1] = digits[-5:-3]` (thousands),
29//! `parts[2] = digits[-6:-5]` (lakh), `parts[3] = digits[-9:-6]` (million), …
30//! `parts[1..]` is then `zip`ped against `reversed(self.cards.items())`, i.e.
31//! the multiplier words in ascending order. `zip` stops at the shorter side,
32//! so `parts[12]` (`digits[-36:-33]`) is never consumed — unreachable anyway,
33//! since MAXVAL caps input at 33 digits.
34//!
35//! # Stem vs nominal
36//!
37//! Every numeral has two forms: a *stem* (bound/attributive, `base_stem`) and
38//! a *nominal* (free-standing, `base_nominal`). Cardinals end in the nominal
39//! form, ordinals in the stem form — that is the whole of the `nominal` flag
40//! threaded through `convert_int`. So `to_cardinal(3)` is `ތިނެއް` while
41//! `to_ordinal(3)` is `ތިން ވަނަ`.
42//!
43//! # Faithfully reproduced Python quirks
44//!
45//! This is a port, not a rewrite. All of the following are verified against
46//! the interpreter and against `bench/corpus.jsonl`:
47//!
48//! 1. **Negative cardinals emit a double space.** When `part_words` is empty
49//! (any |value| < 1000 with a non-zero last group) `convert_int` returns
50//! `"".join([]) + " " + word`, i.e. a string with a *leading* space. The
51//! non-negative path launders this via `result.strip()`, but the `sign == 1`
52//! path does `" ".join([negword, result])` with **no strip**, so
53//! `to_cardinal(-1)` == `"މައިނަސް އެކެއް"` — two spaces. `to_cardinal(-1000)`
54//! has only one, because `parts[0] == "000"` takes the early `int(parts[0])
55//! == 0` return, which has no leading space. Reproduced in
56//! [`LangDv::to_cardinal_float`].
57//!
58//! 2. **`convert_three2nominal` mixes stem and nominal for hundreds.** The
59//! `x00` branch uses `convert_two2nominal(digits_str[0])` while the `xyz`
60//! branch uses `convert_two2stem(digits_str[0])`. So cardinal 300 is
61//! `ތިނެއްސަތޭކަ` (*nominal* three + hundred) but cardinal 555 is
62//! `ފަސްސަތޭކަ ފަންސާސްފަހެއް` (*stem* five + hundred). Almost certainly a
63//! typo for `convert_two2stem`, but it is what Python emits.
64//!
65//! 3. **Hundreds spacing is asymmetric.** 100s and 200s concatenate with no
66//! separator (`ސަތޭކަ` + tail), while 300–900 insert a space before the tail
67//! (`convert_two2stem(d0) + base_stem[100] + " " + tail`). Hence 101 →
68//! `ސަތޭކައެކެއް` (no space) but 555 → `ފަސްސަތޭކަ ފަންސާސްފަހެއް` (space).
69//!
70//! 4. **The overflow bound is not `MAXVAL`.** `MAXVAL` is 10^33, but the guard
71//! reads
72//!
73//! ```python
74//! if Decimal(self.MAXVAL).compare(abs(decimal_value).to_integral(ROUND_FLOOR)) < 1:
75//! ```
76//!
77//! and `Decimal.__abs__` is a **context operation**: it rounds to the
78//! default context precision of 28 significant digits with ROUND_HALF_EVEN.
79//! (`__init__` has `getcontext().prec = 34` sitting commented out, so this
80//! is a latent bug the author half-noticed.) A 33-digit input is therefore
81//! rounded *up* to 10^33 before the comparison, and `compare(...) < 1` means
82//! `MAXVAL <= rounded`, so it raises. The true threshold is
83//! `abs(value) >= 10^33 - 50000`: `to_cardinal(10**33 - 50001)` succeeds but
84//! `to_cardinal(10**33 - 50000)` raises `OverflowError` (the tie rounds to
85//! even, i.e. up). `ROUND_FLOOR` on `to_integral` is a red herring — `abs`
86//! has already made the value integral, so `to_integral` is a no-op.
87//! Modelled by [`round_half_even_28`]. Crucially the *conversion* still uses
88//! the exact digits — only the guard sees the rounded value.
89//!
90//! 5. `to_ordinal`/`to_ordinal_num` raise `TypeError` (not ValueError) for any
91//! negative input, via `verify_ordinal`. `to_cardinal` is unaffected.
92//!
93//! # Hazard: the overflow bound depends on *global* interpreter state
94//!
95//! [`CTX_PREC`] is pinned to 28 — the pristine `decimal` default, and the
96//! value the frozen corpus was generated under. But `getcontext()` is global
97//! (thread-local) mutable state, and `lang_AR.py:408` mutates it and never
98//! restores it:
99//!
100//! ```python
101//! except decimal.InvalidOperation:
102//! decimal.getcontext().prec = len(temp_number_dec.as_tuple().digits)
103//! ```
104//!
105//! That handler fires for any input with more digits than the current
106//! precision, so a single `Num2Word_AR().to_cardinal(10**33 - 1)` earlier in
107//! the *same process* raises the global precision to 33 — after which
108//! `Num2Word_DV().to_cardinal(10**33 - 1)` stops raising `OverflowError` and
109//! happily returns a string. Same object, same input, different answer,
110//! depending on whether an unrelated language ran first.
111//!
112//! This port is deliberately order-independent and reproduces the pristine
113//! (prec == 28) behaviour. If a differential harness ever runs AR-then-DV in
114//! one interpreter, Python — not this file — is what changed.
115//!
116//! # Currency
117//!
118//! `to_currency` shares nothing with `base.py`'s. The signature is
119//!
120//! ```python
121//! def to_currency(self, value, currency="ރުފިޔާ", cents="ލާރި"):
122//! ```
123//!
124//! — `currency` and `cents` are **words**, not an ISO code and a verbosity
125//! flag. `Num2Word_DV` has no `CURRENCY_FORMS`, no `CURRENCY_PRECISION`, no
126//! `pluralize`, and no `separator`/`adjective` kwargs, so those trait hooks all
127//! stay at their defaults and are never consulted.
128//!
129//! The dispatcher passes `currency=<ISO code>` straight through, and DV
130//! interpolates whatever it is given **verbatim** as the unit word: there is no
131//! lookup, so `to_currency(1, currency="EUR")` is `"އެއް EUR"` and no code can
132//! ever raise `NotImplementedError`. Every code is "supported", which is
133//! exactly what the corpus records for all nine it tries. Omitting `currency=`
134//! instead yields the class's own default word — `to_currency(1)` is
135//! `"އެއް ރުފިޔާ"` (rufiyaa), the real Maldivian unit — which is what the
136//! generated [`Lang::default_currency`] override carries; the binding resolves
137//! it before `to_currency` runs, so the `&str` arriving here is already right.
138//!
139//! Continuing the quirk list above, all verified against the interpreter:
140//!
141//! 6. **Cents use ROUND_HALF_EVEN and go negative.** `int_part` is
142//! `value.to_integral_value()` — round to *nearest*, ties to even — not
143//! floor, and `frac_part` is `(value - int_part) * 100` after the same
144//! rounding. So 99.99 rounds **up** to 100 and leaves `frac_part == -1`,
145//! printing `"ސަތޭކަ EUR މައިނަސް އެއް ލާރި"`: "one hundred EUR **minus** one
146//! laari", carrying quirk 1's double space along with it (`to_cardinal_float`
147//! is what renders that -1). 1234.56 likewise becomes 1235 EUR minus 44
148//! laari. And 0.5 ties to even, so `int_part` is 0 and it prints "fifty
149//! laari" with no unit word at all.
150//!
151//! 7. **Either segment vanishes when its part is zero.** Both are guarded by a
152//! bare truthiness test on a `Decimal`, so 0.01 is `"އެއް ލާރި"` (no unit
153//! word) and 1.0 is `"އެއް EUR"` (no cents). When both parts round to zero
154//! but the value itself is non-zero — 0.001, say — `" ".join([])` yields the
155//! **empty string**.
156//!
157//! 8. **A positive Decimal exponent survives `to_integral_value`, so large
158//! floats collapse to their coefficient.** `to_integral_value` returns a
159//! value whose exponent is `>= 0` *unchanged*, and `to_cardinal_float` then
160//! reads `as_tuple().digits` — the coefficient alone. `str(1e21)` is
161//! `'1e+21'`, i.e. `Decimal('1E+21')` with digits `(1,)` and exponent 21, so
162//! `to_currency(1e21)` renders **`"އެއް EUR"`** — "one EUR". The exponent is
163//! silently dropped. Only the float path can reach this: `Decimal(int)`
164//! always has exponent 0, which is why the verified integer modes are
165//! immune. Modelled by keeping the currency path on `BigDecimal` and reading
166//! its `(coefficient, exponent)` rather than flattening to a `BigInt` — see
167//! [`LangDv::to_cardinal_float_dec`].
168//!
169//! `to_cheque` does not exist on the class at all, so it surfaces as
170//! `AttributeError` from the dispatcher's `getattr`, not as the
171//! `NotImplementedError` that `default_to_cheque` would produce.
172//!
173//! # Float/Decimal routing
174//!
175//! DV has no whole-value fast path: `to_cardinal(5.0)` goes through
176//! `Decimal(str(5.0))` == `Decimal('5.0')`, whose `as_tuple()` has exponent
177//! -1, so the pointword branch runs and the result is "five point zero"
178//! (`ފަހެއް ޕޮއިންޓް ސުމެއް`). [`Lang::cardinal_float_entry`] is therefore
179//! overridden to bypass the base's whole -> int routing entirely, and the
180//! ordinal/year entries follow the class's own methods. More reproduced
181//! quirks, all corpus-verified:
182//!
183//! 9. **`str(float)`'s scientific regime collapses to the coefficient.**
184//! `str(1e16)` is `'1e+16'`, so `Decimal(str(1e16))` has digits `(1,)`
185//! and exponent 16 — and `convert_int` only ever sees the digit tuple, so
186//! `to_cardinal(1e16)` is `އެކެއް` ("one"). Same for `Decimal('1E+2')` ->
187//! "one". Reconstructed in [`py_float_dec_tuple`] from Rust's
188//! shortest-round-trip digits (`{:e}`), which match `repr(float)`'s
189//! digits by uniqueness of the shortest representation; the regime
190//! boundary (scientific iff adjusted exponent >= 16 or < -4) is
191//! CPython's `float_repr_style` cutoff.
192//! 10. **Whole floats keep their `.0`.** A fixed-point repr of an integral
193//! float appends ".0", giving the Decimal a trailing zero digit and
194//! exponent -1 — hence the trailing `ސުމެއް`/`ސުން` after the pointword
195//! in every `N.0` row.
196//! 11. **`cardinal(0.0)`, `cardinal(-0.0)` and `cardinal(".5")` raise
197//! IndexError**: `digits[:exponent]` is empty and `convert_int` dies on
198//! `parts[0]` — before the sign is even looked at, which is why `-0.0`
199//! raises rather than printing a negword.
200//! 12. **`verify_ordinal` raises TypeError for non-integral floats first**,
201//! then for negatives (also TypeError); `-0.0` passes both checks
202//! (`-0.0 == int(-0.0)`, `abs(-0.0) == -0.0`) and dies later with
203//! cardinal's IndexError — except in `to_ordinal_num`, which happily
204//! formats it as `-0.0 ވަނަ`.
205//! 13. **Ordinal floats mix nominal and stem.** `to_cardinal_float(value,
206//! nominal=False)` hardcodes the *integer* part nominal — Python calls
207//! `convert_int(digits[:exponent])` with the default — while the
208//! fractional digits follow the flag as stems: `to_ordinal(1.0)` is
209//! `އެކެއް ޕޮއިންޓް ސުން ވަނަ` (nominal one, stem zero).
210//! 14. **`to_year` in [1100, 2000) splits float years absurdly**:
211//! `to_year(1234.0)` computes `high, low = value // 100, value % 100` ==
212//! `12.0, 34.0` (both still floats!) and renders "twelve point zero
213//! hundred thirty-four point zero". Negative years go through `abs()`
214//! *before* the cardinal, so no negword and no double space:
215//! `to_year(-1.5)` == `އެކެއް ޕޮއިންޓް ފަހެއް ބީ.ސީ`.
216//!
217//! # Strings
218//!
219//! `str_to_number` is `to_decimal`, which turns `InvalidOperation` into
220//! `TypeError("{} is not a valid value.")` — that is the whole override.
221//! One deliberate shortcut: Python parses `"NaN"` into `Decimal('NaN')`
222//! successfully and only raises later, when `to_cardinal_float`'s overflow
223//! guard evaluates `Decimal(MAXVAL).compare(NaN) < 1` (ordered comparison
224//! with a NaN Decimal -> `decimal.InvalidOperation`). The pyo3 shim
225//! hardcodes `ParsedNumber::NaN -> ValueError` before any DV hook could
226//! run, so this port raises the InvalidOperation from `str_to_number`
227//! itself — the same observable exception for every corpus row ("NaN" only
228//! appears under `to='cardinal'`). The one place the shortcut is visibly
229//! unfaithful is `to_ordinal("NaN")`, where genuine Python raises
230//! ValueError from `int(Decimal('NaN'))` — a row no corpus exercises.
231//!
232//! # Fraction
233//!
234//! `Num2Word_DV` has no `to_fraction` — like `to_cheque`, the dispatcher's
235//! attribute lookup fails, so every fraction-corpus row and every "n/d"
236//! string (whatever `to=` says) is `AttributeError`, including `"1/0"`,
237//! which dies on lookup before any division could raise ZeroDivisionError.
238//! Known gap: `to='fraction'` with a *plain* numeric string ("5", "1.5")
239//! cannot be reproduced from this file — the pyo3 shim's `dec_mode`
240//! hardcodes the base-class TypeError (`to_fraction() missing 1 required
241//! positional argument`) for that combination and never consults the
242//! language, while Python raises AttributeError on the `getattr`.
243//!
244//! # Grammatical kwargs
245//!
246//! `to_cardinal(value, nominal=True)` is the only extra cardinal kwarg;
247//! Python tests it with bare truthiness (`if not nominal`), so False, 0,
248//! None and "" all select the stem form. `to_year`'s `suffix=""` rides
249//! `to_year_kw`; a truthy suffix overrides the implicit `bcword` on
250//! negative years. Everything else falls back to Python via the
251//! `kw.only` guard.
252//!
253//! `cardinal_from_decimal` stays at its default: DV's `to_currency` never
254//! produces fractional cents, since `frac_part` is always run through
255//! `to_integral_value`.
256
257use crate::base::{Kwargs, KwVal, Lang, N2WError, Result};
258use crate::currency::CurrencyValue;
259use crate::floatpath::FloatValue;
260use crate::strnum::{python_decimal_parse, ParsedNumber};
261use bigdecimal::BigDecimal;
262use num_bigint::BigInt;
263use num_integer::Integer;
264use num_traits::{Signed, Zero};
265
266const NEGWORD: &str = "މައިނަސް";
267const ORDWORD: &str = "ވަނަ";
268const BCWORD: &str = "ބީ.ސީ";
269
270/// `to_currency`'s `cents=` default: the *word* ލާރި (laari, 1/100 rufiyaa),
271/// not a verbosity flag. See the `to_currency` impl for why the trait's bool
272/// cannot carry it.
273const CENTSWORD: &str = "ލާރި";
274
275/// `self.high_numwords`, in source order. `set_high_numwords` zips this with
276/// `range(3 + 3*len(high), 3, -3)` == `30, 27, …, 6`, so index 0 is 10^30.
277const HIGH_NUMWORDS: [&str; 9] = [
278 "ނޮނިލި",
279 "އޮކްޓިލި",
280 "ސެޕްޓިލި",
281 "ސެކްސްޓިލި",
282 "ކުއިންޓިލި",
283 "ކުއަޑްރި",
284 "ޓްރި",
285 "ބިލި",
286 "މިލި",
287];
288
289/// The suffix `set_high_numwords` appends to every high numword.
290const HIGH_SUFFIX: &str = "ޔަން";
291
292/// `self.grouping`, i.e. `[-3, -5] + [(x * 3 + 6) * -1 for x in range(len(self.cards))]`
293/// with `len(self.cards) == 11`.
294const GROUPING: [isize; 13] = [
295 -3, -5, -6, -9, -12, -15, -18, -21, -24, -27, -30, -33, -36,
296];
297
298/// Number of significant digits `Decimal.__abs__` rounds to (the default
299/// context precision, which `__init__` leaves untouched).
300const CTX_PREC: usize = 28;
301
302/// `self.MAXVAL` == `list(self.cards.keys())[0] * 1000` == 10^30 * 1000.
303const MAXVAL_EXP: usize = 33;
304
305pub struct LangDv {
306 /// `reversed(self.cards.items())` — multiplier words in ascending order of
307 /// their key, which is the order `convert_int` zips `parts[1:]` against.
308 /// Only the word is ever read (the comprehension binds `base` and drops
309 /// it), so the keys are not carried.
310 mult_words_asc: Vec<String>,
311 maxval: BigInt,
312}
313
314pub fn new() -> LangDv {
315 LangDv::new()
316}
317
318impl Default for LangDv {
319 fn default() -> Self {
320 LangDv::new()
321 }
322}
323
324impl LangDv {
325 pub fn new() -> LangDv {
326 // Mirror of `set_high_numwords` + the two explicit inserts. Python
327 // builds `cards` descending (10^30 … 10^6, then 100000, then 1000);
328 // `convert_int` only ever consumes it reversed, so store it ascending.
329 let mut cards_desc: Vec<String> = HIGH_NUMWORDS
330 .iter()
331 .map(|w| format!("{}{}", w, HIGH_SUFFIX))
332 .collect();
333 cards_desc.push("ލައްކަ".to_string()); // self.cards[100000]
334 cards_desc.push("ހާސް".to_string()); // self.cards[1000]
335
336 let mut mult_words_asc = cards_desc;
337 mult_words_asc.reverse();
338
339 LangDv {
340 mult_words_asc,
341 maxval: pow10(MAXVAL_EXP),
342 }
343 }
344
345 /// `self.base_stem` — bound/attributive forms. Missing key => `KeyError`.
346 fn base_stem(&self, k: i64) -> Result<&'static str> {
347 Ok(match k {
348 0 => "ސުން",
349 1 => "އެއް",
350 2 => "ދެ",
351 3 => "ތިން",
352 4 => "ހަތަރު",
353 5 => "ފަސް",
354 6 => "ހަ",
355 7 => "ހަތް",
356 8 => "އައް",
357 9 => "ނުވަ",
358 10 => "ދިހަ",
359 100 => "ސަތޭކަ",
360 200 => "ދުއިސައްތަ",
361 _ => return Err(N2WError::Key(k.to_string())),
362 })
363 }
364
365 /// `self.base_nominal` — free-standing forms. Missing key => `KeyError`.
366 fn base_nominal(&self, k: i64) -> Result<&'static str> {
367 Ok(match k {
368 0 => "ސުމެއް",
369 1 => "އެކެއް",
370 2 => "ދޭއް",
371 3 => "ތިނެއް",
372 4 => "ހަތަރެއް",
373 5 => "ފަހެއް",
374 6 => "ހައެއް",
375 7 => "ހަތެއް",
376 8 => "އަށެއް",
377 9 => "ނުވައެއް",
378 10 => "ދިހައެއް",
379 11 => "އެގާރަ",
380 12 => "ބާރަ",
381 13 => "ތޭރަ",
382 14 => "ސައުދަ",
383 15 => "ފަނަރަ",
384 16 => "ސޯޅަ",
385 17 => "ސަތާރަ",
386 18 => "އަށާރަ",
387 19 => "ނަވާރަ",
388 20 => "ވިހި",
389 21 => "އެކާވީސް",
390 22 => "ބާވީސް",
391 23 => "ތޭވީސް",
392 24 => "ސައުވީސް",
393 25 => "ފަންސަވީސް",
394 26 => "ސައްބީސް",
395 27 => "ހަތާވީސް",
396 28 => "އަށާވީސް",
397 29 => "ނަވާވީސް",
398 30 => "ތިރީސް",
399 40 => "ސާޅީސް",
400 50 => "ފަންސާސް",
401 60 => "ފަސްދޮޅަސް",
402 70 => "ހަތްދިހަ",
403 80 => "އައްޑިހަ",
404 90 => "ނުވަދިހަ",
405 _ => return Err(N2WError::Key(k.to_string())),
406 })
407 }
408
409 /// `convert_decade2nominal`: `self.base_nominal[int(decade) * 10]`.
410 fn convert_decade2nominal(&self, decade: &str) -> Result<&'static str> {
411 self.base_nominal(py_int(decade)? * 10)
412 }
413
414 /// `convert_decade2stem`: 1 => `base_stem[10]`, else the nominal decade.
415 fn convert_decade2stem(&self, decade: &str) -> Result<&'static str> {
416 if py_int(decade)? == 1 {
417 return self.base_stem(10);
418 }
419 self.convert_decade2nominal(decade)
420 }
421
422 /// `convert_two2nominal`.
423 ///
424 /// Unlike its `2stem` sibling this does **not** do `str(int(...))` first,
425 /// so it indexes the caller's string as-is. Callers only ever pass an
426 /// already-trimmed 1-char string or an exactly-2-char `digits_str[1:3]`
427 /// slice, so the `< 30` guard always covers the 1-char case and the
428 /// `digits_str[1]` access below is never out of range.
429 fn convert_two2nominal(&self, digits_str: &str) -> Result<String> {
430 let n = py_int(digits_str)?;
431 if n < 30 {
432 return Ok(self.base_nominal(n)?.to_string());
433 }
434
435 let d1 = char_at(digits_str, 1)?;
436 let d0 = char_at(digits_str, 0)?;
437 if py_int(&d1)? == 0 {
438 return Ok(self.convert_decade2stem(&d0)?.to_string());
439 }
440
441 Ok(format!(
442 "{}{}",
443 self.convert_decade2stem(&d0)?,
444 self.base_nominal(py_int(&d1)?)?
445 ))
446 }
447
448 /// `convert_two2stem`.
449 fn convert_two2stem(&self, digits_str: &str) -> Result<String> {
450 let n = py_int(digits_str)?;
451 let ds = n.to_string(); // `str(int(digits_str))` — trim leading zeros
452
453 if n < 11 {
454 return Ok(self.base_stem(n)?.to_string());
455 }
456 if n < 30 {
457 return Ok(self.base_nominal(n)?.to_string());
458 }
459
460 let d1 = char_at(&ds, 1)?;
461 let d0 = char_at(&ds, 0)?;
462 if py_int(&d1)? == 0 {
463 return Ok(self.convert_decade2stem(&d0)?.to_string());
464 }
465
466 Ok(format!(
467 "{}{}",
468 self.convert_decade2stem(&d0)?,
469 self.base_stem(py_int(&d1)?)?
470 ))
471 }
472
473 /// `convert_three2stem`.
474 ///
475 /// 100/200 have dedicated stems and glue straight onto the tail; 300–900
476 /// are built as `<digit stem> + ސަތޭކަ + " " + <tail>` — note the space,
477 /// which the 100/200 branches do not emit. Quirk 3 in the module docs.
478 fn convert_three2stem(&self, digits_str: &str) -> Result<String> {
479 let ds = py_int(digits_str)?.to_string(); // trim leading zeros
480
481 if ds.len() < 3 {
482 return self.convert_two2stem(&ds);
483 }
484
485 let d0 = char_at(&ds, 0)?;
486 let tail = py_slice(&ds, 1, 3); // digits_str[1:3]
487 let tail_n = py_int(&tail)?;
488
489 if py_int(&d0)? == 1 {
490 if tail_n == 0 {
491 return Ok(self.base_stem(100)?.to_string());
492 }
493 return Ok(format!(
494 "{}{}",
495 self.base_stem(100)?,
496 self.convert_two2stem(&tail)?
497 ));
498 }
499
500 if py_int(&d0)? == 2 {
501 if tail_n == 0 {
502 return Ok(self.base_stem(200)?.to_string());
503 }
504 return Ok(format!(
505 "{}{}",
506 self.base_stem(200)?,
507 self.convert_two2stem(&tail)?
508 ));
509 }
510
511 if tail_n == 0 {
512 return Ok(format!(
513 "{}{}",
514 self.convert_two2stem(&d0)?,
515 self.base_stem(100)?
516 ));
517 }
518
519 Ok(format!(
520 "{}{} {}",
521 self.convert_two2stem(&d0)?,
522 self.base_stem(100)?,
523 self.convert_two2stem(&tail)?
524 ))
525 }
526
527 /// `convert_three2nominal`.
528 ///
529 /// Mirrors `convert_three2stem` except the tails are nominal — **and** the
530 /// `x00` branch uses `convert_two2nominal(digits_str[0])` where the `xyz`
531 /// branch uses `convert_two2stem(digits_str[0])`. That inconsistency is
532 /// quirk 2 in the module docs: cardinal 300 == `ތިނެއްސަތޭކަ` (nominal three)
533 /// but cardinal 555 == `ފަސްސަތޭކަ ފަންސާސްފަހެއް` (stem five). Preserved verbatim.
534 fn convert_three2nominal(&self, digits_str: &str) -> Result<String> {
535 let ds = py_int(digits_str)?.to_string(); // trim leading zeros
536
537 if ds.len() < 3 {
538 return self.convert_two2nominal(&ds);
539 }
540
541 let d0 = char_at(&ds, 0)?;
542 let tail = py_slice(&ds, 1, 3);
543 let tail_n = py_int(&tail)?;
544
545 if py_int(&d0)? == 1 {
546 if tail_n == 0 {
547 return Ok(self.base_stem(100)?.to_string());
548 }
549 return Ok(format!(
550 "{}{}",
551 self.base_stem(100)?,
552 self.convert_two2nominal(&tail)?
553 ));
554 }
555
556 if py_int(&d0)? == 2 {
557 if tail_n == 0 {
558 return Ok(self.base_stem(200)?.to_string());
559 }
560 return Ok(format!(
561 "{}{}",
562 self.base_stem(200)?,
563 self.convert_two2nominal(&tail)?
564 ));
565 }
566
567 if tail_n == 0 {
568 // NOTE: nominal here, stem in the branch below. Python bug, kept.
569 return Ok(format!(
570 "{}{}",
571 self.convert_two2nominal(&d0)?,
572 self.base_stem(100)?
573 ));
574 }
575
576 Ok(format!(
577 "{}{} {}",
578 self.convert_two2stem(&d0)?,
579 self.base_stem(100)?,
580 self.convert_two2nominal(&tail)?
581 ))
582 }
583
584 /// `convert_int`. `digits_str` is the decimal magnitude, i.e. Python's
585 /// `"".join(str(d) for d in digits)` where `digits` came from
586 /// `Decimal.as_tuple()` (sign-free, and exponent 0 for integer input).
587 fn convert_int(&self, digits_str: &str, nominal: bool) -> Result<String> {
588 if digits_str == "0" {
589 return Ok(if nominal {
590 self.base_nominal(0)?.to_string()
591 } else {
592 self.base_stem(0)?.to_string()
593 });
594 }
595
596 // parts = filter(truthy, digits_str[start:end] for end, start in
597 // pairwise([len(digits_str)] + self.grouping))
598 //
599 // Emptiness is monotone here: `s[-a:-b]` is non-empty iff `len(s) > b`,
600 // and the `b`s increase along `grouping`, so the filter only ever trims
601 // a suffix of the sequence and never renumbers a surviving element.
602 let mut bounds: Vec<isize> = Vec::with_capacity(1 + GROUPING.len());
603 bounds.push(digits_str.chars().count() as isize);
604 bounds.extend_from_slice(&GROUPING);
605
606 let mut parts: Vec<String> = Vec::new();
607 for w in bounds.windows(2) {
608 let (end, start) = (w[0], w[1]);
609 let p = py_slice(digits_str, start, end);
610 if !p.is_empty() {
611 parts.push(p);
612 }
613 }
614
615 // `parts[0]` — IndexError if the filter left nothing. Unreachable for
616 // integer input (the magnitude always has >= 1 digit), but this is the
617 // site where float input dies, so it is modelled rather than assumed.
618 let head = match parts.first() {
619 Some(h) => h.clone(),
620 None => return Err(N2WError::Index("list index out of range".into())),
621 };
622
623 // zip(parts[1:], reversed(self.cards.items())) — stops at the shorter.
624 let mut part_words: Vec<String> = Vec::new();
625 for (num, word) in parts.iter().skip(1).zip(self.mult_words_asc.iter()) {
626 if py_int(num)? != 0 {
627 part_words.push(format!("{}{}", self.convert_three2stem(num)?, word));
628 }
629 }
630 part_words.reverse();
631
632 if py_int(&head)? == 0 {
633 return Ok(part_words.join(" "));
634 }
635
636 // NOTE: when `part_words` is empty this yields a leading space. The
637 // caller strips it only on the non-negative path — see quirk 1.
638 let tail = if nominal {
639 self.convert_three2nominal(&head)?
640 } else {
641 self.convert_three2stem(&head)?
642 };
643 Ok(format!("{} {}", part_words.join(" "), tail))
644 }
645
646 /// `to_cardinal_float`, restricted to integer input.
647 fn to_cardinal_float(&self, value: &BigInt, nominal: bool) -> Result<String> {
648 // if Decimal(self.MAXVAL).compare(abs(v).to_integral(ROUND_FLOOR)) < 1
649 //
650 // `compare(x) < 1` means `MAXVAL <= x`. `abs()` rounds to 28
651 // significant digits first (quirk 4), so this is *not* `v >= 10^33`.
652 if round_half_even_28(&value.abs()) >= self.maxval {
653 // Python formats the signed value, not its absolute value.
654 return Err(N2WError::Overflow(format!(
655 "abs({}) must be less than {}.",
656 value, self.maxval
657 )));
658 }
659
660 let result = self.convert_int(&value.abs().to_string(), nominal)?;
661
662 if value.is_negative() {
663 // " ".join([negword, result]) — no strip, hence the double space.
664 return Ok(format!("{} {}", NEGWORD, result));
665 }
666 Ok(result.trim().to_string())
667 }
668
669 /// `to_cardinal_float`, for the currency path — where the argument is an
670 /// already-integral `Decimal` rather than an `int`.
671 ///
672 /// Deliberately *not* folded into [`LangDv::to_cardinal_float`]. That one
673 /// takes a `BigInt`, which flattens a `Decimal` to its full digit
674 /// expansion; the whole point here is that Python does **not** expand —
675 /// `as_tuple()` hands `convert_int` the bare coefficient and the exponent
676 /// is dropped on the floor (quirk 8). `Decimal(int)` always has exponent 0,
677 /// so the two agree on every value the verified integer modes can produce
678 /// and diverge only where Python itself does.
679 fn to_cardinal_float_dec(&self, value: &BigDecimal, nominal: bool) -> Result<String> {
680 // sign, digits, exponent = decimal_value.as_tuple()
681 let (coefficient, scale) = value.as_bigint_and_exponent();
682 let exponent = -scale;
683 let digits = coefficient.abs().to_string();
684
685 if self.overflows(&coefficient, exponent) {
686 // Python formats the *Decimal*, so a positive exponent prints in
687 // scientific notation: "abs(1.5E+33) must be less than ...".
688 return Err(N2WError::Overflow(format!(
689 "abs({}) must be less than {}.",
690 py_decimal_str(value),
691 self.maxval
692 )));
693 }
694
695 // The `exponent < 0` arm of `to_cardinal_float` (the pointword/
696 // convert_discrete branch) is unreachable from here: every caller feeds
697 // this a `to_integral_value()` result, which never has a negative
698 // exponent. So only the `convert_int` arm can run.
699 let result = self.convert_int(&digits, nominal)?;
700
701 if coefficient.is_negative() {
702 // " ".join([negword, result]) — no strip, hence the double space
703 // that quirk 1 describes. This is the path 99.99's frac_part of -1
704 // takes, which is why the corpus shows "މައިނަސް އެއް ލާރި".
705 return Ok(format!("{} {}", NEGWORD, result));
706 }
707 Ok(result.trim().to_string())
708 }
709
710 /// `to_cardinal_float`'s overflow guard for a `Decimal` argument:
711 /// `Decimal(MAXVAL).compare(abs(v).to_integral(ROUND_FLOOR)) < 1`, i.e.
712 /// `MAXVAL <= round_to_28_significant_digits(|v|)` — see quirk 4.
713 ///
714 /// `exponent` is Python's, so `>= 0` for every caller.
715 fn overflows(&self, coefficient: &BigInt, exponent: i64) -> bool {
716 if coefficient.is_zero() {
717 return false;
718 }
719 // |v| >= 10^adjusted, where `adjusted` is the power of ten of the
720 // leading digit. So adjusted >= 33 settles it as MAXVAL <= |v| without
721 // materialising 10^exponent — which a Decimal("1E+999999999") would
722 // otherwise demand, turning Python's instant OverflowError into an OOM.
723 // Exact, not an approximation: rounding to nearest cannot pull a value
724 // that is already >= 10^33 below 10^33, since 10^33 is itself
725 // representable in 28 significant digits.
726 let ndigits = coefficient.abs().to_string().len() as i64;
727 if exponent + ndigits - 1 >= MAXVAL_EXP as i64 {
728 return true;
729 }
730 // |v| < 10^33 now, so the exact integer is cheap. Rounding to 28
731 // significant digits can still carry it up to exactly 10^33 — that is
732 // the whole of quirk 4 — so the full check still has to run.
733 let exact = coefficient.abs() * pow10(exponent.max(0) as usize);
734 round_half_even_28(&exact) >= self.maxval
735 }
736
737 /// `verify_ordinal`. The `value == int(value)` float check cannot fail for
738 /// integer input; only the negative check is reachable, and it raises
739 /// `TypeError` rather than the `ValueError` one might expect.
740 fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
741 if value.is_negative() {
742 return Err(N2WError::Type(format!(
743 "Cannot treat negative num {} as ordinal.",
744 value
745 )));
746 }
747 Ok(())
748 }
749
750 /// `convert_discrete`: render each individual digit as a *free* word. The
751 /// fractional tail of the float path is spelled digit by digit, so this
752 /// never sees anything but single 0-9 characters and neither table can
753 /// miss. `nominal` picks `base_nominal` (cardinals) over `base_stem`; the
754 /// non-nominal branch is only reachable via ordinal-float input, which
755 /// `verify_ordinal` rejects before it can arrive here.
756 fn convert_discrete(&self, digits_str: &str, nominal: bool) -> Result<String> {
757 let mut out: Vec<String> = Vec::new();
758 for ch in digits_str.chars() {
759 let d = py_int(&ch.to_string())?;
760 out.push(if nominal {
761 self.base_nominal(d)?.to_string()
762 } else {
763 self.base_stem(d)?.to_string()
764 });
765 }
766 Ok(out.join(" "))
767 }
768
769 /// The *real* `to_cardinal_float(value, nominal)` body — the one Python
770 /// reaches with genuine float/Decimal input. Not to be confused with
771 /// [`LangDv::to_cardinal_float`] above, which shares the Python name but is
772 /// only ever fed an already-integral argument by the integer/currency
773 /// modes. Both the Float and Decimal arms of the trait entry point reduce
774 /// to `decimal_value.as_tuple()` — `(sign_negative, digit_str, exponent)` —
775 /// and converge here.
776 ///
777 /// `digit_str` is the coefficient with no leading zeros (bar a lone `"0"`);
778 /// `exponent` is Python's `as_tuple` exponent. The overflow guard has
779 /// already fired in the caller.
780 fn cardinal_float_body(
781 &self,
782 sign_negative: bool,
783 digit_str: &str,
784 exponent: i64,
785 nominal: bool,
786 ) -> Result<String> {
787 let result = if exponent < 0 {
788 let n = digit_str.chars().count() as i64;
789 // Python slices the coefficient tuple: `digits[:exponent]` is the
790 // integer part, `digits[exponent:len(digits)]` the fractional one.
791 let cut = n + exponent; // == len(digits) - |exponent|
792
793 // `digits[:exponent]` — empty when |exponent| >= len(digits), which
794 // makes convert_int die with IndexError. That is evaluated first in
795 // Python's join list, so 0.5 / 0.01 / 0.001 / 1.0-with-no-int-part
796 // raise before the fractional digits are ever touched.
797 let int_digits: String = if cut > 0 {
798 digit_str.chars().take(cut as usize).collect()
799 } else {
800 String::new()
801 };
802 // Python hardcodes nominal=True for the integer part here (the
803 // default arg), independent of the flag threaded to the fraction.
804 let int_part = self.convert_int(&int_digits, true)?;
805
806 // `digits[exponent:len(digits)]` — the fractional digits, clamped
807 // to the whole coefficient when |exponent| exceeds its length
808 // (unreachable in practice: int_part has already raised by then).
809 let frac_digits: String = if cut > 0 {
810 digit_str.chars().skip(cut as usize).collect()
811 } else {
812 digit_str.chars().collect()
813 };
814 let frac_part = self.convert_discrete(&frac_digits, nominal)?;
815
816 // " ".join([convert_int(...), pointword, convert_discrete(...)]).
817 // int_part still carries convert_int's leading space; the strip on
818 // the non-negative path below launders it (quirk 1).
819 format!("{} {} {}", int_part, self.pointword(), frac_part)
820 } else {
821 self.convert_int(digit_str, nominal)?
822 };
823
824 if sign_negative {
825 // " ".join([negword, result]) — no strip, so convert_int's leading
826 // space survives as the quirk-1 double space.
827 Ok(format!("{} {}", NEGWORD, result))
828 } else {
829 Ok(result.trim().to_string())
830 }
831 }
832
833 /// `to_cardinal_float`'s overflow guard for genuine float/Decimal input:
834 /// `Decimal(MAXVAL).compare(abs(v).to_integral(ROUND_FLOOR)) < 1`, i.e.
835 /// `MAXVAL <= floor(round_to_28_significant_digits(|v|))` (quirk 4). Unlike
836 /// [`LangDv::overflows`], this floors the fractional part, matching
837 /// `to_integral(ROUND_FLOOR)` — the value here can carry a real fraction.
838 ///
839 /// The 28-digit rounding is a no-op for any float (<= 17 significant digits)
840 /// and for every corpus Decimal, but is reproduced for faithfulness.
841 fn float_path_overflows(&self, digit_str: &str, exponent: i64) -> bool {
842 let d = match BigInt::parse_bytes(digit_str.as_bytes(), 10) {
843 Some(d) if !d.is_zero() => d,
844 _ => return false,
845 };
846 // abs(v) context-rounds |coefficient| to 28 significant digits; scaling
847 // by 10**exponent does not change which digits are significant, so
848 // rounding the coefficient is rounding the value.
849 let d = round_half_even_28(&d);
850 // to_integral(ROUND_FLOOR): floor(d * 10**exponent), with d >= 0.
851 let floored = if exponent >= 0 {
852 d * pow10(exponent as usize)
853 } else {
854 d / pow10((-exponent) as usize) // truncation == floor for d >= 0
855 };
856 floored >= self.maxval
857 }
858
859 /// `to_cardinal_float(value, nominal)` for a value already reduced to its
860 /// `as_tuple()` view: overflow guard, then [`LangDv::cardinal_float_body`].
861 fn cardinal_tuple(
862 &self,
863 sign_negative: bool,
864 digit_str: &str,
865 exponent: i64,
866 nominal: bool,
867 ) -> Result<String> {
868 if self.float_path_overflows(digit_str, exponent) {
869 // Python formats the *Decimal* (`errmsg_toobig.format(decimal_value,
870 // self.MAXVAL)`), so a positive exponent prints scientifically.
871 return Err(N2WError::Overflow(format!(
872 "abs({}) must be less than {}.",
873 py_decimal_str_parts(sign_negative, digit_str, exponent),
874 self.maxval
875 )));
876 }
877 self.cardinal_float_body(sign_negative, digit_str, exponent, nominal)
878 }
879
880 /// The full `to_cardinal_float(value, nominal)` for genuine float/Decimal
881 /// input — `to_decimal` (a no-op tuple reduction here), guard, body.
882 fn cardinal_float_full(&self, value: &FloatValue, nominal: bool) -> Result<String> {
883 let (sign_negative, digit_str, exponent) = float_value_tuple(value);
884 self.cardinal_tuple(sign_negative, &digit_str, exponent, nominal)
885 }
886
887 /// `to_cardinal(f)` for an f64 the *port itself* produced (`to_year`'s
888 /// `high`/`low`), routed through `Decimal(str(f))` like any float input.
889 fn cardinal_from_f64(&self, f: f64, nominal: bool) -> Result<String> {
890 let (sign_negative, digit_str, exponent) = py_float_dec_tuple(f);
891 self.cardinal_tuple(sign_negative, &digit_str, exponent, nominal)
892 }
893
894 /// `verify_ordinal` for float/Decimal input.
895 ///
896 /// Order matters: the `value == int(value)` check runs first, so `-1.5`
897 /// raises the *floatord* TypeError, not the negord one. `-0.0` passes
898 /// both (`-0.0 == 0` and `abs(-0.0) == -0.0`) — quirk 12.
899 fn verify_ordinal_float(&self, value: &FloatValue) -> Result<()> {
900 let integral = match value {
901 FloatValue::Float { value, .. } => *value == value.trunc(),
902 FloatValue::Decimal { value, .. } => {
903 let (coeff, scale) = value.as_bigint_and_exponent();
904 scale <= 0 || (&coeff % pow10(scale as usize)).is_zero()
905 }
906 };
907 if !integral {
908 return Err(N2WError::Type(format!(
909 "Cannot treat float {} as ordinal.",
910 float_value_str(value)
911 )));
912 }
913 let negative = match value {
914 // Numeric `<`, not the sign bit: abs(-0.0) == -0.0 in Python.
915 FloatValue::Float { value, .. } => *value < 0.0,
916 // BigDecimal cannot carry a negative zero, so a negative
917 // coefficient is a genuinely negative value.
918 FloatValue::Decimal { value, .. } => {
919 value.as_bigint_and_exponent().0.is_negative()
920 }
921 };
922 if negative {
923 return Err(N2WError::Type(format!(
924 "Cannot treat negative num {} as ordinal.",
925 float_value_str(value)
926 )));
927 }
928 Ok(())
929 }
930
931 /// `to_year(value, suffix)` for integer input — the shared body behind
932 /// the plain trait hook (`suffix=""`) and `to_year_kw`.
933 fn year_int(&self, value: &BigInt, suffix: &str) -> Result<String> {
934 let mut v = value.clone();
935 let mut suffix = suffix.to_string();
936 if v.is_negative() {
937 v = v.abs();
938 // suffix = self.bcword if not suffix else suffix
939 if suffix.is_empty() {
940 suffix = BCWORD.to_string();
941 }
942 }
943
944 // `v` is non-negative by here, so Rust's truncating `/` and `%` agree
945 // with Python's floor semantics.
946 let hundred = BigInt::from(100);
947 let high = &v / &hundred;
948 let low = &v % &hundred;
949
950 let mut result: Vec<String> = Vec::new();
951 if v < BigInt::from(2000) && v >= BigInt::from(1100) {
952 result.push(self.to_cardinal(&high)?);
953 result.push("ސަތޭކަ".to_string()); // literal in Python, == base_stem[100]
954 result.push(self.to_cardinal(&low)?);
955 } else {
956 result.push(self.to_cardinal(&v)?);
957 }
958
959 if !suffix.is_empty() {
960 result.push(suffix);
961 }
962
963 Ok(result.join(" "))
964 }
965}
966
967impl Lang for LangDv {
968
969 fn python_maxval(&self) -> Option<num_bigint::BigInt> {
970 // Python class attribute MAXVAL (self-contained converter).
971 Some(num_bigint::BigInt::from(10u32).pow(33))
972 }
973 /// `self.pointword`, read from the live Python instance.
974 /// Unused by the four integer modes, so phase 1 never needed it —
975 /// the float path is the first caller.
976 fn pointword(&self) -> &str {
977 "ޕޮއިންޓް"
978 }
979
980 fn lang_name(&self) -> &str {
981 "Num2Word_DV"
982 }
983
984 /// This language's own `to_currency(currency=...)` default,
985 /// read from the live Python signature. Only 44 of 156 use EUR.
986 fn default_currency(&self) -> &str {
987 "ރުފިޔާ"
988 }
989
990 fn negword(&self) -> &str {
991 NEGWORD
992 }
993
994 fn to_cardinal(&self, value: &BigInt) -> Result<String> {
995 self.to_cardinal_float(value, true)
996 }
997
998 fn to_ordinal(&self, value: &BigInt) -> Result<String> {
999 self.verify_ordinal(value)?;
1000 Ok(format!(
1001 "{} {}",
1002 self.to_cardinal_float(value, false)?,
1003 ORDWORD
1004 ))
1005 }
1006
1007 fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
1008 self.verify_ordinal(value)?;
1009 Ok(format!("{} {}", value, ORDWORD))
1010 }
1011
1012 /// `to_year(value, suffix="")`. This plain hook carries no `suffix`;
1013 /// callers passing one go through [`Lang::to_year_kw`]. The default empty
1014 /// string only matters for negative years, where Python substitutes
1015 /// `bcword`.
1016 fn to_year(&self, value: &BigInt) -> Result<String> {
1017 self.year_int(value, "")
1018 }
1019
1020 /// `to_year(value, suffix=...)` — the only extra kwarg the Python
1021 /// signature accepts. `suffix=None` is falsy in `if not suffix`, so it
1022 /// behaves like the default; a non-str suffix would only crash later in
1023 /// `" ".join`, so it is punted back to Python instead of guessed at.
1024 fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1025 if !kw.only(&["suffix"]) {
1026 return Err(N2WError::Fallback("kwargs".into()));
1027 }
1028 let suffix = match kw.get("suffix") {
1029 Option::None | Some(KwVal::None) => String::new(),
1030 Some(KwVal::Str(s)) => s.clone(),
1031 Some(_) => return Err(N2WError::Fallback("kwargs".into())),
1032 };
1033 self.year_int(value, &suffix)
1034 }
1035
1036 /// `to_cardinal(value, nominal=True)` — the extra cardinal kwarg. Python
1037 /// only ever tests it with bare truthiness, so any falsy value selects
1038 /// the stem form.
1039 fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1040 if !kw.only(&["nominal"]) {
1041 return Err(N2WError::Fallback("kwargs".into()));
1042 }
1043 self.to_cardinal_float(value, kw_nominal(kw))
1044 }
1045
1046 /// The genuine float/Decimal cardinal path. `Num2Word_DV.to_cardinal(value)`
1047 /// forwards to `to_cardinal_float(value, nominal=True)`, which converts the
1048 /// value to a `Decimal` (a float via `Decimal(str(value))`, so `2.675`
1049 /// yields the exact string digits `2,6,7,5` — DV never touches `float2tuple`
1050 /// and its binary-artefact heuristic) and renders it through
1051 /// `decimal_value.as_tuple()`.
1052 ///
1053 /// `precision_override` is ignored: DV's `to_cardinal_float` signature is
1054 /// `(value, nominal=True)` — no `precision` kwarg — and DV carries no
1055 /// `self.precision` attribute for the dispatcher's `precision=` path to
1056 /// latch onto, so Python could never thread one in here.
1057 fn to_cardinal_float(
1058 &self,
1059 value: &FloatValue,
1060 _precision_override: Option<u32>,
1061 ) -> Result<String> {
1062 self.cardinal_float_full(value, true)
1063 }
1064
1065 /// `to_cardinal(float/Decimal)` — the full entry, whole values included.
1066 /// DV has no whole-value routing: `Decimal(str(5.0))` keeps its `.0`, so
1067 /// even integral floats run the pointword grammar (quirk 10), and a
1068 /// scientific repr collapses to its coefficient (quirk 9).
1069 fn cardinal_float_entry(
1070 &self,
1071 value: &FloatValue,
1072 _precision_override: Option<u32>,
1073 ) -> Result<String> {
1074 self.cardinal_float_full(value, true)
1075 }
1076
1077 /// `to_ordinal(float/Decimal)`: `verify_ordinal(value)` (TypeError on
1078 /// non-integral, then on negative — quirk 12), then the float grammar
1079 /// with `nominal=False` (quirk 13) plus the ordword.
1080 fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
1081 self.verify_ordinal_float(value)?;
1082 Ok(format!(
1083 "{} {}",
1084 self.cardinal_float_full(value, false)?,
1085 ORDWORD
1086 ))
1087 }
1088
1089 /// `to_ordinal_num(float/Decimal)`: `verify_ordinal`, then
1090 /// `"{} {}".format(value, ordword)` — `repr_str` is Python's
1091 /// `str(value)`, so `-0.0` (which passes verify) prints as `-0.0 ވަނަ`.
1092 fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
1093 self.verify_ordinal_float(value)?;
1094 Ok(format!("{} {}", repr_str, ORDWORD))
1095 }
1096
1097 /// `to_year(float/Decimal)` — quirk 14. `value < 0` takes `abs()` first,
1098 /// so negative years carry no negword; [1100, 2000) splits into
1099 /// still-float `high`/`low` halves rendered independently.
1100 fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
1101 match value {
1102 FloatValue::Float { value: f, .. } => {
1103 let mut v = *f;
1104 let mut suffix = "";
1105 // Numeric `<`: -0.0 is not negative and keeps its sign into
1106 // the cardinal, which raises IndexError there regardless.
1107 if v < 0.0 {
1108 v = -v;
1109 suffix = BCWORD;
1110 }
1111
1112 let mut result: Vec<String> = Vec::new();
1113 if v < 2000.0 && v >= 1100.0 {
1114 // Python float `%` is fmod (positive operands here), and
1115 // `//` is the floor of the corrected quotient.
1116 let low = v % 100.0;
1117 let high = ((v - low) / 100.0).floor();
1118 result.push(self.cardinal_from_f64(high, true)?);
1119 result.push("ސަތޭކަ".to_string());
1120 result.push(self.cardinal_from_f64(low, true)?);
1121 } else {
1122 result.push(self.cardinal_from_f64(v, true)?);
1123 }
1124
1125 if !suffix.is_empty() {
1126 result.push(suffix.to_string());
1127 }
1128 Ok(result.join(" "))
1129 }
1130 FloatValue::Decimal { value: d, .. } => {
1131 let mut v = d.clone();
1132 let mut suffix = "";
1133 if v < BigDecimal::from(0) {
1134 v = v.abs();
1135 suffix = BCWORD;
1136 }
1137
1138 let mut result: Vec<String> = Vec::new();
1139 if v < BigDecimal::from(2000) && v >= BigDecimal::from(1100) {
1140 // Decimal divide-integer (`//`) yields exponent 0; the
1141 // remainder keeps the ideal exponent min(exp(v), 0).
1142 let (coeff, scale) = v.as_bigint_and_exponent();
1143 let hundred = BigInt::from(100);
1144 let (q, low_coeff, low_exp) = if scale >= 0 {
1145 let div = &hundred * pow10(scale as usize);
1146 let q = &coeff / ÷ // v >= 1100 > 0: trunc == floor
1147 let low = &coeff - &q * ÷
1148 (q, low, -scale)
1149 } else {
1150 let full = &coeff * pow10((-scale) as usize);
1151 let q = &full / &hundred;
1152 let low = &full - &q * &hundred;
1153 (q, low, 0)
1154 };
1155 result.push(self.cardinal_tuple(false, &q.to_string(), 0, true)?);
1156 result.push("ސަތޭކަ".to_string());
1157 result.push(self.cardinal_tuple(false, &low_coeff.to_string(), low_exp, true)?);
1158 } else {
1159 let (coeff, scale) = v.as_bigint_and_exponent();
1160 result.push(self.cardinal_tuple(
1161 coeff.is_negative(),
1162 &coeff.abs().to_string(),
1163 -scale,
1164 true,
1165 )?);
1166 }
1167
1168 if !suffix.is_empty() {
1169 result.push(suffix.to_string());
1170 }
1171 Ok(result.join(" "))
1172 }
1173 }
1174 }
1175
1176 /// `to_cardinal(float/Decimal, nominal=...)` — the kwarg rides the float
1177 /// entry too; the integer part stays hardcoded-nominal either way
1178 /// (quirk 13), only the fractional digits and the no-point branch follow
1179 /// the flag.
1180 fn to_cardinal_float_kw(
1181 &self,
1182 value: &FloatValue,
1183 _precision_override: Option<u32>,
1184 kw: &Kwargs,
1185 ) -> Result<String> {
1186 if !kw.only(&["nominal"]) {
1187 return Err(N2WError::Fallback("kwargs".into()));
1188 }
1189 self.cardinal_float_full(value, kw_nominal(kw))
1190 }
1191
1192 /// `str_to_number` == `to_decimal`: `Decimal(value)` with
1193 /// `InvalidOperation` rewritten to `TypeError(errmsg_nonnum)`. The NaN
1194 /// arm short-circuits the InvalidOperation Python would raise a step
1195 /// later (`Decimal(MAXVAL).compare(NaN) < 1` in `to_cardinal_float`) —
1196 /// see the "Strings" section of the module docs for why it lives here.
1197 fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
1198 match python_decimal_parse(s) {
1199 Ok(ParsedNumber::NaN) => Err(N2WError::Custom {
1200 module: "decimal",
1201 class: "InvalidOperation",
1202 msg: "comparison involving NaN".into(),
1203 }),
1204 // A successful parse is served natively. The `to="fraction"`
1205 // AttributeError (Num2Word_DV has no to_fraction) is now handled
1206 // by the binding's fraction arm, which probes `to_fraction(1,1)`
1207 // and surfaces the language's AttributeError — so the earlier
1208 // blanket defer-to-Python is no longer needed.
1209 Ok(other) => Ok(other),
1210 Err(N2WError::Custom {
1211 module: "decimal",
1212 class: "InvalidOperation",
1213 ..
1214 }) => Err(N2WError::Type(format!("{} is not a valid value.", s))),
1215 Err(e) => Err(e),
1216 }
1217 }
1218
1219 /// Like `to_cheque`, `Num2Word_DV` defines no `to_fraction` and inherits
1220 /// none, so the dispatcher's attribute lookup raises `AttributeError`
1221 /// before the arguments are even parsed — `to_fraction("1", "0")` never
1222 /// reaches a division, so no ZeroDivisionError either.
1223 fn to_fraction(&self, _numerator: &BigInt, _denominator: &BigInt) -> Result<String> {
1224 Err(N2WError::Attribute(
1225 "'Num2Word_DV' object has no attribute 'to_fraction'".into(),
1226 ))
1227 }
1228
1229 /// `to_currency(self, value, currency="ރުފިޔާ", cents="ލާރި")`.
1230 ///
1231 /// Shares no machinery with `base.to_currency`: no forms table, no
1232 /// precision, no pluralize, no separator, no adjective. `separator` and
1233 /// `adjective` are therefore ignored rather than defaulted — the Python
1234 /// method has no such parameters, so nothing could consume them.
1235 ///
1236 /// `has_decimal` is ignored for the same reason. `base.to_currency` needs
1237 /// it to decide whether the cents segment appears; DV decides that with a
1238 /// plain truthiness test on `frac_part` instead (quirk 7), and its
1239 /// `to_decimal` treats an `int` and a `float` identically.
1240 fn to_currency(
1241 &self,
1242 val: &CurrencyValue,
1243 currency: &str,
1244 cents: bool,
1245 _separator: Option<&str>,
1246 _adjective: bool,
1247 ) -> Result<String> {
1248 // decimal_value = self.to_decimal(value). Decimal(int) and
1249 // Decimal(str(float)) — which is what the shim already handed us.
1250 let decimal_value = match val {
1251 CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
1252 CurrencyValue::Decimal { value, .. } => value.clone(),
1253 };
1254
1255 if decimal_value.is_zero() {
1256 return Ok(format!("{} {}", self.base_stem(0)?, currency));
1257 }
1258
1259 // ROUND_HALF_EVEN, not floor — quirk 6. 99.99 rounds *up* to 100 and
1260 // frac_part comes out negative.
1261 let int_part = to_integral_value(&decimal_value);
1262 let frac_part =
1263 to_integral_value(&((&decimal_value - &int_part) * BigDecimal::from(100)));
1264
1265 let mut result: Vec<String> = Vec::new();
1266
1267 if !int_part.is_zero() {
1268 result.push(self.to_cardinal_float_dec(&int_part, false)?);
1269 result.push(currency.to_string());
1270 }
1271
1272 if !frac_part.is_zero() {
1273 result.push(self.to_cardinal_float_dec(&frac_part, false)?);
1274 // Python appends `cents` — a *word*, defaulting to ލާރި. The trait
1275 // hands us a bool, because `base.to_currency`'s `cents=` is a
1276 // verbosity flag. The shim sends `kwargs.get("cents", True)`, so
1277 // `true` is the only value an ordinary call produces and the
1278 // default word is what Python would have used.
1279 //
1280 // `false` can only mean the caller explicitly passed `cents=False`,
1281 // and Python then puts the bool itself into the list and dies in
1282 // `" ".join(...)`. Reproduced rather than papered over — but note
1283 // it fires only once the cents segment exists at all, which is why
1284 // `to_currency(1.0, cents=False)` still returns "އެއް EUR", and why
1285 // this sits after frac_part has been rendered and pushed: an
1286 // OverflowError from that render happens first in Python too.
1287 if !cents {
1288 return Err(N2WError::Type(format!(
1289 "sequence item {}: expected str instance, bool found",
1290 result.len()
1291 )));
1292 }
1293 result.push(CENTSWORD.to_string());
1294 }
1295
1296 // Both parts zero but the value itself non-zero (0.001) => "" — quirk 7.
1297 Ok(result.join(" "))
1298 }
1299
1300 /// `Num2Word_DV` defines no `to_cheque`, and inherits none — it has no base
1301 /// class. The dispatcher's `getattr(converter, "to_cheque")` is what fails,
1302 /// before any conversion runs, so this is an `AttributeError` and *not* the
1303 /// `NotImplementedError` that `default_to_cheque` would raise on the empty
1304 /// forms table. The corpus records `AttributeError` for all nine codes.
1305 fn to_cheque(&self, _val: &BigDecimal, _currency: &str) -> Result<String> {
1306 Err(N2WError::Attribute(
1307 "'Num2Word_DV' object has no attribute 'to_cheque'".into(),
1308 ))
1309 }
1310}
1311
1312/// 10^n.
1313fn pow10(n: usize) -> BigInt {
1314 let mut p = BigInt::from(1);
1315 for _ in 0..n {
1316 p *= 10;
1317 }
1318 p
1319}
1320
1321/// The shortest round-trip decimal digits of `f` and its adjusted (power of
1322/// the leading digit) exponent, via Rust's `{:e}` — whose default precision
1323/// is "the smallest number of digits necessary to represent the value
1324/// uniquely", i.e. the same digits CPython's `repr(float)` prints, since the
1325/// shortest representation of a double is unique.
1326fn shortest_digits(f: f64) -> (String, i64) {
1327 let s = format!("{:e}", f); // e.g. "1.234e3", "-5e-1", "0e0"
1328 let (mant, exp) = s.split_once('e').expect("LowerExp always contains 'e'");
1329 let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
1330 let adj: i64 = exp.parse().expect("LowerExp exponent is an integer");
1331 (digits, adj)
1332}
1333
1334/// Reproduce `Decimal(str(f)).as_tuple()` as `(sign_negative, digit_str,
1335/// exponent)`, matching Python's `to_decimal(float)`.
1336///
1337/// `repr(float)` prints the shortest round-trip digits, fixed-point while the
1338/// adjusted exponent is in [-4, 16) and scientific outside. The regimes give
1339/// the Decimal *different* tuples:
1340///
1341/// * scientific: the coefficient is the bare digits (`'1e+16'` -> `(1,)`,
1342/// exponent 16) — quirk 9's collapse-to-coefficient;
1343/// * fixed-point, integral value: repr appends ".0", so the coefficient
1344/// gains the padding zeros plus one more and the exponent is -1
1345/// (`'1000.0'` -> `(1,0,0,0,0)`, exponent -1) — quirk 10;
1346/// * fixed-point, fractional value: digits as-is with the natural exponent
1347/// (`'12.34'` -> `(1,2,3,4)`, exponent -2). The tiny-scientific regime
1348/// (`'1e-05'`) lands on the same tuple as its fixed expansion would,
1349/// because `Decimal` strips leading coefficient zeros.
1350fn py_float_dec_tuple(f: f64) -> (bool, String, i64) {
1351 let sign_negative = f.is_sign_negative();
1352 let (m, adj) = shortest_digits(f);
1353 let ndig = m.chars().count() as i64;
1354 let e_nat = adj - (ndig - 1);
1355 if adj >= 16 || adj < -4 {
1356 // repr is scientific either way; both directions keep the bare digits.
1357 return (sign_negative, m, e_nat);
1358 }
1359 if e_nat >= 0 {
1360 // Integral fixed-point repr: '.0' appended.
1361 let mut d = m;
1362 for _ in 0..=e_nat {
1363 d.push('0');
1364 }
1365 // 0.0's coefficient collapses back to a single 0.
1366 let trimmed = d.trim_start_matches('0');
1367 let d = if trimmed.is_empty() { "0".to_string() } else { trimmed.to_string() };
1368 (sign_negative, d, -1)
1369 } else {
1370 (sign_negative, m, e_nat)
1371 }
1372}
1373
1374/// Python's `str(float)` — repr with the same regime split as
1375/// [`py_float_dec_tuple`]. Only used for exception messages
1376/// (`errmsg_floatord` / `errmsg_negord` format the original value); the
1377/// corpora compare exception types, not text.
1378fn py_float_str(f: f64) -> String {
1379 let sign = if f.is_sign_negative() { "-" } else { "" };
1380 let (m, adj) = shortest_digits(f);
1381 if (-4..16).contains(&adj) {
1382 let intlen = adj + 1;
1383 if intlen <= 0 {
1384 format!("{}0.{}{}", sign, "0".repeat((-intlen) as usize), m)
1385 } else if intlen as usize >= m.len() {
1386 format!("{}{}{}.0", sign, m, "0".repeat(intlen as usize - m.len()))
1387 } else {
1388 let (a, b) = m.split_at(intlen as usize);
1389 format!("{}{}.{}", sign, a, b)
1390 }
1391 } else {
1392 let mut chars = m.chars();
1393 let lead = chars.next().unwrap_or('0');
1394 let rest: String = chars.collect();
1395 let coeff = if rest.is_empty() {
1396 lead.to_string()
1397 } else {
1398 format!("{}.{}", lead, rest)
1399 };
1400 // Python zero-pads the exponent to two digits: 1e-05, 1e+16.
1401 format!(
1402 "{}{}e{}{:02}",
1403 sign,
1404 coeff,
1405 if adj < 0 { '-' } else { '+' },
1406 adj.abs()
1407 )
1408 }
1409}
1410
1411/// `self.to_decimal(value).as_tuple()` — the `(sign, digits, exponent)` view
1412/// every DV float path renders from.
1413fn float_value_tuple(value: &FloatValue) -> (bool, String, i64) {
1414 match value {
1415 // float -> Decimal(str(value)), reconstructed from the f64 repr.
1416 FloatValue::Float { value, .. } => py_float_dec_tuple(*value),
1417 // Decimal -> the Decimal unchanged. as_bigint_and_exponent() is
1418 // (coefficient, scale); Python's exponent is -scale. bigdecimal
1419 // preserves trailing zeros from the source string, so Decimal("1.10")
1420 // keeps coefficient 110 / exponent -2 — the trailing zero the corpus
1421 // depends on.
1422 FloatValue::Decimal { value, .. } => {
1423 let (coeff, scale) = value.as_bigint_and_exponent();
1424 (coeff.is_negative(), coeff.abs().to_string(), -scale)
1425 }
1426 }
1427}
1428
1429/// `"{}".format(value)` for verify_ordinal's error messages: `str(float)`
1430/// for a float, `str(Decimal)` for a Decimal.
1431fn float_value_str(value: &FloatValue) -> String {
1432 match value {
1433 FloatValue::Float { value, .. } => py_float_str(*value),
1434 FloatValue::Decimal { .. } => {
1435 let (sign, digits, exponent) = float_value_tuple(value);
1436 py_decimal_str_parts(sign, &digits, exponent)
1437 }
1438 }
1439}
1440
1441/// The `nominal=` kwarg under Python truthiness — `if not nominal` is the
1442/// only test the class ever applies, so False/0/None/""/[] all mean stem.
1443fn kw_nominal(kw: &Kwargs) -> bool {
1444 match kw.get("nominal") {
1445 Option::None => true, // not passed: the default
1446 Some(KwVal::Bool(b)) => *b,
1447 Some(KwVal::Int(i)) => *i != 0,
1448 Some(KwVal::None) => false,
1449 Some(KwVal::Str(s)) => !s.is_empty(),
1450 Some(KwVal::List(l)) => !l.is_empty(),
1451 }
1452}
1453
1454/// `str(Decimal)` (the spec's to-scientific-string) from a
1455/// `(sign, digit_str, exponent)` tuple. Only reached to build an OverflowError
1456/// *message*; the differential harness compares exception *type*, not text, so
1457/// this is never on the parity path — implemented in full for fidelity anyway.
1458fn py_decimal_str_parts(sign_negative: bool, digit_str: &str, exponent: i64) -> String {
1459 let sign = if sign_negative { "-" } else { "" };
1460 let chars: Vec<char> = digit_str.chars().collect();
1461 let ndigits = chars.len() as i64;
1462 let adjusted = exponent + ndigits - 1;
1463
1464 if exponent <= 0 && adjusted >= -6 {
1465 // Fixed-point form.
1466 if exponent == 0 {
1467 return format!("{}{}", sign, digit_str);
1468 }
1469 let leftdigits = exponent + ndigits; // where the point falls
1470 let (intpart, fracpart) = if leftdigits <= 0 {
1471 (
1472 "0".to_string(),
1473 format!(".{}{}", "0".repeat((-leftdigits) as usize), digit_str),
1474 )
1475 } else {
1476 let l = leftdigits as usize;
1477 (
1478 chars[..l].iter().collect::<String>(),
1479 format!(".{}", chars[l..].iter().collect::<String>()),
1480 )
1481 };
1482 return format!("{}{}{}", sign, intpart, fracpart);
1483 }
1484
1485 // Scientific form.
1486 let coeff = if ndigits == 1 {
1487 chars[0].to_string()
1488 } else {
1489 format!("{}.{}", chars[0], chars[1..].iter().collect::<String>())
1490 };
1491 let esign = if adjusted < 0 { "-" } else { "+" };
1492 format!("{}{}E{}{}", sign, coeff, esign, adjusted.abs())
1493}
1494
1495/// Round a non-negative integer to `CTX_PREC` significant decimal digits using
1496/// ROUND_HALF_EVEN — what `Decimal.__abs__` does under the default context.
1497///
1498/// Verified against CPython at the boundary: `10**33 - 50001` rounds to
1499/// `9.999999999999999999999999999E+32` (below MAXVAL, converts fine) while
1500/// `10**33 - 50000` is a tie and rounds to even, giving `1E+33` == MAXVAL and
1501/// tripping the overflow guard.
1502fn round_half_even_28(a: &BigInt) -> BigInt {
1503 let ndigits = a.to_string().len(); // `a` is non-negative: no sign char
1504 if ndigits <= CTX_PREC {
1505 return a.clone();
1506 }
1507
1508 let divisor = pow10(ndigits - CTX_PREC);
1509 let (mut q, r) = a.div_rem(&divisor);
1510 let twice = &r * 2;
1511
1512 if twice > divisor {
1513 q += 1;
1514 } else if twice == divisor {
1515 // Exact tie: round half to even.
1516 if !q.is_even() {
1517 q += 1;
1518 }
1519 }
1520 q * divisor
1521}
1522
1523/// Python's `Decimal.to_integral_value()` — round to an integer using the
1524/// context's rounding, which `__init__` leaves at the default ROUND_HALF_EVEN.
1525/// (The commented-out `getcontext().rounding = ROUND_FLOOR` never took effect;
1526/// were it live, 99.99 would floor to 99 and quirk 6's negative cents could not
1527/// arise.)
1528///
1529/// A value whose exponent is already `>= 0` comes back **untouched**, keeping
1530/// its coefficient and exponent — so `Decimal("1E+21")` stays `1E+21` rather
1531/// than expanding to 22 digits. That is what quirk 8 rides on.
1532fn to_integral_value(d: &BigDecimal) -> BigDecimal {
1533 let (coefficient, scale) = d.as_bigint_and_exponent();
1534 // scale <= 0 is Python's exponent >= 0.
1535 if scale <= 0 {
1536 return d.clone();
1537 }
1538
1539 let divisor = pow10(scale as usize);
1540 let negative = coefficient.is_negative();
1541 let (mut q, r) = coefficient.abs().div_rem(&divisor);
1542 // ROUND_HALF_EVEN on the magnitude; the mode is symmetric about zero.
1543 let twice = &r * 2;
1544 if twice > divisor || (twice == divisor && !q.is_even()) {
1545 q += 1;
1546 }
1547 if negative {
1548 q = -q;
1549 }
1550 // Rounding always lands on exponent 0, matching Python.
1551 BigDecimal::new(q, 0)
1552}
1553
1554/// Python's `str(Decimal)` — the spec's *to-scientific-string* — restricted to
1555/// the integral values this file can reach it with.
1556///
1557/// Only two of the spec's three branches can fire. Exponent 0 prints the
1558/// coefficient plainly; a positive exponent switches to scientific notation
1559/// keyed on the adjusted exponent, so `Decimal('1.5E+33')` is `"1.5E+33"` and
1560/// not `"1500000000000000000000000000000000"`. A negative exponent would need
1561/// the third branch, which `to_integral_value` rules out.
1562fn py_decimal_str(d: &BigDecimal) -> String {
1563 let (coefficient, scale) = d.as_bigint_and_exponent();
1564 let exponent = -scale;
1565 let digits = coefficient.abs().to_string();
1566 let sign = if coefficient.is_negative() { "-" } else { "" };
1567
1568 if exponent <= 0 {
1569 return format!("{}{}", sign, digits);
1570 }
1571
1572 // adjusted exponent = exponent + len(digits) - 1
1573 let adjusted = exponent + digits.chars().count() as i64 - 1;
1574 let mut chars = digits.chars();
1575 let lead = chars.next().unwrap_or('0');
1576 let rest: String = chars.collect();
1577 let coeff = if rest.is_empty() {
1578 lead.to_string()
1579 } else {
1580 format!("{}.{}", lead, rest)
1581 };
1582 // Python prints the exponent's sign explicitly and never zero-pads it.
1583 let esign = if adjusted < 0 { "-" } else { "+" };
1584 format!("{}{}E{}{}", sign, coeff, esign, adjusted.abs())
1585}
1586
1587/// Python's `int(s)` over an ASCII digit string. Every call site feeds it a
1588/// slice of at most 3 digits, so `i64` cannot overflow.
1589fn py_int(s: &str) -> Result<i64> {
1590 s.parse::<i64>().map_err(|_| {
1591 N2WError::Value(format!(
1592 "invalid literal for int() with base 10: '{}'",
1593 s
1594 ))
1595 })
1596}
1597
1598/// Python's `s[i]`, raising `IndexError` past the end.
1599fn char_at(s: &str, i: usize) -> Result<String> {
1600 s.chars()
1601 .nth(i)
1602 .map(|c| c.to_string())
1603 .ok_or_else(|| N2WError::Index("string index out of range".into()))
1604}
1605
1606/// Python's `s[start:end]` with negative-index and clamping semantics.
1607/// Operates on chars; every caller passes an ASCII digit string.
1608fn py_slice(s: &str, start: isize, end: isize) -> String {
1609 let chars: Vec<char> = s.chars().collect();
1610 let len = chars.len() as isize;
1611
1612 let norm = |i: isize| -> usize {
1613 if i < 0 {
1614 let j = len + i;
1615 if j < 0 {
1616 0
1617 } else {
1618 j as usize
1619 }
1620 } else if i > len {
1621 len as usize
1622 } else {
1623 i as usize
1624 }
1625 };
1626
1627 let (a, b) = (norm(start), norm(end));
1628 if a >= b {
1629 return String::new();
1630 }
1631 chars[a..b].iter().collect()
1632}
1633
1634#[cfg(test)]
1635mod float_tests {
1636 use super::*;
1637 use std::str::FromStr;
1638
1639 fn f(value: f64, precision: u32) -> Result<String> {
1640 // Fully-qualified: the inherent to_cardinal_float(&BigInt, bool) would
1641 // otherwise shadow the trait method under method-call syntax.
1642 Lang::to_cardinal_float(
1643 &LangDv::new(),
1644 &FloatValue::Float { value, precision },
1645 None,
1646 )
1647 }
1648
1649 fn d(s: &str) -> Result<String> {
1650 Lang::to_cardinal_float(
1651 &LangDv::new(),
1652 &FloatValue::Decimal {
1653 value: BigDecimal::from_str(s).unwrap(),
1654 // DV ignores precision; a plausible value all the same.
1655 precision: s.split_once('.').map_or(0, |(_, fr)| fr.len() as u32),
1656 },
1657 None,
1658 )
1659 }
1660
1661 fn is_index(r: &Result<String>) -> bool {
1662 matches!(r, Err(N2WError::Index(_)))
1663 }
1664
1665 #[test]
1666 fn float_cardinal_matches_corpus_and_interpreter() {
1667 assert!(is_index(&f(0.5, 1)));
1668 assert_eq!(f(1.5, 1).unwrap(), "އެކެއް ޕޮއިންޓް ފަހެއް");
1669 assert_eq!(f(3.14, 2).unwrap(), "ތިނެއް ޕޮއިންޓް އެކެއް ހަތަރެއް");
1670 assert_eq!(f(12.34, 2).unwrap(), "ބާރަ ޕޮއިންޓް ތިނެއް ހަތަރެއް");
1671 assert_eq!(f(-12.34, 2).unwrap(), "މައިނަސް ބާރަ ޕޮއިންޓް ތިނެއް ހަތަރެއް");
1672 assert_eq!(f(1.005, 3).unwrap(), "އެކެއް ޕޮއިންޓް ސުމެއް ސުމެއް ފަހެއް");
1673 assert_eq!(f(2.675, 3).unwrap(), "ދޭއް ޕޮއިންޓް ހައެއް ހަތެއް ފަހެއް");
1674 assert!(is_index(&f(0.01, 2)));
1675 assert_eq!(f(99.99, 2).unwrap(), "ނުވަދިހަނުވައެއް ޕޮއިންޓް ނުވައެއް ނުވައެއް");
1676 assert_eq!(f(1.0, 1).unwrap(), "އެކެއް ޕޮއިންޓް ސުމެއް");
1677 assert!(is_index(&f(0.0, 1)));
1678 assert!(is_index(&f(-0.5, 1)));
1679 assert_eq!(f(100.5, 1).unwrap(), "ސަތޭކަ ޕޮއިންޓް ފަހެއް");
1680 assert_eq!(
1681 f(1234.56, 2).unwrap(),
1682 "އެއްހާސް ދުއިސައްތަތިރީސްހަތަރެއް ޕޮއިންޓް ފަހެއް ހައެއް"
1683 );
1684 assert_eq!(f(-1.5, 1).unwrap(), "މައިނަސް އެކެއް ޕޮއިންޓް ފަހެއް");
1685 assert!(is_index(&f(0.1, 1)));
1686 assert!(is_index(&f(0.99, 2)));
1687 assert_eq!(f(1.01, 2).unwrap(), "އެކެއް ޕޮއިންޓް ސުމެއް އެކެއް");
1688 assert_eq!(f(2.25, 2).unwrap(), "ދޭއް ޕޮއިންޓް ދޭއް ފަހެއް");
1689 }
1690
1691 #[test]
1692 fn decimal_cardinal_matches_corpus() {
1693 assert!(is_index(&d("0.01")));
1694 assert_eq!(d("1.10").unwrap(), "އެކެއް ޕޮއިންޓް އެކެއް ސުމެއް");
1695 assert_eq!(d("12.345").unwrap(), "ބާރަ ޕޮއިންޓް ތިނެއް ހަތަރެއް ފަހެއް");
1696 assert_eq!(
1697 d("98746251323029.99").unwrap(),
1698 "ނުވަދިހައައްޓްރިޔަން ހަތްސަތޭކަ ސާޅީސްހަބިލިޔަން ދުއިސައްތަފަންސާސްއެއްމިލިޔަން ތިންލައްކަ ތޭވީސްހާސް ނަވާވީސް ޕޮއިންޓް ނުވައެއް ނުވައެއް"
1699 );
1700 assert!(is_index(&d("0.001")));
1701 assert_eq!(d("1.005").unwrap(), "އެކެއް ޕޮއިންޓް ސުމެއް ސުމެއް ފަހެއް");
1702 assert_eq!(d("2.675").unwrap(), "ދޭއް ޕޮއިންޓް ހައެއް ހަތެއް ފަހެއް");
1703 assert!(is_index(&d("0.50")));
1704 }
1705
1706 #[test]
1707 fn whole_float_entries_match_corpus() {
1708 let l = LangDv::new();
1709 let fv = |v: f64| FloatValue::Float { value: v, precision: 1 };
1710
1711 // cardinal: no whole-value fast path (quirk 10) + scientific collapse
1712 // (quirk 9) + IndexError on empty integer part (quirk 11).
1713 assert_eq!(
1714 l.cardinal_float_entry(&fv(5.0), None).unwrap(),
1715 "ފަހެއް ޕޮއިންޓް ސުމެއް"
1716 );
1717 assert_eq!(
1718 l.cardinal_float_entry(&fv(-2.0), None).unwrap(),
1719 "މައިނަސް ދޭއް ޕޮއިންޓް ސުމެއް"
1720 );
1721 assert_eq!(
1722 l.cardinal_float_entry(&fv(-1000.0), None).unwrap(),
1723 "މައިނަސް އެއްހާސް ޕޮއިންޓް ސުމެއް"
1724 );
1725 assert_eq!(l.cardinal_float_entry(&fv(1e16), None).unwrap(), "އެކެއް");
1726 assert_eq!(l.cardinal_float_entry(&fv(1e20), None).unwrap(), "އެކެއް");
1727 assert!(matches!(
1728 l.cardinal_float_entry(&fv(0.0), None),
1729 Err(N2WError::Index(_))
1730 ));
1731 assert!(matches!(
1732 l.cardinal_float_entry(&fv(-0.0), None),
1733 Err(N2WError::Index(_))
1734 ));
1735
1736 // ordinal: verify_ordinal order (quirk 12) + nominal/stem mix (13).
1737 assert_eq!(
1738 l.ordinal_float_entry(&fv(1.0)).unwrap(),
1739 "އެކެއް ޕޮއިންޓް ސުން ވަނަ"
1740 );
1741 assert_eq!(l.ordinal_float_entry(&fv(1e16)).unwrap(), "އެއް ވަނަ");
1742 assert!(matches!(l.ordinal_float_entry(&fv(2.5)), Err(N2WError::Type(_))));
1743 assert!(matches!(l.ordinal_float_entry(&fv(-1.5)), Err(N2WError::Type(_))));
1744 assert!(matches!(l.ordinal_float_entry(&fv(-1.0)), Err(N2WError::Type(_))));
1745 assert!(matches!(l.ordinal_float_entry(&fv(-0.0)), Err(N2WError::Index(_))));
1746
1747 // ordinal_num: -0.0 passes verify and echoes the repr.
1748 assert_eq!(
1749 l.ordinal_num_float_entry(&fv(-0.0), "-0.0").unwrap(),
1750 "-0.0 ވަނަ"
1751 );
1752 assert!(matches!(
1753 l.ordinal_num_float_entry(&fv(-1.0), "-1.0"),
1754 Err(N2WError::Type(_))
1755 ));
1756
1757 // year: abs-before-cardinal + the [1100, 2000) float split (quirk 14).
1758 assert_eq!(
1759 l.year_float_entry(&fv(-1.5)).unwrap(),
1760 "އެކެއް ޕޮއިންޓް ފަހެއް ބީ.ސީ"
1761 );
1762 assert_eq!(
1763 l.year_float_entry(&fv(1234.0)).unwrap(),
1764 "ބާރަ ޕޮއިންޓް ސުމެއް ސަތޭކަ ތިރީސްހަތަރެއް ޕޮއިންޓް ސުމެއް"
1765 );
1766 assert_eq!(l.year_float_entry(&fv(1e16)).unwrap(), "އެކެއް");
1767 assert!(matches!(l.year_float_entry(&fv(-0.0)), Err(N2WError::Index(_))));
1768 }
1769
1770 #[test]
1771 fn decimal_entries_match_corpus() {
1772 let l = LangDv::new();
1773 let dv = |s: &str| FloatValue::Decimal {
1774 value: BigDecimal::from_str(s).unwrap(),
1775 precision: 0,
1776 };
1777
1778 assert_eq!(l.cardinal_float_entry(&dv("1E+2"), None).unwrap(), "އެކެއް");
1779 assert_eq!(l.cardinal_float_entry(&dv("1E+20"), None).unwrap(), "އެކެއް");
1780 assert_eq!(l.ordinal_float_entry(&dv("0")).unwrap(), "ސުން ވަނަ");
1781 assert_eq!(l.ordinal_float_entry(&dv("5")).unwrap(), "ފަސް ވަނަ");
1782 assert_eq!(l.ordinal_float_entry(&dv("100")).unwrap(), "ސަތޭކަ ވަނަ");
1783 assert_eq!(
1784 l.ordinal_float_entry(&dv("5.00")).unwrap(),
1785 "ފަހެއް ޕޮއިންޓް ސުން ސުން ވަނަ"
1786 );
1787 assert_eq!(l.ordinal_float_entry(&dv("1E+2")).unwrap(), "އެއް ވަނަ");
1788 assert!(matches!(l.ordinal_float_entry(&dv("-3.0")), Err(N2WError::Type(_))));
1789 assert_eq!(
1790 l.year_float_entry(&dv("-3.0")).unwrap(),
1791 "ތިނެއް ޕޮއިންޓް ސުމެއް ބީ.ސީ"
1792 );
1793 assert_eq!(l.year_float_entry(&dv("1E+2")).unwrap(), "އެކެއް");
1794 assert_eq!(
1795 l.year_float_entry(&dv("12345.000")).unwrap(),
1796 "ބާރަހާސް ތިންސަތޭކަ ސާޅީސްފަހެއް ޕޮއިންޓް ސުމެއް ސުމެއް ސުމެއް"
1797 );
1798 }
1799
1800 #[test]
1801 fn strings_fraction_and_kwargs_match_corpus() {
1802 use crate::base::{KwVal, Kwargs};
1803 let l = LangDv::new();
1804
1805 // str_to_number: TypeError instead of InvalidOperation; NaN
1806 // short-circuits the compare's InvalidOperation.
1807 assert!(matches!(l.str_to_number("abc"), Err(N2WError::Type(_))));
1808 assert!(matches!(l.str_to_number(""), Err(N2WError::Type(_))));
1809 assert!(matches!(
1810 l.str_to_number("NaN"),
1811 Err(N2WError::Custom { class: "InvalidOperation", .. })
1812 ));
1813 assert!(matches!(l.str_to_number("1e3"), Ok(ParsedNumber::Dec(_))));
1814
1815 // to_fraction: AttributeError on lookup, even for 1/0.
1816 assert!(matches!(
1817 l.to_fraction(&BigInt::from(1), &BigInt::from(0)),
1818 Err(N2WError::Attribute(_))
1819 ));
1820
1821 // nominal= kwarg (the kwargs corpus rows).
1822 let kw = |v: KwVal| Kwargs(vec![("nominal".to_string(), v)]);
1823 assert_eq!(
1824 l.to_cardinal_kw(&BigInt::from(1234), &kw(KwVal::Bool(false))).unwrap(),
1825 "އެއްހާސް ދުއިސައްތަތިރީސްހަތަރު"
1826 );
1827 assert_eq!(
1828 l.to_cardinal_kw(&BigInt::from(-5), &kw(KwVal::Bool(false))).unwrap(),
1829 "މައިނަސް ފަސް"
1830 );
1831 assert_eq!(
1832 l.to_cardinal_kw(&BigInt::from(0), &kw(KwVal::Bool(false))).unwrap(),
1833 "ސުން"
1834 );
1835 assert_eq!(
1836 l.to_cardinal_kw(&BigInt::from(0), &kw(KwVal::Bool(true))).unwrap(),
1837 "ސުމެއް"
1838 );
1839 assert!(matches!(
1840 l.to_cardinal_kw(
1841 &BigInt::from(1),
1842 &Kwargs(vec![("case".to_string(), KwVal::Str("dative".into()))])
1843 ),
1844 Err(N2WError::Fallback(_))
1845 ));
1846
1847 // suffix= kwarg on to_year.
1848 let skw = Kwargs(vec![("suffix".to_string(), KwVal::Str("AD".into()))]);
1849 assert_eq!(l.to_year_kw(&BigInt::from(5), &skw).unwrap(), "ފަހެއް AD");
1850 assert_eq!(l.to_year_kw(&BigInt::from(-17), &skw).unwrap(), "ސަތާރަ AD");
1851 assert_eq!(
1852 l.to_year_kw(
1853 &BigInt::from(-17),
1854 &Kwargs(vec![("suffix".to_string(), KwVal::None)])
1855 )
1856 .unwrap(),
1857 "ސަތާރަ ބީ.ސީ"
1858 );
1859 }
1860
1861 #[test]
1862 fn overflow_boundary_matches_python() {
1863 // Python raises OverflowError when floor(round28(|v|)) >= 10**33 in the
1864 // float/Decimal path (quirk 4: abs() rounds to 28 significant digits
1865 // half-to-even first). Boundary values verified against CPython.
1866 let overflow = |s: &str| matches!(d(s), Err(N2WError::Overflow(_)));
1867
1868 assert!(overflow(&"9".repeat(33))); // 10**33 - 1 rounds up to 10**33
1869 assert!(overflow(&format!("1{}", "0".repeat(33)))); // 10**33
1870 assert!(overflow(&(10u128.pow(33) - 50000).to_string())); // exact tie, up
1871 assert!(d(&(10u128.pow(33) - 50001).to_string()).is_ok()); // just under
1872 assert!(d(&"9".repeat(28)).is_ok()); // 28 digits, no rounding
1873 assert!(d(&format!("1{}", "0".repeat(32))).is_ok()); // 10**32
1874 // A fractional value floors below the bound and converts fine.
1875 assert!(d(&format!("{}.5", "9".repeat(32))).is_ok());
1876 }
1877}