Skip to main content

num2words2_core/
lang_or.rs

1//! Port of `lang_OR.py` (Odia / Oriya), the class registered under key `"or"`
2//! (`__init__.py:395` → `"or": lang_OR.Num2Word_OR()`).
3//!
4//! Shape: **self-contained**. `Num2Word_OR` subclasses `Num2Word_Base` but
5//! defines no `high_numwords`/`mid_numwords`/`low_numwords` and no
6//! `set_high_numwords`, so Python never builds `self.cards` and never sets
7//! `MAXVAL`. `to_cardinal` is overridden outright and drives a hand-written
8//! `_int_to_word` recursion. Consequently `cards`/`maxval`/`merge` stay at
9//! their trait defaults here, and **there is no overflow check at all** — see
10//! "bug 1" below for what happens instead of `OverflowError`.
11//!
12//! Nothing is inherited from `Num2Word_Base` in a load-bearing way: OR
13//! overrides all four in-scope entry points itself.
14//!   * `to_cardinal`    — overridden (below)
15//!   * `to_ordinal`     — overridden: suppletive for 1..=10, else
16//!     `to_cardinal(n) + ମ` (see divergence 2 below)
17//!   * `to_ordinal_num` — overridden: `str(n) + ମ` (note: NOT the base's
18//!     bare `str(n)`, so the trait default would be wrong here)
19//!   * `to_year(val, longval=True)` — overridden to ignore `longval` entirely
20//!     and just delegate to `to_cardinal`. Same result as the base default,
21//!     but it is an explicit override rather than inheritance.
22//!
23//! `setup()` also sets `pointword`/`exclude_title`, which only matter to the
24//! float and title paths — out of scope. `is_title` is never enabled by OR.
25//!
26//! # Deliberate divergences from upstream
27//!
28//! Three, all documented here: the **script** (Odia, not IAST), the
29//! **tens[7]/teens[7] collision**, and the **ordinals for 1..=10**.
30//! Everything else is ported verbatim, bug for bug.
31//!
32//! ## 1. Odia script
33//!
34//! `lang_OR.py` spells every numeral in **IAST-style Latin transliteration**
35//! ("eka", "duī", "calīśa", "hajāra"). Odia is written in the Odia script,
36//! and every sibling Indic module in this crate (`lang_bn.rs`, `lang_hi.rs`,
37//! `lang_ta.rs`, `lang_te.rs`, ...) already emits its own script, so the
38//! transliteration was an upstream accident rather than a choice. The tables
39//! here are the Odia spellings: "ଏକ", "ଦୁଇ", "ଚାଳିଶ", "ହଜାର".
40//!
41//! This is a lexicon change only — the composition rules, the multiplier
42//! guards, the 10^9 digit cliff and the currency fallback are all still
43//! ported verbatim, including the non-idiomatic tens-and-units composition
44//! (bug 7 below).
45//!
46//! ## 2. tens[7] no longer collides with teens[7]
47//!
48//! Both tables held "satara". 70 and 17 therefore produced the *same* string,
49//! and 77 came out as "satara o sāta" — "seventeen and seven". The module
50//! header already called this out as wrong ("Odia for 70 is sattari; satara
51//! is 17") and kept it only because the frozen corpus pinned it. Since the
52//! whole lexicon is being rewritten anyway, `TENS[7]` is now ସତୁରୀ (70) and
53//! `TEENS[7]` stays ସତର (17).
54//!
55//! This is a value fix, not a spelling fix: it changes which number the
56//! output denotes. It is the one change here that a caller could have been
57//! relying on, and it is the reason 17/70/77 have their own test cases.
58//!
59//! ## 3. Ordinals 1..=10 are suppletive
60//!
61//! Upstream builds every ordinal as `to_cardinal(n) + "ma"`, which gives the
62//! non-words *ଏକମ for 1st and *ଛଅମ for 6th. Odia 1-10 are Sanskrit-derived
63//! forms with their own stems: ପ୍ରଥମ, ଦ୍ୱିତୀୟ, ତୃତୀୟ, ଚତୁର୍ଥ, ପଞ୍ଚମ, ଷଷ୍ଠ,
64//! ସପ୍ତମ, ଅଷ୍ଟମ, ନବମ, ଦଶମ. See [`ORDINAL_IRREGULARS`].
65//!
66//! **From 11 up nothing changed but the script.** ମ is a genuine Odia ordinal
67//! marker — it is what ends ପଞ୍ଚମ, ସପ୍ତମ, ଅଷ୍ଟମ, ନବମ and ଦଶମ — so the suffix
68//! arm is kept as upstream had it rather than replaced with a rule this port
69//! cannot source. The formal Sanskrit series (ଏକାଦଶ for 11th, ...) and the
70//! productive -ତମ of the sibling `lang_bn.rs` ("বিশতম" for 20th) are both
71//! attested alternatives; picking between them needs a native speaker, so
72//! this PR does not. `to_ordinal(11)` is ଏଗାରମ, as before.
73//!
74//! Zero is likewise left alone: `to_ordinal(0)` is still *ଶୂନ୍ୟମ. Odia has no
75//! ordinal for zero, but upstream never called `verify_ordinal` and this
76//! module does not start.
77//!
78//! # Faithfully reproduced Python bugs
79//!
80//! This is a port, not a rewrite. Every item below looks wrong and is exactly
81//! what Python emits — each one is pinned by a row in the frozen corpus.
82//!
83//! 1. **No number word above 10^9.** `_int_to_word` ends with a bare
84//!    `return str(number)`, so any `number >= 1_000_000_000` converts to its
85//!    own *decimal digits* instead of words, with no error:
86//!    `to_cardinal(10**9) == "1000000000"` and
87//!    `to_ordinal(10**9) == "1000000000ମ"`. Corpus confirms this all the way
88//!    up to 10^21. This is why the port keeps `BigInt` end-to-end and never
89//!    casts: the fallback must stringify arbitrarily large values verbatim.
90//! 2. ~~`tens[7]` collides with `teens[7]`~~ — **fixed**, see divergence 2
91//!    above. 70 is now ସତୁରୀ and 77 is "ସତୁରୀ ଓ ସାତ"; 17 stays ସତର.
92//! 3. **Tens and units are composed, not lexicalised.** Odia has a distinct
93//!    word for each of 21..=99 (42 is ବୟାଳିଶ, not "forty and two"), but
94//!    `_int_to_word` builds them as `tens[t] + " ଓ " + ones[o]`, so 42 is
95//!    "ଚାଳିଶ ଓ ଦୁଇ". Kept verbatim — replacing it means an 80-entry table and
96//!    a different algorithm, which is a separate change.
97//! 4. **`million` is the string "ଦଶ ଲକ୍ଷ"**, literally "ten lakh", used as
98//!    the multiplier for 10^6. This makes the Indian-system word collide with
99//!    the Western scale it is applied to: `to_cardinal(10**6)` ==
100//!    "ଏକ ଦଶ ଲକ୍ଷ" ("one ten-lakh"), and `to_cardinal(10**7)` ==
101//!    "ଦଶ ଦଶ ଲକ୍ଷ" ("ten ten-lakh"). The module otherwise uses a strict
102//!    Western 10^3/10^6 grouping and never uses lakh/crore grouping at all.
103//! 5. **`tens[0]` and `tens[1]` are unreachable.** `tens[1]` is "ଦଶ", but
104//!    the `number < 20` branch catches 10..=19 via `teens` first, and the
105//!    `number < 100` branch only ever computes `t` in 2..=9. Preserved in the
106//!    table for index alignment; never read.
107//! 6. **Above 10 the ordinal is still pure suffixation with no morphology**:
108//!    `to_ordinal` appends ମ to the *whole* cardinal string, so the suffix
109//!    lands on the final word of a phrase — `to_ordinal(999) ==
110//!    "ନଅ ଶହ ଓ ନବେ ଓ ନଅମ"` and `to_ordinal(10**6) == "ଏକ ଦଶ ଲକ୍ଷମ"`.
111//!    Negatives keep the sign word and now take the suppletive arm where it
112//!    applies: `to_ordinal(-1) == "ପ୍ରଥମ"` is *not* what happens — the
113//!    1..=10 test is on the signed value, so a negative falls through to the
114//!    suffix arm and `to_ordinal(-1) == "ଋଣ ଏକମ"`, as upstream.
115//!
116//! # Error variants
117//!
118//! For integer input this module cannot raise: there is no overflow check
119//! (bug 1 swallows it), every list index is provably in range (each branch
120//! bounds its divisor result before indexing), and the sign is stripped before
121//! any arithmetic. All 459 corpus rows for `"or"` in the four integer modes
122//! (`cardinal`/`ordinal`/`ordinal_num`/`year`) are `"ok": true`.
123//!
124//! `to_currency` cannot raise either — see currency quirk C1: the unknown-code
125//! fallback means the `NotImplementedError` path is unreachable. The only
126//! raising surface is the inherited `to_cheque`, which *does* raise
127//! `NotImplementedError` for any code outside `CURRENCY_FORMS` (6 of the 9
128//! `cheque:*` corpus rows). `fraction` (`TypeError`) remains out of scope.
129//!
130//! # Currency (`to_currency` / `to_cheque`)
131//!
132//! `Num2Word_OR` declares `CURRENCY_FORMS` for **INR, USD and EUR only** and
133//! overrides `to_currency` and `pluralize`. It does *not* define
134//! `CURRENCY_PRECISION` or `CURRENCY_ADJECTIVES`, and does not override
135//! `to_cheque`, `_money_verbose`, `_cents_verbose` or `_cents_terse` — those
136//! come from `Num2Word_Base`, whose `CURRENCY_PRECISION`/`CURRENCY_ADJECTIVES`
137//! are both `{}` (so the divisor is always the 100 default, and no adjective
138//! ever applies). The corresponding trait defaults are therefore already
139//! correct and are deliberately left alone.
140//!
141//! ## Faithfully reproduced Python bugs — currency
142//!
143//! C1. **An unknown currency code silently becomes INR.** The lookup is
144//!     `self.CURRENCY_FORMS.get(currency, list(self.CURRENCY_FORMS.values())[0])`
145//!     — a `dict.get` with a *default*, not a `[]` subscript. So `to_currency`
146//!     never raises `NotImplementedError`; it falls back to the first entry of
147//!     the dict literal, which is INR. Corpus pins this hard:
148//!     `to_currency(0, "JPY") == "ଶୂନ୍ୟ ଟଙ୍କା"` (Odia rupees, for yen), and
149//!     likewise for GBP/KWD/BHD/CNY/CHF. The *inherited* `to_cheque` uses a
150//!     strict `self.CURRENCY_FORMS[currency]` subscript and does raise for the
151//!     very same codes — hence `cheque:JPY` is a `NotImplementedError` row
152//!     while `currency:JPY` happily prints rupees. The two lookups must stay
153//!     distinct; see [`LangOr::forms_or_first`] vs [`Lang::currency_forms`].
154//! C2. **`CURRENCY_PRECISION` is ignored entirely.** OR's `to_currency` does
155//!     its own string surgery and never consults the divisor, so the 3-decimal
156//!     currencies (KWD/BHD, divisor 1000) and the 0-decimal ones (JPY,
157//!     divisor 1) are all treated as 2-decimal — and, per C1, as INR anyway.
158//!     `to_currency(12.34, "JPY") == "ବାର ଟଙ୍କା ତିରିଶ ଓ ଚାରି ପଇସା"`: yen
159//!     has no subunit at all, yet 34 paise are printed.
160//! C3. **`adjective` is accepted and never read.** No `prefix_currency` call.
161//! C4. **`pluralize` is dead code.** OR defines it, but its own `to_currency`
162//!     indexes the form tuple inline (`cr1[1] if left != 1 else cr1[0]`) and
163//!     the inherited `to_cheque` takes `cr1[-1]` directly. Nothing in either
164//!     path calls it. Implemented anyway, because Python defines it.
165//! C5. **Every form tuple has identical singular and plural** — `("ଟଙ୍କା",
166//!     "ଟଙ୍କା")`, `("ୟୁରୋ", "ୟୁରୋ")`, `("ସେଣ୍ଟ", "ସେଣ୍ଟ")`. The `left != 1`
167//!     and `right != 1` selections are therefore unobservable: both arms yield
168//!     the same word. Both entries are kept because the arity is load-bearing
169//!     (`cr1[1]` would `IndexError` on a 1-tuple) and because `to_cheque`'s
170//!     `cr1[-1]` reads the last one.
171//! C6. **A float with zero cents prints no cents segment.** `if cents and
172//!     right:` gates on `right` being *truthy*, and `1.0` parses to
173//!     `right == 0`. So `to_currency(1.0)` and `to_currency(1)` both give
174//!     "ଏକ ୟୁରୋ" — OR collapses the int/float distinction that
175//!     `Num2Word_Base.to_currency` preserves (base would render
176//!     "... ଶୂନ୍ୟ ସେଣ୍ଟ" for the float). Verified against the real module, not assumed:
177//!     corpus row `currency:EUR arg "1.0"` → "ଏକ ୟୁରୋ". The `CurrencyValue`
178//!     int/decimal split is still honoured on the way in — it is what
179//!     [`split_currency_parts`] reads — it simply cannot change the output
180//!     here.
181//!
182//! # Out of scope (present in Python, deliberately not ported)
183//!
184//! `to_cardinal`'s `"." in n` branch (spells the fractional part digit by
185//! digit against `pointword` = "ଦଶମିକ", mapping digit 0 to "ଶୂନ୍ୟ" via the
186//! `self.ones[0] or "ଶୂନ୍ୟ"` falsy-empty-string trick). The trait hands
187//! `to_cardinal` a `&BigInt`, so the decimal branch is unreachable by
188//! construction. `cardinal_from_decimal` is likewise left at its default:
189//! nothing in OR's currency surface can reach the fractional-cents path,
190//! because `to_currency` truncates to two digits itself and never delegates to
191//! `currency::default_to_currency`.
192
193use crate::base::{Lang, N2WError, Result};
194use crate::currency::{CurrencyForms, CurrencyValue};
195use crate::floatpath::FloatValue;
196use crate::strnum::{python_decimal_parse, ParsedNumber};
197use bigdecimal::BigDecimal;
198use num_bigint::BigInt;
199use num_integer::Integer;
200use num_traits::{One, Signed, ToPrimitive, Zero};
201use std::collections::HashMap;
202
203/// `setup()`: `self.negword = "ଋଣ "` — note the **trailing space**, which is
204/// what separates it from the number, and which `.strip()` then makes
205/// harmless at the ends.
206const NEGWORD: &str = "ଋଣ ";
207
208const ZERO_WORD: &str = "ଶୂନ୍ୟ";
209
210/// `self.ones`. Index 0 is `""` (used by the out-of-scope float path via
211/// `self.ones[int(digit)] or "ଶୂନ୍ୟ"`); `_int_to_word` guards 0 before
212/// reaching here, so only 1..=9 are ever read on the integer path.
213const ONES: [&str; 10] = [
214    "", "ଏକ", "ଦୁଇ", "ତିନି", "ଚାରି", "ପାଞ୍ଚ", "ଛଅ", "ସାତ", "ଆଠ", "ନଅ",
215];
216
217/// `self.teens`, covering 10..=19 (indexed as `number - 10`).
218const TEENS: [&str; 10] = [
219    "ଦଶ", "ଏଗାର", "ବାର", "ତେର", "ଚଉଦ", "ପନ୍ଦର", "ଷୋହଳ", "ସତର", "ଅଠର", "ଉଣେଇଶ",
220];
221
222/// `self.tens`. Only indices 2..=9 are reachable (bug 5). Index 7 is "ସତର",
223/// a verbatim duplicate of `TEENS[7]` (bug 2) — do not "correct" it.
224const TENS: [&str; 10] = [
225    "", "ଦଶ", "କୋଡ଼ିଏ", "ତିରିଶ", "ଚାଳିଶ", "ପଚାଶ", "ଷାଠିଏ", "ସତୁରୀ", "ଅଶୀ", "ନବେ",
226];
227
228/// Odia ordinals 1-10 are **suppletive**: Sanskrit-derived forms with their
229/// own stems, not the cardinal plus a suffix. ଏକ ("one") does not become
230/// *ଏକମ but ପ୍ରଥମ; ଛଅ ("six") becomes ଷଷ୍ଠ, not *ଛଅମ.
231///
232/// Index 0 is unused — Odia has no ordinal for zero, and upstream's blind
233/// suffix produced the non-word *ଶୂନ୍ୟମ for it.
234///
235/// From 11 up the suffix arm applies; see [`ORDINAL_SUFFIX`].
236const ORDINAL_IRREGULARS: [&str; 11] = [
237    "",
238    "ପ୍ରଥମ",
239    "ଦ୍ୱିତୀୟ",
240    "ତୃତୀୟ",
241    "ଚତୁର୍ଥ",
242    "ପଞ୍ଚମ",
243    "ଷଷ୍ଠ",
244    "ସପ୍ତମ",
245    "ଅଷ୍ଟମ",
246    "ନବମ",
247    "ଦଶମ",
248];
249
250/// The ordinal suffix for 11 and up, and for the digit form at every
251/// magnitude: ମ, upstream's "ma" written in Odia.
252///
253/// It is a real Odia ordinal marker — it is exactly what ends ପଞ୍ଚମ, ସପ୍ତମ,
254/// ଅଷ୍ଟମ, ନବମ and ଦଶମ above — which is why upstream's blind `+ "ma"` looked
255/// plausible across the whole range. It is wrong only where a suppletive
256/// form exists, i.e. 1..=10.
257const ORDINAL_SUFFIX: &str = "ମ";
258
259const HUNDRED: &str = "ଶହ";
260const THOUSAND: &str = "ହଜାର";
261/// `self.million` — the 10^6 multiplier word, literally "ten lakh" (bug 4).
262const MILLION: &str = "ଦଶ ଲକ୍ଷ";
263
264/// The key `list(self.CURRENCY_FORMS.values())[0]` resolves to.
265///
266/// Python evaluates that against a plain dict literal, and since 3.7 dicts
267/// iterate in insertion order — so this is "whichever code is written first in
268/// the `CURRENCY_FORMS` literal", which for `lang_OR.py` is INR. It is *not*
269/// "INR by name": reordering the Python literal would change this constant.
270/// Only [`LangOr::forms_or_first`] reads it (quirk C1).
271const FIRST_FORM_KEY: &str = "INR";
272
273/// Python's `str(abs(val)).split(".")` surgery, as `(left, right)`.
274///
275/// ```text
276/// parts = str(val).split(".")
277/// left  = int(parts[0]) if parts[0] else 0
278/// right = int(parts[1][:2].ljust(2, "0")) if len(parts) > 1 and parts[1] else 0
279/// ```
280///
281/// The original `str(val)` is gone by the time we get here — the boundary hands
282/// us `Decimal(str(value))` — so the digit string is rebuilt from the decimal's
283/// own unscaled value and exponent. That is deliberately *not* `Display`:
284/// `BigDecimal`'s `Display` switches to exponential notation past a leading- or
285/// trailing-zero threshold, and reconstructing the plain digits sidesteps that
286/// entirely. Equivalences that make this safe:
287///
288/// * `parts[0]` is the integer digits, so `left` is `trunc(|val|)`.
289/// * `parts[1][:2]` **truncates** to two digits and `.ljust(2, "0")` pads a
290///   1-digit fraction on the *right* — so `0.5` is 50 paise, not 5, and
291///   `12.345` is 34 paise, never rounded up to 35.
292/// * A missing `"."` and a fraction of `"0"` both give `right == 0`, which is
293///   why `1.0` and `1` agree (quirk C6).
294///
295/// Scale `<= 0` (an integral `Decimal` such as `Decimal("12")` or `1E+2`) has
296/// no `"."` in `str()` either, so `right` is 0 there too.
297fn split_currency_parts(val: &CurrencyValue) -> (BigInt, BigInt) {
298    let d = match val {
299        // `str(int)` never contains a ".", so parts[1] does not exist.
300        CurrencyValue::Int(v) => return (v.abs(), BigInt::zero()),
301        CurrencyValue::Decimal { value: d, .. } => d.abs(),
302    };
303
304    // value == unscaled * 10^-scale; the abs() above makes unscaled >= 0.
305    let (unscaled, scale) = d.as_bigint_and_exponent();
306    let digits = unscaled.to_string();
307
308    if scale <= 0 {
309        // Integral: trailing zeros are implied by the negative scale, so
310        // append them to recover the integer digits.
311        let mut int_digits = digits;
312        int_digits.push_str(&"0".repeat((-scale) as usize));
313        return (parse_digits(&int_digits), BigInt::zero());
314    }
315
316    // Left-pad so there is at least one integer digit, exactly as str() writes
317    // "0.01" rather than ".01".
318    let scale = scale as usize;
319    let padded = if digits.len() <= scale {
320        format!("{}{}", "0".repeat(scale - digits.len() + 1), digits)
321    } else {
322        digits
323    };
324    let split_at = padded.len() - scale;
325    let (int_part, frac_part) = padded.split_at(split_at);
326
327    // `parts[1][:2].ljust(2, "0")` — truncate to two digits, then pad right.
328    let mut cents: String = frac_part.chars().take(2).collect();
329    while cents.len() < 2 {
330        cents.push('0');
331    }
332
333    (parse_digits(int_part), parse_digits(&cents))
334}
335
336/// `int(<ascii digit string>)`. Every caller feeds it digits it just built from
337/// a `BigInt`'s own decimal form, so this cannot fail.
338fn parse_digits(s: &str) -> BigInt {
339    s.parse::<BigInt>()
340        .expect("digit string built from a BigInt's own decimal form")
341}
342
343/// CPython's `repr(float)` (== `str(float)`): shortest round-trip digits,
344/// fixed notation iff `-4 < decpt <= 16`, `.0` appended when integral,
345/// two-digit-padded exponent otherwise. `Num2Word_OR.to_cardinal` is driven
346/// entirely by this string — `str(1e16) == "1e+16"` has no `"."`, so `int()`
347/// raises `ValueError` there. Mirrors `lang_sk.rs`'s `python_repr_f64`.
348fn python_repr_f64(f: f64) -> String {
349    if f.is_nan() {
350        return "nan".to_string();
351    }
352    if f.is_infinite() {
353        return if f < 0.0 { "-inf" } else { "inf" }.to_string();
354    }
355
356    // `is_sign_negative` captures -0.0, whose repr is '-0.0'.
357    let neg = f.is_sign_negative();
358    let s = format!("{:e}", f.abs()); // e.g. "1.234e1", "5e-1", "0e0"
359    let (mantissa, exp_s) = match s.split_once('e') {
360        Some(parts) => parts,
361        None => return s, // unreachable for finite f64
362    };
363    let exp: i32 = exp_s.parse().unwrap_or(0);
364    let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
365    let ndigits = digits.len() as i32;
366    let decpt = exp + 1;
367
368    let body = if decpt <= -4 || decpt > 16 {
369        let mut m = String::new();
370        m.push_str(&digits[..1]);
371        if ndigits > 1 {
372            m.push('.');
373            m.push_str(&digits[1..]);
374        }
375        let e = decpt - 1;
376        let (esign, eabs) = if e < 0 { ('-', -e) } else { ('+', e) };
377        format!("{}e{}{:02}", m, esign, eabs)
378    } else if decpt <= 0 {
379        format!("0.{}{}", "0".repeat((-decpt) as usize), digits)
380    } else if decpt >= ndigits {
381        format!("{}{}.0", digits, "0".repeat((decpt - ndigits) as usize))
382    } else {
383        let dp = decpt as usize;
384        format!("{}.{}", &digits[..dp], &digits[dp..])
385    };
386
387    if neg {
388        format!("-{}", body)
389    } else {
390        body
391    }
392}
393
394/// CPython's `str(Decimal)`: trailing zeros preserved (`"1.10"` stays
395/// `"1.10"`), `E±n` notation exactly when `exp > 0` or the adjusted exponent
396/// `< -6` — so `Decimal("1E+2")` prints `"1E+2"` and feeds `int()` a
397/// `ValueError`, matching Python instead of the plain-notation
398/// reconstruction an earlier revision used. Mirrors `lang_sk.rs`'s
399/// `python_str_decimal`.
400fn python_str_decimal(bd: &BigDecimal) -> String {
401    let (coeff, scale) = bd.as_bigint_and_exponent();
402    let py_exp: i64 = -scale; // Decimal._exp
403    let neg = coeff.is_negative(); // BigInt drops the sign of a negative zero
404    let int_str = coeff.abs().to_string(); // Decimal._int
405    let ndigits = int_str.len() as i64;
406    let leftdigits = py_exp + ndigits;
407
408    // dotplace, non-engineering branch of _pydecimal.__str__.
409    let dotplace: i64 = if py_exp <= 0 && leftdigits > -6 {
410        leftdigits
411    } else {
412        1
413    };
414
415    let (intpart, fracpart) = if dotplace <= 0 {
416        (
417            "0".to_string(),
418            format!(".{}{}", "0".repeat((-dotplace) as usize), int_str),
419        )
420    } else if dotplace >= ndigits {
421        (
422            format!("{}{}", int_str, "0".repeat((dotplace - ndigits) as usize)),
423            String::new(),
424        )
425    } else {
426        let dp = dotplace as usize;
427        (int_str[..dp].to_string(), format!(".{}", &int_str[dp..]))
428    };
429
430    let exp = if leftdigits == dotplace {
431        String::new()
432    } else {
433        // "%+d" — a sign but no zero-padding, unlike float repr.
434        format!("E{:+}", leftdigits - dotplace)
435    };
436
437    let sign = if neg { "-" } else { "" };
438    format!("{}{}{}{}", sign, intpart, fracpart, exp)
439}
440
441pub struct LangOr {
442    /// `CURRENCY_FORMS`, built once here rather than per call.
443    ///
444    /// Python builds this dict once at class-definition time; the binding holds
445    /// one `LangOr` in a `OnceLock`, so this table is allocated exactly once per
446    /// process too.
447    forms: HashMap<&'static str, CurrencyForms>,
448}
449
450impl Default for LangOr {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456impl LangOr {
457    pub fn new() -> Self {
458        // Insertion order is irrelevant to a HashMap, but the *first* literal
459        // entry is what `list(values())[0]` picks — see FIRST_FORM_KEY. The
460        // order below mirrors lang_OR.py's literal so the two read alike.
461        let mut forms = HashMap::new();
462        forms.insert(
463            "INR",
464            CurrencyForms::new(&["ଟଙ୍କା", "ଟଙ୍କା"], &["ପଇସା", "ପଇସା"]),
465        );
466        forms.insert(
467            "USD",
468            CurrencyForms::new(&["ଡଲାର", "ଡଲାର"], &["ସେଣ୍ଟ", "ସେଣ୍ଟ"]),
469        );
470        forms.insert(
471            "EUR",
472            CurrencyForms::new(&["ୟୁରୋ", "ୟୁରୋ"], &["ସେଣ୍ଟ", "ସେଣ୍ଟ"]),
473        );
474        LangOr { forms }
475    }
476
477    /// `self.CURRENCY_FORMS.get(currency, list(self.CURRENCY_FORMS.values())[0])`
478    /// — the lenient lookup, with the INR fallback of quirk C1.
479    ///
480    /// Private, and deliberately *not* [`Lang::currency_forms`]: the inherited
481    /// `to_cheque` needs the strict subscript that raises. Conflating the two
482    /// would make `cheque:JPY` print rupees instead of raising.
483    fn forms_or_first(&self, currency: &str) -> &CurrencyForms {
484        self.forms
485            .get(currency)
486            .or_else(|| self.forms.get(FIRST_FORM_KEY))
487            .expect("the first-entry fallback key is always present in the table")
488    }
489
490    /// Python's `_int_to_word`. Called only with `number >= 0`: `to_cardinal`
491    /// strips the sign textually before ever reaching arithmetic.
492    ///
493    /// Python uses `divmod`, which floors. Operands here are non-negative, so
494    /// `div_mod_floor` and truncating division agree; `div_mod_floor` is used
495    /// anyway to mirror `divmod` exactly rather than rely on that coincidence.
496    fn int_to_word(&self, number: &BigInt) -> String {
497        if number.is_zero() {
498            return ZERO_WORD.to_string();
499        }
500
501        let ten = BigInt::from(10);
502        let hundred = BigInt::from(100);
503        let thousand = BigInt::from(1_000);
504        let million = BigInt::from(1_000_000);
505        let billion = BigInt::from(1_000_000_000);
506
507        // `if number < 10: return self.ones[number]`
508        if *number < ten {
509            // 1..=9 — 0 was returned above.
510            return ONES[number.to_usize().unwrap()].to_string();
511        }
512
513        // `if number < 20: return self.teens[number - 10]`
514        if *number < BigInt::from(20) {
515            return TEENS[(number - &ten).to_usize().unwrap()].to_string();
516        }
517
518        // `if number < 100: t, o = divmod(number, 10)`
519        // `return self.tens[t] + (" ଓ " + self.ones[o] if o else "")`
520        if *number < hundred {
521            let (t, o) = number.div_mod_floor(&ten);
522            let t = t.to_usize().unwrap(); // 2..=9
523            let o = o.to_usize().unwrap(); // 0..=9
524            let mut out = TENS[t].to_string();
525            if o != 0 {
526                out.push_str(" ଓ ");
527                out.push_str(ONES[o]);
528            }
529            return out;
530        }
531
532        // `if number < 1000: h, r = divmod(number, 100)`
533        // `base = self.ones[h] + " " + self.hundred`
534        // `return base + (" ଓ " + self._int_to_word(r) if r else "")`
535        //
536        // Note the separator here is " ଓ " (as in the tens), unlike the
537        // thousand/million branches below which use a bare " ".
538        if *number < thousand {
539            let (h, r) = number.div_mod_floor(&hundred);
540            let h = h.to_usize().unwrap(); // 1..=9
541            let mut out = format!("{} {}", ONES[h], HUNDRED);
542            if !r.is_zero() {
543                out.push_str(" ଓ ");
544                out.push_str(&self.int_to_word(&r));
545            }
546            return out;
547        }
548
549        // `if number < 1000000: t, r = divmod(number, 1000)`
550        // `base = self._int_to_word(t) + " " + self.thousand`
551        // `return base + (" " + self._int_to_word(r) if r else "")`
552        if *number < million {
553            let (t, r) = number.div_mod_floor(&thousand);
554            let mut out = format!("{} {}", self.int_to_word(&t), THOUSAND);
555            if !r.is_zero() {
556                out.push(' ');
557                out.push_str(&self.int_to_word(&r));
558            }
559            return out;
560        }
561
562        // `if number < 1000000000: m, r = divmod(number, 1000000)`
563        // `base = self._int_to_word(m) + " " + self.million`
564        // `return base + (" " + self._int_to_word(r) if r else "")`
565        if *number < billion {
566            let (m, r) = number.div_mod_floor(&million);
567            let mut out = format!("{} {}", self.int_to_word(&m), MILLION);
568            if !r.is_zero() {
569                out.push(' ');
570                out.push_str(&self.int_to_word(&r));
571            }
572            return out;
573        }
574
575        // `return str(number)` — bug 1. No words, no error, any magnitude.
576        number.to_string()
577    }
578
579    /// Port of the *string* body of `Num2Word_OR.to_cardinal`, driven from the
580    /// value's `str()` form (`n`):
581    ///
582    /// ```text
583    /// n = str(number).strip()
584    /// if n.startswith("-"):
585    ///     return (self.negword + self.to_cardinal(n[1:])).strip()
586    /// if "." in n:
587    ///     left, right = n.split(".", 1)
588    ///     ret = self._int_to_word(int(left)) + " " + self.pointword
589    ///     for digit in right:
590    ///         ret += " " + (self.ones[int(digit)] or "ଶୂନ୍ୟ")
591    ///     return ret.strip()
592    /// return self._int_to_word(int(n))
593    /// ```
594    ///
595    /// The fractional part is spelled **digit by digit** off `self.ones`, with
596    /// `ones[0] == ""` falling back to "ଶୂନ୍ୟ" (the `or "ଶୂନ୍ୟ"` trick) — so a
597    /// trailing/leading zero becomes "ଶୂନ୍ୟ", not silence. The integer part,
598    /// by contrast, goes through the full `_int_to_word` recursion. The sign is
599    /// handled textually: the "-" is sliced off and the call recurses on the
600    /// remaining string, so `negword` (which ends in a space) is prepended and
601    /// the final `.strip()` tidies the ends.
602    ///
603    /// `int(left)` / `int(n)` can be arbitrarily large — the fractional case of
604    /// `98746251323029.99` runs the integer part through `_int_to_word`, which
605    /// stringifies it verbatim past 10^9 (bug 1). Kept in `BigInt`, never cast.
606    fn cardinal_from_repr(&self, n: &str) -> Result<String> {
607        // `str(number).strip()`. Python's str.strip() and Rust's trim() agree
608        // on every character these reconstructed numeric strings can contain.
609        let n = n.trim();
610
611        // `if n.startswith("-"): return (negword + to_cardinal(n[1:])).strip()`
612        if let Some(rest) = n.strip_prefix('-') {
613            let inner = self.cardinal_from_repr(rest)?;
614            return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
615        }
616
617        // `if "." in n:` — split once, integer part in words, fraction spelled
618        // digit by digit.
619        if let Some((left, right)) = n.split_once('.') {
620            let left_bi = left.parse::<BigInt>().map_err(|_| {
621                N2WError::Value(format!("invalid literal for int() with base 10: {:?}", left))
622            })?;
623            let mut ret = format!("{} {}", self.int_to_word(&left_bi), self.pointword());
624            for ch in right.chars() {
625                let d = ch.to_digit(10).ok_or_else(|| {
626                    N2WError::Value(format!(
627                        "invalid literal for int() with base 10: {:?}",
628                        ch
629                    ))
630                })? as usize;
631                // `self.ones[int(digit)] or "ଶୂନ୍ୟ"`: ONES[0] == "" is falsy.
632                let word = if ONES[d].is_empty() { ZERO_WORD } else { ONES[d] };
633                ret.push(' ');
634                ret.push_str(word);
635            }
636            return Ok(ret.trim().to_string());
637        }
638
639        // `return self._int_to_word(int(n))`
640        let bi = n.parse::<BigInt>().map_err(|_| {
641            N2WError::Value(format!("invalid literal for int() with base 10: {:?}", n))
642        })?;
643        Ok(self.int_to_word(&bi))
644    }
645}
646
647impl Lang for LangOr {
648    /// This language's own `to_currency(currency=...)` default,
649    /// read from the live Python signature. Only 44 of 156 use EUR.
650    fn default_currency(&self) -> &str {
651        "INR"
652    }
653
654    /// This language's own `to_currency(separator=...)` default,
655    /// read from the live Python signature. Base's is ",", but only
656    /// 36 of 149 languages actually use it — most default to " " or a
657    /// conjunction, so inheriting Base's comma silently corrupts them.
658    fn default_separator(&self) -> &str {
659        " "
660    }
661
662    fn negword(&self) -> &str {
663        NEGWORD
664    }
665
666    fn pointword(&self) -> &str {
667        "ଦଶମିକ"
668    }
669
670    /// Python:
671    /// ```text
672    /// n = str(number).strip()
673    /// if n.startswith("-"):
674    ///     return (self.negword + self.to_cardinal(n[1:])).strip()
675    /// if "." in n: ...            # unreachable for BigInt input
676    /// return self._int_to_word(int(n))
677    /// ```
678    ///
679    /// The sign is handled *textually*: Python slices the "-" off the string
680    /// and recurses. For an integer that is exactly `abs()`, and `str(BigInt)`
681    /// never yields "-0", so there is no negative-zero wrinkle.
682    ///
683    /// `.strip()` is a no-op for integer input — `NEGWORD`'s trailing space is
684    /// always followed by a non-empty word, and `int_to_word` never returns a
685    /// string with leading or trailing whitespace. It is applied anyway to
686    /// mirror the source. (Python's `str.strip()` and Rust's `str::trim()`
687    /// disagree on a few exotic control characters such as U+001C..U+001F;
688    /// none can occur in these strings, so the two coincide here.)
689    fn to_cardinal(&self, value: &BigInt) -> Result<String> {
690        if *value < BigInt::zero() {
691            let inner = self.to_cardinal(&-value)?;
692            return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
693        }
694        Ok(self.int_to_word(value))
695    }
696
697    /// The suppletive form for 1..=10, otherwise the cardinal plus ମ.
698    ///
699    /// Upstream glued "ma" to *every* cardinal, which gives the non-words
700    /// *ଏକମ for 1st and *ଛଅମ for 6th where Odia has ପ୍ରଥମ and ଷଷ୍ଠ. Above 10
701    /// the suffix arm is kept exactly as upstream had it, so the inherited
702    /// quirks are unchanged: the suffix still lands on the final word of a
703    /// multi-word phrase, and still on a digit run from the 10^9 cliff
704    /// ("1000000000ମ").
705    fn to_ordinal(&self, value: &BigInt) -> Result<String> {
706        if let Some(i) = value.to_usize() {
707            if (1..=10).contains(&i) {
708                return Ok(ORDINAL_IRREGULARS[i].to_string());
709            }
710        }
711        Ok(format!("{}{}", self.to_cardinal(value)?, ORDINAL_SUFFIX))
712    }
713
714    /// The digits plus ମ — "1ମ", "10ମ", "21ମ". Unchanged from upstream apart
715    /// from the script: the digit form takes the suffix at every magnitude,
716    /// 1..=10 included, because ୧ମ is how the abbreviation is written even
717    /// where the spelled ordinal is suppletive.
718    ///
719    /// The minus sign survives: `to_ordinal_num(-1) == "-1ମ"`. This overrides
720    /// the base's bare `str(number)`, so the trait default must not be used.
721    fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
722        Ok(format!("{}{}", value, ORDINAL_SUFFIX))
723    }
724
725    /// `def to_year(self, val, longval=True): return self.to_cardinal(val)`
726    /// — `longval` is accepted and ignored; there is no century/pair logic, so
727    /// 1900 is "ଏକ ହଜାର ନଅ ଶହ", not "nineteen hundred".
728    fn to_year(&self, value: &BigInt) -> Result<String> {
729        self.to_cardinal(value)
730    }
731
732    /// Float/Decimal cardinal path.
733    ///
734    /// OR does **not** inherit `Num2Word_Base.to_cardinal_float`: it overrides
735    /// `to_cardinal` outright and lets a `float`/`Decimal` fall through it as
736    /// `str(number)` (§ "Out of scope" in the module header — now in scope).
737    /// So this override reproduces `to_cardinal`'s string branch rather than
738    /// delegating to `floatpath::default_to_cardinal_float`.
739    ///
740    /// `base.float2tuple` and its `< 0.01` f64-artefact heuristic are **never**
741    /// reached here — OR reads the value's `str()` digits directly. For a float
742    /// that string is `repr(value)`, reconstructed as `format!("{:.*}", prec,
743    /// value)`: Rust's fixed-precision formatting is correctly-rounded
744    /// ties-to-even, identical to Python's, and `prec` (the repr-derived
745    /// fractional-digit count carried on `FloatValue::Float`) makes it round to
746    /// exactly the digits `repr` chose — so `repr(2.675) == "2.675"` gives
747    /// fraction "675", not the `674999…` that float2tuple would surface.
748    ///
749    /// `precision_override` (the `precision=` kwarg) is **ignored**, matching
750    /// Python: OR's `to_cardinal` never reads `self.precision`, so setting it
751    /// has no effect on the output. The repr-derived precision is used instead.
752    fn to_cardinal_float(
753        &self,
754        value: &FloatValue,
755        _precision_override: Option<u32>,
756    ) -> Result<String> {
757        let n = match value {
758            // `str(float)` == repr(float), exponent form included — so 1e16
759            // reaches `int("1e+16")` and raises ValueError, as Python does.
760            FloatValue::Float { value, .. } => python_repr_f64(*value),
761            // `str(Decimal)` — scale preserved, `E+n` form for a positive
762            // exponent (`Decimal("1E+2")` raises too).
763            FloatValue::Decimal { value, .. } => python_str_decimal(value),
764        };
765        self.cardinal_from_repr(&n)
766    }
767
768    /// `to_cardinal(float/Decimal)` — the FULL entry. Python routes *every*
769    /// float/Decimal through the `str(number)` algorithm, so a whole value
770    /// keeps its visible point: `5.0` -> "ପାଞ୍ଚ ଦଶମିକ ଶୂନ୍ୟ", `-0.0` ->
771    /// "ଋଣ ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ", `Decimal("5.00")` -> "ପାଞ୍ଚ ଦଶମିକ
772    /// ଶୂନ୍ୟ ଶୂନ୍ୟ". The base default's whole-value shortcut must not fire.
773    fn cardinal_float_entry(
774        &self,
775        value: &FloatValue,
776        precision_override: Option<u32>,
777    ) -> Result<String> {
778        self.to_cardinal_float(value, precision_override)
779    }
780
781    /// `to_ordinal(float/Decimal)`: the float cardinal + ମ — the
782    /// cardinal being the string algorithm above, suffix glued on raw.
783    /// Exponent forms raise ValueError before the suffix is appended.
784    fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
785        Ok(format!(
786            "{}{}",
787            self.to_cardinal_float(value, None)?,
788            ORDINAL_SUFFIX
789        ))
790    }
791
792    /// `to_ordinal_num(float/Decimal)`: `str(number) + ମ`. `repr_str` is
793    /// the binding's Python `str(value)`, so exponent forms echo verbatim:
794    /// `to_ordinal_num(1e16)` == "1e+16ମ".
795    fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
796        Ok(format!("{}{}", repr_str, ORDINAL_SUFFIX))
797    }
798
799    /// `to_year(float/Decimal)`: OR's `to_year` forwards to `to_cardinal`,
800    /// which for a float/Decimal is the string algorithm above.
801    fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
802        self.to_cardinal_float(value, None)
803    }
804
805    /// Base's `str_to_number` parses "Infinity" *successfully*; OR's
806    /// ValueError comes later, from `int("Infinity")` inside `to_cardinal`.
807    /// The Rust dispatcher hard-codes Base's OverflowError for
808    /// `ParsedNumber::Inf`, which OR never executes — so Inf parses punt to
809    /// the Python fallback, which reproduces every mode byte for byte. NaN
810    /// stays on the dispatcher path (its hard-coded ValueError matches).
811    /// Same shape as `lang_as.rs`.
812    fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
813        let parsed = python_decimal_parse(s)?;
814        Ok(parsed)
815    }
816
817    /// `Decimal('Infinity')` / `-Infinity` reached OR. The parse succeeds
818    /// (Base's `Decimal(value)`), so the failure surfaces per mode inside OR's
819    /// own converters — **not** at Base's `int(Decimal('Infinity'))` (the trait
820    /// default `OverflowError`). OR's `to_cardinal` slices the sign textually
821    /// then does `int("Infinity")` → `ValueError`; `to_ordinal`
822    /// (`to_cardinal + ମ`) and `to_year` (`to_cardinal`) raise it first.
823    /// `to_ordinal_num` is the textual `str(number) + ମ`, so it echoes.
824    fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
825        match to {
826            "ordinal_num" => {
827                let token = if negative { "-Infinity" } else { "Infinity" };
828                Ok(format!("{}{}", token, ORDINAL_SUFFIX))
829            }
830            _ => Err(N2WError::Value(
831                "invalid literal for int() with base 10: 'Infinity'".to_string(),
832            )),
833        }
834    }
835
836    /// `Decimal('NaN')` reached OR. `int("NaN")` → `ValueError` on the
837    /// cardinal/ordinal/year paths; `to_ordinal_num` echoes "NaNମ".
838    fn nan_result(&self, to: &str) -> Result<String> {
839        match to {
840            "ordinal_num" => Ok(format!("NaN{}", ORDINAL_SUFFIX)),
841            _ => Err(N2WError::Value(
842                "invalid literal for int() with base 10: 'NaN'".to_string(),
843            )),
844        }
845    }
846
847    // ---- currency ----------------------------------------------------
848    //
849    // Not overridden, because Python does not override them either:
850    //   * `currency_precision`  — base's CURRENCY_PRECISION is `{}`, so
851    //     `.get(code, 100)` is always 100. The trait default returns 100.
852    //   * `currency_adjective`  — base's CURRENCY_ADJECTIVES is `{}` -> None.
853    //   * `money_verbose` / `cents_verbose` — base returns `to_cardinal(n)`;
854    //     the trait default does the same and resolves to OR's `to_cardinal`.
855    //   * `cents_terse`         — base's `%0*d` width-from-divisor logic.
856    //   * `to_cheque`           — inherited wholesale from `Num2Word_Base`;
857    //     `currency::default_to_cheque` is a faithful port of it, and needs
858    //     only `lang_name` + `currency_forms` from here.
859
860    /// `self.__class__.__name__`, for `to_cheque`'s NotImplementedError:
861    /// `Currency code "JPY" not implemented for "Num2Word_OR"`.
862    fn lang_name(&self) -> &str {
863        "Num2Word_OR"
864    }
865
866    /// The strict `self.CURRENCY_FORMS[currency]` subscript.
867    ///
868    /// Only the inherited `to_cheque` reads this, and it must raise
869    /// `NotImplementedError` on a miss — so, unlike `to_currency`, there is
870    /// **no** INR fallback here. See quirk C1 and [`LangOr::forms_or_first`].
871    fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
872        self.forms.get(code)
873    }
874
875    /// Port of `Num2Word_OR.pluralize`, which replaces base's abstract
876    /// `raise NotImplementedError`:
877    ///
878    /// ```text
879    /// if not forms:
880    ///     return ""
881    /// return forms[0] if n == 1 else forms[-1]
882    /// ```
883    ///
884    /// Dead code in practice (quirk C4) — nothing in OR's currency path calls
885    /// it — but Python defines it, so it is ported rather than left to the
886    /// default that raises. Note `forms[-1]`, the *last* form, not `forms[1]`:
887    /// identical for OR's 2-tuples, but the two differ for a 3-form table and
888    /// this is the one Python wrote.
889    fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
890        // `if not forms:` — an empty tuple is falsy.
891        let Some(last) = forms.last() else {
892            return Ok(String::new());
893        };
894        Ok(if n.is_one() {
895            forms[0].clone()
896        } else {
897            last.clone()
898        })
899    }
900
901    /// Port of `Num2Word_OR.to_currency`, which replaces the base version
902    /// entirely:
903    ///
904    /// ```text
905    /// is_negative = val < 0
906    /// val = abs(val)
907    /// parts = str(val).split(".")
908    /// left = int(parts[0]) if parts[0] else 0
909    /// right = int(parts[1][:2].ljust(2, "0")) if len(parts) > 1 and parts[1] else 0
910    /// cr1, cr2 = self.CURRENCY_FORMS.get(currency, list(self.CURRENCY_FORMS.values())[0])
911    /// result = self._int_to_word(left) + " " + (cr1[1] if left != 1 else cr1[0])
912    /// if cents and right:
913    ///     result += separator + self._int_to_word(right) + " " + (cr2[1] if right != 1 else cr2[0])
914    /// if is_negative:
915    ///     result = self.negword + result
916    /// return result.strip()
917    /// ```
918    ///
919    /// Everything the base version does that this one does not — `pluralize`,
920    /// `CURRENCY_PRECISION`, `adjective`, the `isinstance(val, int)` cents
921    /// branch — is covered by quirks C1..C6 in the module docs.
922    fn to_currency(
923        &self,
924        val: &CurrencyValue,
925        currency: &str,
926        cents: bool,
927        separator: Option<&str>,
928        // In the Python signature, never read — quirk C3.
929        _adjective: bool,
930    ) -> Result<String> {
931        // Trait now hands us None when the caller omitted separator=;
932        // resolve it to this language's own default before the ported body.
933        let separator = separator.unwrap_or(self.default_separator());
934        // `is_negative = val < 0` is evaluated *before* `val = abs(val)`.
935        let is_negative = val.is_negative();
936        let (left, right) = split_currency_parts(val);
937
938        // The lenient lookup: an unknown code yields INR, not an error (C1).
939        let forms = self.forms_or_first(currency);
940
941        // `cr1[1] if left != 1 else cr1[0]`. All three OR tables carry exactly
942        // two forms, so the fixed indices cannot go out of range — and both
943        // hold the same word anyway (C5).
944        let unit = if left.is_one() {
945            &forms.unit[0]
946        } else {
947            &forms.unit[1]
948        };
949        let mut result = format!("{} {}", self.int_to_word(&left), unit);
950
951        // `if cents and right:` — a zero `right` is falsy and skips the entire
952        // segment, however the value was written (C6).
953        if cents && !right.is_zero() {
954            let subunit = if right.is_one() {
955                &forms.subunit[0]
956            } else {
957                &forms.subunit[1]
958            };
959            result.push_str(separator);
960            result.push_str(&self.int_to_word(&right));
961            result.push(' ');
962            result.push_str(subunit);
963        }
964
965        if is_negative {
966            // `self.negword + result` — NEGWORD already ends in a space, so
967            // this is not the `"%s " % negword.strip()` form base uses.
968            result = format!("{}{}", NEGWORD, result);
969        }
970
971        // Python's trailing `.strip()`. A no-op in practice: `int_to_word`
972        // never returns "" and the string always ends in a currency word.
973        // Mirrored anyway.
974        Ok(result.trim().to_string())
975    }
976}
977
978#[cfg(test)]
979mod float_tests {
980    use super::*;
981    use crate::base::Lang;
982    use std::str::FromStr;
983
984    fn f(v: f64, p: u32) -> FloatValue {
985        FloatValue::Float { value: v, precision: p }
986    }
987    fn d(s: &str, p: u32) -> FloatValue {
988        FloatValue::Decimal { value: BigDecimal::from_str(s).unwrap(), precision: p }
989    }
990
991    #[test]
992    fn corpus_float() {
993        let l = LangOr::new();
994        let cases: &[(FloatValue, &str)] = &[
995            (f(0.0, 1), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ"),
996            (f(0.5, 1), "ଶୂନ୍ୟ ଦଶମିକ ପାଞ୍ଚ"),
997            (f(1.0, 1), "ଏକ ଦଶମିକ ଶୂନ୍ୟ"),
998            (f(1.5, 1), "ଏକ ଦଶମିକ ପାଞ୍ଚ"),
999            (f(2.25, 2), "ଦୁଇ ଦଶମିକ ଦୁଇ ପାଞ୍ଚ"),
1000            (f(3.14, 2), "ତିନି ଦଶମିକ ଏକ ଚାରି"),
1001            (f(0.01, 2), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ ଏକ"),
1002            (f(0.1, 1), "ଶୂନ୍ୟ ଦଶମିକ ଏକ"),
1003            (f(0.99, 2), "ଶୂନ୍ୟ ଦଶମିକ ନଅ ନଅ"),
1004            (f(1.01, 2), "ଏକ ଦଶମିକ ଶୂନ୍ୟ ଏକ"),
1005            (f(12.34, 2), "ବାର ଦଶମିକ ତିନି ଚାରି"),
1006            (f(99.99, 2), "ନବେ ଓ ନଅ ଦଶମିକ ନଅ ନଅ"),
1007            (f(100.5, 1), "ଏକ ଶହ ଦଶମିକ ପାଞ୍ଚ"),
1008            (f(1234.56, 2), "ଏକ ହଜାର ଦୁଇ ଶହ ଓ ତିରିଶ ଓ ଚାରି ଦଶମିକ ପାଞ୍ଚ ଛଅ"),
1009            (f(-0.5, 1), "ଋଣ ଶୂନ୍ୟ ଦଶମିକ ପାଞ୍ଚ"),
1010            (f(-1.5, 1), "ଋଣ ଏକ ଦଶମିକ ପାଞ୍ଚ"),
1011            (f(-12.34, 2), "ଋଣ ବାର ଦଶମିକ ତିନି ଚାରି"),
1012            (f(1.005, 3), "ଏକ ଦଶମିକ ଶୂନ୍ୟ ଶୂନ୍ୟ ପାଞ୍ଚ"),
1013            (f(2.675, 3), "ଦୁଇ ଦଶମିକ ଛଅ ସାତ ପାଞ୍ଚ"),
1014        ];
1015        for (v, want) in cases {
1016            let got = l.to_cardinal_float(v, None).unwrap();
1017            assert_eq!(&got, want, "float {:?}", v);
1018        }
1019    }
1020
1021    #[test]
1022    fn corpus_decimal() {
1023        let l = LangOr::new();
1024        let cases: &[(FloatValue, &str)] = &[
1025            (d("0.01", 2), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ ଏକ"),
1026            (d("1.10", 2), "ଏକ ଦଶମିକ ଏକ ଶୂନ୍ୟ"),
1027            (d("12.345", 3), "ବାର ଦଶମିକ ତିନି ଚାରି ପାଞ୍ଚ"),
1028            (d("98746251323029.99", 2), "98746251323029 ଦଶମିକ ନଅ ନଅ"),
1029            (d("0.001", 3), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ ଶୂନ୍ୟ ଏକ"),
1030        ];
1031        for (v, want) in cases {
1032            let got = l.to_cardinal_float(v, None).unwrap();
1033            assert_eq!(&got, want, "decimal {:?}", v);
1034        }
1035    }
1036}