Skip to main content

num2words2_core/
lang_tr.rs

1//! Port of `lang_TR.py` (Turkish).
2//!
3//! `Num2Word_TR` subclasses `Num2Word_Base` but overrides `to_cardinal` /
4//! `to_ordinal` / `to_splitnum` with its own digit-string algorithm, so this
5//! is a *self-contained* language: `cards`/`merge` are never consulted.
6//! `to_year` is not overridden in Python, so it falls through to
7//! `Num2Word_Base.to_year`, which is just `to_cardinal`.
8//!
9//! Turkish number words are written solid (no spaces): 1234 →
10//! "binikiyüzotuzdört". The Python algorithm walks the decimal digit string
11//! triplet by triplet with a thicket of special cases; this file transliterates
12//! that control flow branch for branch, including its bugs (see below).
13//!
14//! Faithfully reproduced Python quirks — do NOT "fix" these:
15//!
16//! * `ORDINAL_TRIPLETS[6]` is `"kentilyon"`, not `"kentilyonuncu"` — every
17//!   other entry carries the ordinal suffix. So `to_ordinal(10**18)` returns
18//!   "birkentilyon" (a cardinal), and `to_ordinal_num(10**18)` returns
19//!   "1000000000000000000'" with an empty suffix.
20//! * `to_cardinal` casts its argument to `float` before splitting digits
21//!   (`_to_cardinal_impl`), while `to_ordinal` does not. Above 2^53 the two
22//!   therefore disagree: `to_cardinal(1234567890123456789)` reads ...768 but
23//!   `to_ordinal` of the same reads ...789. `float_round_digits` reproduces
24//!   the double rounding exactly. See `python_float_int`.
25//! * Because of that float cast, the 65535 inputs in
26//!   `10**21 - 65536 ..= 10**21 - 2` (all still below MAXVAL = 10**21 - 1, so
27//!   they pass `verify_cardinal`) round *up* to the 22-digit "1000...0" and
28//!   index `CARDINAL_TRIPLETS[7]`, which Python raises `KeyError` for. Mapped
29//!   to `N2WError::Key`. `to_ordinal` is immune — it never casts to float, so
30//!   it tops out at 21 digits and `CARDINAL_TRIPLETS[6]`.
31//! * `verify_ordinal` raises `TypeError(errmsg_negord)` *inside* its own
32//!   `try`, which its `except (ValueError, TypeError)` then swallows and
33//!   re-raises as `TypeError(errmsg_nonnum)`. Negative ordinals therefore
34//!   report the "not a number" message, not the "not positive" one.
35//! * The issue-#64 fix (keep "bir" in e.g. 401607 → "dörtyüzbirbinaltıyüzyedi")
36//!   was applied to `to_cardinal` only. `to_ordinal` keeps the old
37//!   `not (T == 2 and d[2] == '1')` guard and still drops it, so
38//!   `to_ordinal(401607)` is "dörtyüzbinaltıyüzyedinci".
39//! * `verify_ordinal` does NOT raise on a non-integral value — it returns
40//!   `isordinal = False` and `to_ordinal` then returns its pristine `wrd`,
41//!   the empty string: `to_ordinal(0.5)` is `""`. Negatives are flagged by
42//!   the *numeric* `abs(value) == value` (so -0.0 passes and reads
43//!   "sıfırıncı") and re-raised as `TypeError(errmsg_nonnum)`; the MAXVAL
44//!   check still applies to non-integral values, so a huge non-whole
45//!   Decimal is OverflowError, not `""`. See `verify_ordinal_float`.
46//! * `to_ordinal_num` recovers the vowel-harmonised suffix by stripping
47//!   `to_cardinal(value)` off `to_ordinal(value)`; when that fails it keeps
48//!   the last four chars of the ordinal. For a non-integral value the
49//!   ordinal is `""` (above), the strip fails, and `""[-4:]` is `""` —
50//!   hence `to_ordinal_num(0.5)` == `"0.5'"`. And since
51//!   `ORDINAL_TRIPLETS[6]` lacks its suffix, `to_ordinal_num(1e20)` is
52//!   `"1e+20'"` (cardinal == ordinal == "yüzkentilyon", empty suffix).
53//! * `str_to_number` is Base's plain `Decimal(value)`. `Decimal("NaN")`
54//!   parses, then `verify_cardinal` dies on `abs(value) >= self.MAXVAL`
55//!   with `decimal.InvalidOperation` — not the int()-ValueError the
56//!   binding's generic NaN mapping assumes — while `verify_ordinal` turns
57//!   `int(NaN)`'s ValueError into `TypeError(errmsg_nonnum)`. A NaN cannot
58//!   ride `ParsedNumber` (BigDecimal has no NaN), so the override returns
59//!   NotImplemented and the shim reruns the original Python string path,
60//!   which owns every mode's exact outcome. Infinity stays on the generic
61//!   mapping: OverflowError, the same type TR raises via the MAXVAL compare.
62//!
63//! Grammatical kwargs (issue #64 part 2/3, upstream #486 + #534):
64//! `to_cardinal(value, spaced=False, precision=None, decimal_word=None)`.
65//! `precision` is assigned to `self.precision` (used only by the float
66//! grammar — the integer reader never consults it); `decimal_word` swaps
67//! `self.pointword` for the call; `spaced=True` re-tokenizes the solid
68//! output with `_insert_spaces`, a greedy longest-first tokenizer whose
69//! token set includes the *current* pointword (i.e. the caller's
70//! decimal_word) but NOT the negword — "eksi" survives only because its
71//! letters pass through the unknown-char branch verbatim. `to_year` is
72//! inherited from `Num2Word_Base` as `to_year(value, **kwargs)`: every
73//! kwarg is swallowed unread and the result is plain `to_cardinal(value)`.
74//! `to_ordinal`/`to_ordinal_num`/`to_currency` accept no extra kwargs
75//! (trait defaults fall back to Python's own TypeError).
76//!
77//! Currency quirks, equally deliberate:
78//!
79//! * `to_currency` short-circuits on a *pure* `int` and never consults
80//!   `CURRENCY_FORMS`: it ignores the `currency` argument entirely and always
81//!   appends `CURRENCY_UNIT` ("lira"), with no separating space. So
82//!   `to_currency(100, "JPY")` is "yüzlira", and an unimplemented code does
83//!   *not* raise on that path — only the inherited float path does.
84//! * That int branch also uses the bare `negword` ("eksibeşlira"), while the
85//!   inherited float path uses `"%s " % negword.strip()` ("eksi oniki avro,
86//!   otuzdört sent"). The space is present in one and absent in the other.
87//! * `pluralize` is `forms[0] if number == 1 else forms[0]` — a no-op ternary.
88//!   Turkish does not inflect the currency name, so `number` is dead.
89
90use crate::base::{KwVal, Kwargs, Lang, N2WError, Result};
91use crate::currency::{CurrencyForms, CurrencyValue};
92use crate::floatpath::FloatValue;
93use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
94use bigdecimal::{BigDecimal, RoundingMode};
95use num_bigint::BigInt;
96use num_integer::Integer;
97use num_traits::{One, Signed, Zero};
98use std::collections::HashMap;
99
100const ZERO: &str = "sıfır";
101const ZEROTH: &str = "sıfırıncı";
102const NEGWORD: &str = "eksi";
103const CARDINAL_HUNDRED: &str = "yüz";
104const ORDINAL_HUNDRED: &str = "yüzüncü";
105
106/// `self.CURRENCY_UNIT`. The int branch of `to_currency` hardcodes this
107/// regardless of the requested currency. (`CURRENCY_SUBUNIT`, "kuruş", is set
108/// alongside it in Python but never read — the subunit words come from
109/// `CURRENCY_FORMS` — so it is not modelled here.)
110const CURRENCY_UNIT: &str = "lira";
111
112const ERRMSG_NONNUM: &str = "Sadece sayılar yazıya çevrilebilir.";
113
114fn errmsg_toobig(value: &BigInt, maxval: &BigInt) -> String {
115    format!(
116        "abs({}) sayı yazıya çevirmek için çok büyük. \
117         Yazıya çevrilebilecek en büyük rakam {}.",
118        value, maxval
119    )
120}
121
122// The `_ => ""` arms stand in for Python's `dict.get(key, "")`: "0" is absent
123// from every table, and "1" is additionally absent from HUNDREDS (which is why
124// 100 reads "yüz", not "biryüz").
125
126fn cardinal_ones(d: u8) -> &'static str {
127    match d {
128        b'1' => "bir",
129        b'2' => "iki",
130        b'3' => "üç",
131        b'4' => "dört",
132        b'5' => "beş",
133        b'6' => "altı",
134        b'7' => "yedi",
135        b'8' => "sekiz",
136        b'9' => "dokuz",
137        _ => "",
138    }
139}
140
141fn ordinal_ones(d: u8) -> &'static str {
142    match d {
143        b'1' => "birinci",
144        b'2' => "ikinci",
145        b'3' => "üçüncü",
146        b'4' => "dördüncü",
147        b'5' => "beşinci",
148        b'6' => "altıncı",
149        b'7' => "yedinci",
150        b'8' => "sekizinci",
151        b'9' => "dokuzuncu",
152        _ => "",
153    }
154}
155
156fn cardinal_tens(d: u8) -> &'static str {
157    match d {
158        b'1' => "on",
159        b'2' => "yirmi",
160        b'3' => "otuz",
161        b'4' => "kırk",
162        b'5' => "elli",
163        b'6' => "altmış",
164        b'7' => "yetmiş",
165        b'8' => "seksen",
166        b'9' => "doksan",
167        _ => "",
168    }
169}
170
171fn ordinal_tens(d: u8) -> &'static str {
172    match d {
173        b'1' => "onuncu",
174        b'2' => "yirminci",
175        b'3' => "otuzuncu",
176        b'4' => "kırkıncı",
177        b'5' => "ellinci",
178        b'6' => "altmışıncı",
179        b'7' => "yetmişinci",
180        b'8' => "sekseninci",
181        b'9' => "doksanıncı",
182        _ => "",
183    }
184}
185
186/// Python's `HUNDREDS` — deliberately has no "1" key.
187fn hundreds(d: u8) -> &'static str {
188    match d {
189        b'2' => "iki",
190        b'3' => "üç",
191        b'4' => "dört",
192        b'5' => "beş",
193        b'6' => "altı",
194        b'7' => "yedi",
195        b'8' => "sekiz",
196        b'9' => "dokuz",
197        _ => "",
198    }
199}
200
201/// `CARDINAL_TRIPLETS[i]`. Python raises `KeyError` for i outside 1..=6.
202fn cardinal_triplet(i: usize) -> Result<&'static str> {
203    match i {
204        1 => Ok("bin"),
205        2 => Ok("milyon"),
206        3 => Ok("milyar"),
207        4 => Ok("trilyon"),
208        5 => Ok("katrilyon"),
209        6 => Ok("kentilyon"),
210        _ => Err(N2WError::Key(i.to_string())),
211    }
212}
213
214/// `ORDINAL_TRIPLETS[i]`. Note index 6 is "kentilyon" — missing the ordinal
215/// suffix in the Python source. Reproduced verbatim.
216fn ordinal_triplet(i: usize) -> Result<&'static str> {
217    match i {
218        1 => Ok("bininci"),
219        2 => Ok("milyonuncu"),
220        3 => Ok("milyarıncı"),
221        4 => Ok("trilyonuncu"),
222        5 => Ok("katrilyonuncu"),
223        6 => Ok("kentilyon"),
224        _ => Err(N2WError::Key(i.to_string())),
225    }
226}
227
228/// Python's `int(float(n))` for `n >= 0`.
229///
230/// `float(int)` rounds to nearest double, ties to even. Every double with
231/// magnitude >= 1 is an exact integer, so `int()` of it is lossless and the
232/// result is that rounded integer. Done on `BigInt` rather than via `to_f64`
233/// so the rounding mode is explicit and not dependent on num-bigint's
234/// conversion details.
235fn python_float_int(n: &BigInt) -> BigInt {
236    if n.is_zero() {
237        return BigInt::zero();
238    }
239    let bits = n.bits();
240    if bits <= 53 {
241        return n.clone();
242    }
243    let shift = (bits - 53) as usize;
244    let mut q = n >> shift;
245    let rem = n - (&q << shift);
246    let half = BigInt::one() << (shift - 1);
247    // Round half to even.
248    if rem > half || (rem == half && q.is_odd()) {
249        q += 1;
250    }
251    q << shift
252}
253
254/// The fields `to_splitnum` sets on the Python instance.
255///
256/// Only `integers_to_read[0]` (the integer digit string) is read by the
257/// integer paths, so the fractional element is not modelled.
258struct SplitNum {
259    /// `integers_to_read[0]`, ASCII decimal digits (byte indexing is safe).
260    digits: Vec<u8>,
261    total_triplets_to_read: usize,
262    total_digits_outside_triplets: usize,
263    order_of_last_zero_digit: usize,
264}
265
266impl SplitNum {
267    /// Python's `to_splitnum`, given the already-computed digit string
268    /// (`str(int(val))`).
269    fn new(digits: Vec<u8>) -> Self {
270        let l = digits.len();
271        let total_triplets_to_read = if l % 3 > 0 { l / 3 + 1 } else { l / 3 };
272        let total_digits_outside_triplets = l % 3;
273
274        // Count trailing zeros, but the Python loop is `range(len - 1)`, so the
275        // count saturates at l - 1: an all-zero string "0" yields 0, not 1.
276        let mut order_of_last_zero_digit = 0usize;
277        let mut found = false;
278        for i in 0..l.saturating_sub(1) {
279            if digits[l - 1 - i] == b'0' && !found {
280                order_of_last_zero_digit = i + 1;
281            } else {
282                found = true;
283            }
284        }
285
286        SplitNum {
287            digits,
288            total_triplets_to_read,
289            total_digits_outside_triplets,
290            order_of_last_zero_digit,
291        }
292    }
293}
294
295/// `Num2Word_TR.__init__` *rebinds* `self.CURRENCY_FORMS` to this dict rather
296/// than mutating the inherited one, so the `lang_EUR`/`lang_EN` shared-class-dict
297/// trap does not apply: TR sees exactly these three codes and nothing else.
298/// Verified against the live interpreter, not just the source.
299///
300/// Every entry is a 2-tuple with both forms identical — Turkish does not
301/// pluralize the currency name.
302fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
303    let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
304    m.insert("TRY", CurrencyForms::new(&["lira", "lira"], &["kuruş", "kuruş"]));
305    m.insert("EUR", CurrencyForms::new(&["avro", "avro"], &["sent", "sent"]));
306    m.insert("USD", CurrencyForms::new(&["dolar", "dolar"], &["sent", "sent"]));
307    m
308}
309
310pub struct LangTr {
311    maxval: BigInt,
312    currency_forms: HashMap<&'static str, CurrencyForms>,
313}
314
315impl Default for LangTr {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321impl LangTr {
322    pub fn new() -> Self {
323        // MAXVAL = 10 ** ((len(CARDINAL_TRIPLETS) + 1) * 3) - 1 = 10**21 - 1.
324        LangTr {
325            maxval: BigInt::from(10u8).pow(21u32) - 1,
326            // Built once here, never per call.
327            currency_forms: build_currency_forms(),
328        }
329    }
330
331    /// Python's `verify_cardinal`.
332    ///
333    /// The Python `float(value)` probe only guards non-numeric input (which
334    /// cannot occur for a `BigInt`). It *can* raise `OverflowError` for ints
335    /// above ~1.8e308, but any such value also trips the MAXVAL check below
336    /// and would raise `OverflowError` regardless, so the outcome is identical.
337    fn verify_cardinal(&self, value: &BigInt) -> Result<()> {
338        if value.abs() >= self.maxval {
339            return Err(N2WError::Overflow(errmsg_toobig(value, &self.maxval)));
340        }
341        Ok(())
342    }
343
344    /// Python's `verify_ordinal`.
345    ///
346    /// The negative check precedes the MAXVAL check, so a large negative is a
347    /// `TypeError`, not an `OverflowError`. And the `TypeError` Python raises
348    /// there is caught by its own `except (ValueError, TypeError)` and
349    /// re-raised with `errmsg_nonnum` — hence that message here.
350    fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
351        if value.is_negative() {
352            return Err(N2WError::Type(ERRMSG_NONNUM.to_string()));
353        }
354        if value.abs() >= self.maxval {
355            return Err(N2WError::Overflow(errmsg_toobig(value, &self.maxval)));
356        }
357        Ok(())
358    }
359
360    /// Python's `_to_cardinal_impl` for integral input.
361    fn to_cardinal_impl(&self, value: &BigInt) -> Result<String> {
362        self.verify_cardinal(value)?;
363
364        // Python: `if str(value).startswith("-")` → negword, then re-parses the
365        // digits as a float. Both branches go through `float(...)`, so the
366        // digit string is the float-rounded one either way.
367        let (pre_word, magnitude) = if value.is_negative() {
368            (NEGWORD, value.abs())
369        } else {
370            ("", value.clone())
371        };
372
373        let digits = python_float_int(&magnitude).to_string().into_bytes();
374        let sn = SplitNum::new(digits);
375        self.read_cardinal(&sn, pre_word)
376    }
377
378    fn read_cardinal(&self, sn: &SplitNum, pre_word: &str) -> Result<String> {
379        let d = &sn.digits;
380        let l = d.len();
381        let t = sn.total_triplets_to_read;
382        let o = sn.total_digits_outside_triplets;
383        let z = sn.order_of_last_zero_digit;
384        let mut wrd = String::new();
385
386        // Dead in practice: z saturates at l - 1. Kept for structural fidelity.
387        if z >= l {
388            return Ok(format!("{}{}", pre_word, wrd));
389        }
390
391        if t == 1 {
392            if o == 2 {
393                if z == 1 {
394                    wrd += cardinal_tens(d[0]);
395                    return Ok(format!("{}{}", pre_word, wrd));
396                }
397                if z == 0 {
398                    wrd += cardinal_tens(d[0]);
399                    wrd += cardinal_ones(d[1]);
400                }
401                return Ok(format!("{}{}", pre_word, wrd));
402            }
403
404            if o == 1 && z == 0 {
405                wrd += cardinal_ones(d[0]);
406                // Note: zero returns bare ZERO, dropping pre_word.
407                if d[0] == b'0' {
408                    return Ok(ZERO.to_string());
409                }
410                return Ok(format!("{}{}", pre_word, wrd));
411            }
412
413            if o == 0 {
414                if z == 2 {
415                    wrd += hundreds(d[0]);
416                    wrd += CARDINAL_HUNDRED;
417                    return Ok(format!("{}{}", pre_word, wrd));
418                }
419                if z == 1 {
420                    wrd += hundreds(d[0]);
421                    wrd += CARDINAL_HUNDRED;
422                    wrd += cardinal_tens(d[1]);
423                    return Ok(format!("{}{}", pre_word, wrd));
424                }
425                if z == 0 {
426                    wrd += hundreds(d[0]);
427                    wrd += CARDINAL_HUNDRED;
428                    wrd += cardinal_tens(d[1]);
429                    wrd += cardinal_ones(d[2]);
430                    return Ok(format!("{}{}", pre_word, wrd));
431                }
432            }
433        }
434
435        if t >= 2 {
436            // Leading (partial) triplet. Branches that do not return fall
437            // through into the triplet loop below.
438            if o == 2 {
439                if z == l - 1 {
440                    wrd += cardinal_tens(d[0]);
441                    wrd += cardinal_triplet(t - 1)?;
442                    return Ok(format!("{}{}", pre_word, wrd));
443                }
444                if z == l - 2 {
445                    wrd += cardinal_tens(d[0]);
446                    wrd += cardinal_ones(d[1]);
447                    wrd += cardinal_triplet(t - 1)?;
448                    return Ok(format!("{}{}", pre_word, wrd));
449                }
450                if z < l - 2 {
451                    wrd += cardinal_tens(d[0]);
452                    wrd += cardinal_ones(d[1]);
453                    wrd += cardinal_triplet(t - 1)?;
454                }
455            }
456
457            if o == 1 {
458                if z == l - 1 {
459                    // "bin", not "birbin", for exactly 1_000..1_999-shaped leads.
460                    if !(t == 2 && d[0] == b'1') {
461                        wrd += cardinal_ones(d[0]);
462                    }
463                    wrd += cardinal_triplet(t - 1)?;
464                    return Ok(format!("{}{}", pre_word, wrd));
465                }
466                if z < l - 1 {
467                    if !(t == 2 && d[0] == b'1') {
468                        wrd += cardinal_ones(d[0]);
469                    }
470                    wrd += cardinal_triplet(t - 1)?;
471                }
472            }
473
474            if o == 0 {
475                if z == l - 1 {
476                    wrd += hundreds(d[0]);
477                    wrd += CARDINAL_HUNDRED;
478                    wrd += cardinal_triplet(t - 1)?;
479                    return Ok(format!("{}{}", pre_word, wrd));
480                }
481                if z == l - 2 {
482                    wrd += hundreds(d[0]);
483                    wrd += CARDINAL_HUNDRED;
484                    wrd += cardinal_tens(d[1]);
485                    wrd += cardinal_triplet(t - 1)?;
486                    return Ok(format!("{}{}", pre_word, wrd));
487                }
488                if z == l - 3 {
489                    wrd += hundreds(d[0]);
490                    wrd += CARDINAL_HUNDRED;
491                    wrd += cardinal_tens(d[1]);
492                    wrd += cardinal_ones(d[2]);
493                    wrd += cardinal_triplet(t - 1)?;
494                    return Ok(format!("{}{}", pre_word, wrd));
495                }
496                if z < l - 3 {
497                    // Issue #64: unlike to_ordinal, this keeps "bir"
498                    // (401607 → "dörtyüzbirbinaltıyüzyedi").
499                    wrd += hundreds(d[0]);
500                    wrd += CARDINAL_HUNDRED;
501                    wrd += cardinal_tens(d[1]);
502                    wrd += cardinal_ones(d[2]);
503                    wrd += cardinal_triplet(t - 1)?;
504                }
505            }
506
507            for i in (1..=t - 1).rev() {
508                let reading_triplet_order = t - i;
509                let lrdo = if o == 0 {
510                    reading_triplet_order * 3
511                } else {
512                    (reading_triplet_order - 1) * 3 + o
513                };
514                // lrdo + 2 <= l - 1 for every reachable i; see the module tests.
515                if &d[lrdo..lrdo + 3] == b"000" {
516                    continue;
517                }
518
519                if d[lrdo] != b'0' {
520                    wrd += hundreds(d[lrdo]);
521                    if z == l - lrdo - 1 {
522                        if i == 1 {
523                            wrd += CARDINAL_HUNDRED;
524                            return Ok(format!("{}{}", pre_word, wrd));
525                        } else {
526                            wrd += CARDINAL_HUNDRED;
527                            wrd += cardinal_triplet(i - 1)?;
528                            return Ok(format!("{}{}", pre_word, wrd));
529                        }
530                    } else {
531                        wrd += CARDINAL_HUNDRED;
532                    }
533                }
534
535                if d[lrdo + 1] != b'0' {
536                    if z == l - lrdo - 2 {
537                        if i == 1 {
538                            wrd += cardinal_tens(d[lrdo + 1]);
539                            return Ok(format!("{}{}", pre_word, wrd));
540                        } else {
541                            wrd += cardinal_tens(d[lrdo + 1]);
542                            wrd += cardinal_triplet(i - 1)?;
543                            return Ok(format!("{}{}", pre_word, wrd));
544                        }
545                    } else {
546                        wrd += cardinal_tens(d[lrdo + 1]);
547                    }
548                }
549
550                if d[lrdo + 2] != b'0' {
551                    if z == l - lrdo - 3 {
552                        if i == 1 {
553                            wrd += cardinal_ones(d[lrdo + 2]);
554                            return Ok(format!("{}{}", pre_word, wrd));
555                        }
556                        if i == 2 {
557                            // "x001yyy" drops the "bir" before "bin".
558                            if &d[lrdo..lrdo + 2] != b"00" || d[lrdo + 2] != b'1' {
559                                wrd += cardinal_ones(d[lrdo + 2]);
560                            }
561                            wrd += cardinal_triplet(i - 1)?;
562                            return Ok(format!("{}{}", pre_word, wrd));
563                        }
564                        // i > 2
565                        wrd += cardinal_ones(d[lrdo + 2]);
566                        wrd += cardinal_triplet(i - 1)?;
567                        return Ok(format!("{}{}", pre_word, wrd));
568                    } else if &d[lrdo..lrdo + 2] != b"00" {
569                        wrd += cardinal_ones(d[lrdo + 2]);
570                    } else if i == 2 && d[lrdo + 2] != b'1' {
571                        // Python's inner `if not d[lrdo:lrdo+2] == "00"` here is
572                        // dead (it is "00" by the branch we are in), so only the
573                        // elif can fire. For i != 2 the ones digit of a "00x"
574                        // triplet is dropped entirely — reproduced.
575                        wrd += cardinal_ones(d[lrdo + 2]);
576                    }
577                }
578
579                wrd += cardinal_triplet(i - 1)?;
580            }
581        }
582
583        Ok(format!("{}{}", pre_word, wrd))
584    }
585
586    /// Python's `to_ordinal`. Note it feeds `to_splitnum` the *exact* integer,
587    /// with no float round-trip — unlike `to_cardinal`.
588    fn to_ordinal_impl(&self, value: &BigInt) -> Result<String> {
589        self.verify_ordinal(value)?;
590
591        let sn = SplitNum::new(value.to_string().into_bytes());
592        let d = &sn.digits;
593        let l = d.len();
594        let t = sn.total_triplets_to_read;
595        let o = sn.total_digits_outside_triplets;
596        let z = sn.order_of_last_zero_digit;
597        let mut wrd = String::new();
598
599        if z >= l {
600            return Ok(wrd);
601        }
602
603        if t == 1 {
604            if o == 2 {
605                if z == 1 {
606                    wrd += ordinal_tens(d[0]);
607                    return Ok(wrd);
608                }
609                if z == 0 {
610                    wrd += cardinal_tens(d[0]);
611                    wrd += ordinal_ones(d[1]);
612                    return Ok(wrd);
613                }
614            }
615
616            if o == 1 && z == 0 {
617                wrd += ordinal_ones(d[0]);
618                if d[0] == b'0' {
619                    return Ok(ZEROTH.to_string());
620                }
621                return Ok(wrd);
622            }
623
624            if o == 0 {
625                if z == 2 {
626                    wrd += hundreds(d[0]);
627                    wrd += ORDINAL_HUNDRED;
628                    return Ok(wrd);
629                }
630                if z == 1 {
631                    wrd += hundreds(d[0]);
632                    wrd += CARDINAL_HUNDRED;
633                    wrd += ordinal_tens(d[1]);
634                    return Ok(wrd);
635                }
636                if z == 0 {
637                    wrd += hundreds(d[0]);
638                    wrd += CARDINAL_HUNDRED;
639                    wrd += cardinal_tens(d[1]);
640                    if d[2] != b'0' {
641                        wrd += ordinal_ones(d[2]);
642                    }
643                    return Ok(wrd);
644                }
645            }
646        }
647
648        if t >= 2 {
649            if o == 2 {
650                if z == l - 1 {
651                    wrd += cardinal_tens(d[0]);
652                    wrd += ordinal_triplet(t - 1)?;
653                    return Ok(wrd);
654                }
655                if z == l - 2 {
656                    wrd += cardinal_tens(d[0]);
657                    wrd += cardinal_ones(d[1]);
658                    wrd += ordinal_triplet(t - 1)?;
659                    return Ok(wrd);
660                }
661                if z < l - 2 {
662                    wrd += cardinal_tens(d[0]);
663                    wrd += cardinal_ones(d[1]);
664                    wrd += cardinal_triplet(t - 1)?;
665                }
666            }
667
668            if o == 1 {
669                if z == l - 1 {
670                    if !(t == 2 && d[0] == b'1') {
671                        wrd += cardinal_ones(d[0]);
672                    }
673                    wrd += ordinal_triplet(t - 1)?;
674                    return Ok(wrd);
675                }
676                if z < l - 1 {
677                    if !(t == 2 && d[0] == b'1') {
678                        wrd += cardinal_ones(d[0]);
679                    }
680                    wrd += cardinal_triplet(t - 1)?;
681                }
682            }
683
684            if o == 0 {
685                if z == l - 1 {
686                    wrd += hundreds(d[0]);
687                    wrd += CARDINAL_HUNDRED;
688                    wrd += ordinal_triplet(t - 1)?;
689                    return Ok(wrd);
690                }
691                if z == l - 2 {
692                    wrd += hundreds(d[0]);
693                    wrd += CARDINAL_HUNDRED;
694                    wrd += cardinal_tens(d[1]);
695                    wrd += ordinal_triplet(t - 1)?;
696                    return Ok(wrd);
697                }
698                if z == l - 3 {
699                    wrd += hundreds(d[0]);
700                    wrd += CARDINAL_HUNDRED;
701                    wrd += cardinal_tens(d[1]);
702                    wrd += cardinal_ones(d[2]);
703                    wrd += ordinal_triplet(t - 1)?;
704                    return Ok(wrd);
705                }
706                if z < l - 3 {
707                    wrd += hundreds(d[0]);
708                    wrd += CARDINAL_HUNDRED;
709                    wrd += cardinal_tens(d[1]);
710                    // The issue-#64 fix was NOT applied here: to_ordinal still
711                    // drops "bir" where to_cardinal keeps it.
712                    if !(t == 2 && d[2] == b'1') {
713                        wrd += cardinal_ones(d[2]);
714                    }
715                    wrd += cardinal_triplet(t - 1)?;
716                }
717            }
718
719            for i in (1..=t - 1).rev() {
720                let reading_triplet_order = t - i;
721                let lrdo = if o == 0 {
722                    reading_triplet_order * 3
723                } else {
724                    (reading_triplet_order - 1) * 3 + o
725                };
726                if &d[lrdo..lrdo + 3] == b"000" {
727                    continue;
728                }
729
730                if d[lrdo] != b'0' {
731                    // Equivalent to to_cardinal's `hundreds(d[lrdo])`: HUNDREDS
732                    // simply lacks a "1" key. Spelled out as Python does here.
733                    if d[lrdo] != b'1' {
734                        wrd += cardinal_ones(d[lrdo]);
735                    }
736                    if z == l - lrdo - 1 {
737                        if i == 1 {
738                            wrd += ORDINAL_HUNDRED;
739                            return Ok(wrd);
740                        } else {
741                            wrd += CARDINAL_HUNDRED;
742                            wrd += ordinal_triplet(i - 1)?;
743                            return Ok(wrd);
744                        }
745                    } else {
746                        wrd += CARDINAL_HUNDRED;
747                    }
748                }
749
750                if d[lrdo + 1] != b'0' {
751                    if z == l - lrdo - 2 {
752                        if i == 1 {
753                            wrd += ordinal_tens(d[lrdo + 1]);
754                            return Ok(wrd);
755                        } else {
756                            wrd += cardinal_tens(d[lrdo + 1]);
757                            wrd += ordinal_triplet(i - 1)?;
758                            return Ok(wrd);
759                        }
760                    } else {
761                        wrd += cardinal_tens(d[lrdo + 1]);
762                    }
763                }
764
765                if d[lrdo + 2] != b'0' {
766                    if z == l - lrdo - 3 {
767                        if i == 1 {
768                            wrd += ordinal_ones(d[lrdo + 2]);
769                            return Ok(wrd);
770                        }
771                        if i == 2 {
772                            if &d[lrdo..lrdo + 2] != b"00" || d[lrdo + 2] != b'1' {
773                                wrd += cardinal_ones(d[lrdo + 2]);
774                            }
775                            wrd += ordinal_triplet(i - 1)?;
776                            return Ok(wrd);
777                        }
778                        wrd += cardinal_ones(d[lrdo + 2]);
779                        wrd += ordinal_triplet(i - 1)?;
780                        return Ok(wrd);
781                    } else if &d[lrdo..lrdo + 2] != b"00" {
782                        wrd += cardinal_ones(d[lrdo + 2]);
783                    } else if d[lrdo + 2] != b'1' {
784                        // NOTE: unlike to_cardinal's matching branch, Python has
785                        // no `if i == 2` guard here, so the "00x" ones digit is
786                        // emitted for every triplet (only a literal "001" is
787                        // dropped). to_cardinal instead drops it whenever i != 2.
788                        // The asymmetry is Python's; do not "harmonise" it.
789                        wrd += cardinal_ones(d[lrdo + 2]);
790                    }
791                }
792
793                wrd += cardinal_triplet(i - 1)?;
794            }
795        }
796
797        Ok(wrd)
798    }
799
800    /// Python's `verify_ordinal` on a float/Decimal (see module header).
801    ///
802    /// * `int(value) == value` failing does NOT raise — it only clears
803    ///   `isordinal`, and the caller then returns the pristine `""`.
804    /// * `not abs(value) == value` is a *numeric* comparison: -0.0 passes
805    ///   (0.0 == -0.0), any true negative raises TypeError — which Python's
806    ///   own `except (ValueError, TypeError)` re-raises with `errmsg_nonnum`.
807    ///   `FloatValue::is_negative()` is sign-bit aware and would wrongly
808    ///   flag -0.0, hence the explicit `< 0` checks.
809    /// * The MAXVAL check runs for non-integral values too, so a huge
810    ///   non-whole Decimal is OverflowError, not `""`.
811    fn verify_ordinal_float(&self, value: &FloatValue) -> Result<()> {
812        let numerically_negative = match value {
813            FloatValue::Float { value: f, .. } => *f < 0.0,
814            // Decimal("-0.0") never reaches this arm: the binding re-routes
815            // signed decimal zero through the Float arm.
816            FloatValue::Decimal { value: d, .. } => d.is_negative(),
817        };
818        if numerically_negative {
819            return Err(N2WError::Type(ERRMSG_NONNUM.to_string()));
820        }
821        if self.float_exceeds_maxval(value) {
822            return Err(N2WError::Overflow(errmsg_toobig_repr(
823                &float_value_repr(value),
824                &self.maxval,
825            )));
826        }
827        Ok(())
828    }
829
830    /// `abs(value) >= self.MAXVAL` on the original float/Decimal, exactly.
831    fn float_exceeds_maxval(&self, value: &FloatValue) -> bool {
832        if let Some(i) = value.as_whole_int() {
833            return i.abs() >= self.maxval;
834        }
835        match value {
836            // A non-whole f64 is < 2^53 ~ 9e15, five orders below MAXVAL.
837            FloatValue::Float { .. } => false,
838            FloatValue::Decimal { value: d, .. } => {
839                d.abs() >= BigDecimal::from(self.maxval.clone())
840            }
841        }
842    }
843
844    /// Port of `Num2Word_TR.to_cardinal_float` (issue #64 part 3 / upstream
845    /// #487 + #534), parameterized on the pointword so `decimal_word=` can
846    /// swap it per call. TR does *not* use `Num2Word_Base.to_cardinal_float`
847    /// (`float2tuple`); it formats the value to a fixed number of decimals
848    /// with Python's `"{:.{p}f}".format` and reads the integer/fraction
849    /// parts.
850    ///
851    /// The float-vs-Decimal split is load-bearing: `"{:.2f}"` of the *float*
852    /// `12.345` is "12.35" (the double is 12.34500000000000064), while of the
853    /// *Decimal* `12.345` it is "12.34" (exact half-to-even at the 2-decimal
854    /// scale). `FloatValue` keeps them apart, so each arm reproduces its own
855    /// rounding: `f64` string formatting for `Float`, `with_scale_round(0,
856    /// HalfEven)` on `abs*10^p` for `Decimal` (both round half-to-even).
857    ///
858    /// `precision_override` is the `precision=` kwarg, which Python assigns
859    /// to `self.precision`; the default is 2. `natural` (the repr-derived
860    /// fractional-digit count, i.e. `FloatValue::precision`) only shrinks the
861    /// effective precision when the caller left it at the default 2 and the
862    /// value has a single fractional digit (so 0.1 -> "bir", not "on").
863    fn cardinal_float_impl(
864        &self,
865        value: &FloatValue,
866        precision_override: Option<u32>,
867        pointword: &str,
868    ) -> Result<String> {
869        // self.precision (default 2) possibly overridden by the kwarg.
870        let self_precision: u32 = precision_override.unwrap_or(2);
871        // natural = abs(Decimal(str(abs_value)).as_tuple().exponent).
872        let natural: u32 = value.precision();
873        let prec: usize = if self_precision == 2 && natural != 0 && natural < 2 {
874            natural as usize
875        } else {
876            self_precision.max(1) as usize
877        };
878
879        let is_negative = value.is_negative();
880
881        // whole_str = "{:.{prec}f}".format(abs_value) -> (int_part, frac_part),
882        // frac_part having exactly `prec` digits. Reproduce Python's rounding
883        // per arm; never collapse Float into Decimal or the digits can differ.
884        let (int_part, frac_part) = match value {
885            FloatValue::Float { value: f, .. } => {
886                let s = format!("{:.*}", prec, f.abs());
887                match s.split_once('.') {
888                    Some((i, fr)) => (i.to_string(), fr.to_string()),
889                    None => (s, "0".repeat(prec)),
890                }
891            }
892            FloatValue::Decimal { value: d, .. } => {
893                let ten_pow = BigInt::from(10).pow(prec as u32);
894                // round(abs*10^prec) half-to-even == "{:.prec f}" for Decimal.
895                let scaled = (d.abs() * BigDecimal::from(ten_pow.clone()))
896                    .with_scale_round(0, RoundingMode::HalfEven)
897                    .as_bigint_and_exponent()
898                    .0;
899                let int_val = &scaled / &ten_pow;
900                let frac_val = &scaled % &ten_pow;
901                let frac_str = frac_val.to_string();
902                let frac_part = format!(
903                    "{}{}",
904                    "0".repeat(prec.saturating_sub(frac_str.len())),
905                    frac_str
906                );
907                (int_val.to_string(), frac_part)
908            }
909        };
910
911        let mut wrd = String::from(pointword);
912        if prec == 2 {
913            // Historical 2-digit reading: preserve tens-and-ones, with the
914            // "0x" leading-zero case spelled "sıfır <one>".
915            let fb = frac_part.as_bytes();
916            if fb[0] == b'0' && fb[1] != b'0' {
917                wrd.push_str(ZERO);
918                wrd.push_str(cardinal_ones(fb[1]));
919            } else {
920                wrd.push_str(cardinal_tens(fb[0]));
921                wrd.push_str(cardinal_ones(fb[1]));
922            }
923        } else {
924            // Read the fractional part as one integer, spelling leading zeros
925            // out separately (as 0.03 reads at precision 2).
926            let stripped = frac_part.trim_start_matches('0');
927            let leading_zeros = frac_part.len() - stripped.len();
928            for _ in 0..leading_zeros {
929                wrd.push_str(ZERO);
930            }
931            if !stripped.is_empty() {
932                let n: BigInt = stripped.parse().unwrap();
933                wrd.push_str(&self.to_cardinal_impl(&n)?);
934            }
935        }
936
937        if int_part == "0" {
938            wrd = format!("{}{}", ZERO, wrd);
939        } else {
940            let n: BigInt = int_part.parse().unwrap();
941            wrd = format!("{}{}", self.to_cardinal_impl(&n)?, wrd);
942        }
943
944        if is_negative {
945            // Python: `self.negword + " " + wrd` — the *spaced* form, unlike
946            // the currency int fast-path which uses the bare negword.
947            wrd = format!("{} {}", NEGWORD, wrd);
948        }
949        Ok(wrd)
950    }
951}
952
953/// `errmsg_toobig` with the value already rendered. Only the exception
954/// *type* is corpus-observable for these rows.
955fn errmsg_toobig_repr(value_repr: &str, maxval: &BigInt) -> String {
956    format!(
957        "abs({}) sayı yazıya çevirmek için çok büyük. \
958         Yazıya çevrilebilecek en büyük rakam {}.",
959        value_repr, maxval
960    )
961}
962
963/// Best-effort Python `str(value)` for the OverflowError message.
964///
965/// Reachable only for |value| >= MAXVAL ~ 1e21. `str(Decimal)` is exact via
966/// `python_decimal_str`; a float that big always reprs in exponent form, so
967/// Rust's `{:e}` ("1e21") is massaged into Python's shape ("1e+21").
968fn float_value_repr(value: &FloatValue) -> String {
969    match value {
970        FloatValue::Float { value: f, .. } => {
971            let s = format!("{:e}", f);
972            match s.split_once('e') {
973                Some((m, e)) if !e.starts_with('-') => format!("{}e+{}", m, e),
974                _ => s,
975            }
976        }
977        FloatValue::Decimal { value: d, .. } => python_decimal_str(d),
978    }
979}
980
981/// Python truthiness of a kwargs value (`if spaced:` accepts any object).
982fn kw_truthy(v: &KwVal) -> bool {
983    match v {
984        KwVal::Bool(b) => *b,
985        KwVal::Int(i) => *i != 0,
986        KwVal::Str(s) => !s.is_empty(),
987        KwVal::List(l) => !l.is_empty(),
988        KwVal::None => false,
989    }
990}
991
992/// `decimal_word=` extraction. Python's `if decimal_word is not None:
993/// self.pointword = decimal_word` accepts anything, but a non-str only
994/// crashes where the pointword is actually consumed (the float grammar's
995/// `str + int` TypeError, `_insert_spaces`'s `len(int)` TypeError) — return
996/// NotImplemented for those so the Python original owns its exact crash.
997fn tr_decimal_word(kw: &Kwargs) -> Result<Option<&str>> {
998    match kw.get("decimal_word") {
999        None | Some(KwVal::None) => Ok(None),
1000        Some(KwVal::Str(s)) => Ok(Some(s.as_str())),
1001        Some(_) => Err(N2WError::Fallback("kwargs".into())),
1002    }
1003}
1004
1005/// Port of `Num2Word_TR._insert_spaces` (issue #64 part 2, upstream #486).
1006///
1007/// Greedy longest-first tokenizer over the solid output. The token set is
1008/// the *cardinal* tables + ZERO + the current pointword (i.e. the caller's
1009/// decimal_word) — NOT the negword: "eksi" survives only because its letters
1010/// fall through the unknown-char branch verbatim, and the next matched token
1011/// then gets a space in front ("eksibir" -> "eksi bir"). Python sorts with
1012/// `key=len` (code points, not bytes — hence `chars().count()`) over a set;
1013/// equal-length ties cannot collide (two distinct equal-length tokens cannot
1014/// both prefix the same suffix), so the set's arbitrary tie order is
1015/// irrelevant and duplicates need no dedup.
1016fn insert_spaces(text: &str, pointword: &str) -> String {
1017    let mut tokens: Vec<&str> = vec![
1018        // CARDINAL_TRIPLETS.values()
1019        "bin", "milyon", "milyar", "trilyon", "katrilyon", "kentilyon",
1020        // CARDINAL_HUNDRED
1021        "yüz",
1022        // CARDINAL_TENS.values()
1023        "on", "yirmi", "otuz", "kırk", "elli", "altmış", "yetmiş", "seksen",
1024        "doksan",
1025        // CARDINAL_ONES.values()
1026        "bir", "iki", "üç", "dört", "beş", "altı", "yedi", "sekiz", "dokuz",
1027        // ZERO
1028        "sıfır",
1029    ];
1030    tokens.push(pointword);
1031    // `{t for t in tokens if t}` — drop empties (pointword may be "").
1032    tokens.retain(|t| !t.is_empty());
1033    tokens.sort_by_key(|t| std::cmp::Reverse(t.chars().count()));
1034
1035    let mut out: Vec<&str> = Vec::new();
1036    let mut i = 0usize;
1037    while i < text.len() {
1038        let ch = text[i..].chars().next().unwrap();
1039        if ch.is_whitespace() {
1040            // Python folds runs of whitespace into a single " " and never
1041            // emits one at the very start (`if out and out[-1] != " "`).
1042            if matches!(out.last(), Some(last) if *last != " ") {
1043                out.push(" ");
1044            }
1045            i += ch.len_utf8();
1046            continue;
1047        }
1048        if let Some(tok) = tokens.iter().copied().find(|t| text[i..].starts_with(*t)) {
1049            // A space goes before every matched token — including right
1050            // after an unknown char, which is how "eksi" gets its space.
1051            if matches!(out.last(), Some(last) if *last != " ") {
1052                out.push(" ");
1053            }
1054            out.push(tok);
1055            i += tok.len();
1056        } else {
1057            // Unknown char: pass through verbatim, no space of its own.
1058            out.push(&text[i..i + ch.len_utf8()]);
1059            i += ch.len_utf8();
1060        }
1061    }
1062    out.concat().trim().to_string()
1063}
1064
1065impl Lang for LangTr {
1066    /// This language's own `to_currency(currency=...)` default,
1067    /// read from the live Python signature. Only 44 of 156 use EUR.
1068    fn default_currency(&self) -> &str {
1069        "EUR"
1070    }
1071
1072    /// This language's own `to_currency(separator=...)` default,
1073    /// read from the live Python signature. Base's is ",", but only
1074    /// 36 of 149 languages actually use it — most default to " " or a
1075    /// conjunction, so inheriting Base's comma silently corrupts them.
1076    fn default_separator(&self) -> &str {
1077        ","
1078    }
1079
1080    fn maxval(&self) -> &BigInt {
1081        &self.maxval
1082    }
1083
1084    fn negword(&self) -> &str {
1085        // Python's negword carries no trailing space; it is concatenated
1086        // directly ("eksi" + "bir" = "eksibir").
1087        NEGWORD
1088    }
1089
1090    fn pointword(&self) -> &str {
1091        "virgül"
1092    }
1093
1094    fn to_cardinal(&self, value: &BigInt) -> Result<String> {
1095        self.to_cardinal_impl(value)
1096    }
1097
1098    fn to_ordinal(&self, value: &BigInt) -> Result<String> {
1099        self.to_ordinal_impl(value)
1100    }
1101
1102    fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
1103        self.verify_ordinal(value)?;
1104        let cardinal = self.to_cardinal_impl(value)?;
1105        let ordinal = self.to_ordinal_impl(value)?;
1106        // Recover the vowel-harmonised suffix by stripping the cardinal off the
1107        // ordinal. `starts_with` guarantees a char boundary, so the byte slice
1108        // is safe despite the non-ASCII words.
1109        let suffix: String = if ordinal.starts_with(&cardinal) {
1110            ordinal[cardinal.len()..].to_string()
1111        } else {
1112            // Python's defensive fallback: `ordinal[-4:]` — last four *chars*.
1113            let chars: Vec<char> = ordinal.chars().collect();
1114            chars[chars.len().saturating_sub(4)..].iter().collect()
1115        };
1116        Ok(format!("{}'{}", value, suffix))
1117    }
1118
1119    /// `lang_TR.py` does not override `to_year`; `Num2Word_Base.to_year`
1120    /// delegates straight to `to_cardinal`.
1121    fn to_year(&self, value: &BigInt) -> Result<String> {
1122        self.to_cardinal(value)
1123    }
1124
1125    /// `Num2Word_TR.to_cardinal_float` — see [`LangTr::cardinal_float_impl`]
1126    /// for the full port; this entry uses the default pointword.
1127    fn to_cardinal_float(
1128        &self,
1129        value: &FloatValue,
1130        precision_override: Option<u32>,
1131    ) -> Result<String> {
1132        self.cardinal_float_impl(value, precision_override, self.pointword())
1133    }
1134
1135    // ---- float/Decimal entry routing -------------------------------------
1136
1137    /// `to_cardinal(float/Decimal)` full routing (`_to_cardinal_impl`):
1138    /// `verify_cardinal` first (its MAXVAL check is reachable by huge
1139    /// non-whole Decimals, which must raise OverflowError rather than read
1140    /// through the float grammar), then `int(value) == value` decides between
1141    /// the integer digit reader and the float grammar.
1142    fn cardinal_float_entry(
1143        &self,
1144        value: &FloatValue,
1145        precision_override: Option<u32>,
1146    ) -> Result<String> {
1147        if self.float_exceeds_maxval(value) {
1148            return Err(N2WError::Overflow(errmsg_toobig_repr(
1149                &float_value_repr(value),
1150                &self.maxval,
1151            )));
1152        }
1153        match value.as_whole_int() {
1154            Some(i) => self.to_cardinal_impl(&i),
1155            None => self.to_cardinal_float(value, precision_override),
1156        }
1157    }
1158
1159    /// `to_ordinal(float/Decimal)`: `verify_ordinal` raises TypeError for
1160    /// numeric negatives (-0.0 passes and reads "sıfırıncı") and
1161    /// OverflowError above MAXVAL; a non-integral value merely clears
1162    /// `isordinal`, so `to_ordinal(0.5)` returns the pristine `""`. Whole
1163    /// values read the *exact* integer digits — no float round-trip, unlike
1164    /// to_cardinal.
1165    fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
1166        self.verify_ordinal_float(value)?;
1167        match value.as_whole_int() {
1168            Some(i) => self.to_ordinal_impl(&i),
1169            None => Ok(String::new()),
1170        }
1171    }
1172
1173    /// `to_ordinal_num(float/Decimal)`: after `verify_ordinal`, the suffix
1174    /// is recovered by stripping `to_cardinal(value)` off
1175    /// `to_ordinal(value)`. For a non-integral value the ordinal is `""`
1176    /// (above), the strip fails, and the fallback `""[-4:]` is `""` — hence
1177    /// `to_ordinal_num(0.5)` == `"0.5'"`. And since `ORDINAL_TRIPLETS[6]`
1178    /// lacks its suffix, `to_ordinal_num(1e20)` is `"1e+20'"` (cardinal ==
1179    /// ordinal == "yüzkentilyon"). The cardinal is computed first, exactly
1180    /// as Python orders it — its float-cast KeyError band fires before the
1181    /// (immune) ordinal runs.
1182    fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
1183        self.verify_ordinal_float(value)?;
1184        let (cardinal, ordinal) = match value.as_whole_int() {
1185            Some(i) => {
1186                let c = self.to_cardinal_impl(&i)?;
1187                let o = self.to_ordinal_impl(&i)?;
1188                (c, o)
1189            }
1190            None => (self.to_cardinal_float(value, None)?, String::new()),
1191        };
1192        let suffix: String = if ordinal.starts_with(&cardinal) {
1193            ordinal[cardinal.len()..].to_string()
1194        } else {
1195            // Python's defensive fallback: `ordinal[-4:]` — last four chars.
1196            let chars: Vec<char> = ordinal.chars().collect();
1197            chars[chars.len().saturating_sub(4)..].iter().collect()
1198        };
1199        Ok(format!("{}'{}", repr_str, suffix))
1200    }
1201
1202    // `to_year(float/Decimal)` needs no override: the trait default routes
1203    // through `cardinal_float_entry`, exactly like `Num2Word_Base.to_year`
1204    // -> `to_cardinal`.
1205
1206    // ---- string inputs ----------------------------------------------------
1207
1208    /// Base's `str_to_number` is plain `Decimal(value)`; TR then treats a
1209    /// NaN mode-dependently (decimal.InvalidOperation from the MAXVAL
1210    /// compare in `verify_cardinal`, TypeError(errmsg_nonnum) from
1211    /// `verify_ordinal`'s int() catch). The binding's generic
1212    /// `ParsedNumber::NaN` mapping (int()-ValueError) matches neither, so
1213    /// return NotImplemented: the shim reruns the original Python string
1214    /// pipeline, which owns every mode's exact outcome. Infinity stays on
1215    /// the generic mapping: OverflowError, the same type TR raises via the
1216    /// MAXVAL compare.
1217    fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
1218        // NaN is carried through natively now and split per mode in
1219        // `nan_result`; Infinity keeps riding the generic mapping
1220        // (`inf_result` default → OverflowError, the type TR raises via the
1221        // MAXVAL compare).
1222        python_decimal_parse(s)
1223    }
1224
1225    /// `Decimal('NaN')` per mode, reproducing TR's own crash sites:
1226    ///
1227    /// * `to_cardinal` / `to_year` (== cardinal) run `verify_cardinal`, whose
1228    ///   `abs(value) >= self.MAXVAL` compares `Decimal('NaN')` against an int
1229    ///   and raises `decimal.InvalidOperation` — not the int()-ValueError the
1230    ///   binding's generic NaN mapping assumes.
1231    /// * `to_ordinal` / `to_ordinal_num` run `verify_ordinal`, whose
1232    ///   `int(Decimal('NaN'))` raises `ValueError`, caught by its own
1233    ///   `except (ValueError, TypeError)` and re-raised as
1234    ///   `TypeError(errmsg_nonnum)`.
1235    fn nan_result(&self, to: &str) -> Result<String> {
1236        match to {
1237            "cardinal" | "year" => Err(N2WError::Custom {
1238                module: "decimal",
1239                class: "InvalidOperation",
1240                msg: "[<class 'decimal.InvalidOperation'>]".into(),
1241            }),
1242            _ => Err(N2WError::Type(ERRMSG_NONNUM.to_string())),
1243        }
1244    }
1245
1246    // ---- grammatical kwargs ------------------------------------------------
1247    //
1248    // Python signature: `to_cardinal(value, spaced=False, precision=None,
1249    // decimal_word=None)`. `precision=` never reaches these hooks — the
1250    // dispatcher keeps it on the Python side for the int and string entries
1251    // (`"precision" not in kwargs` guards both) and strips it into
1252    // `precision_override` for the float entry — so it is deliberately
1253    // absent from the `only()` lists: were it ever to arrive, the
1254    // NotImplemented fallback to Python is exact by construction.
1255    // `to_ordinal`/`to_ordinal_num`/`to_currency` accept no extra kwargs;
1256    // their trait defaults fall back to Python's own TypeError.
1257
1258    /// `to_cardinal(int, spaced=, decimal_word=)`. The integer reader never
1259    /// consults the pointword, so `decimal_word` only matters through
1260    /// `_insert_spaces`'s token set when `spaced` is truthy.
1261    fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1262        if !kw.only(&["spaced", "decimal_word"]) {
1263            return Err(N2WError::Fallback("kwargs".into()));
1264        }
1265        let decimal_word = tr_decimal_word(kw)?;
1266        let wrd = self.to_cardinal_impl(value)?;
1267        if kw.get("spaced").map(kw_truthy).unwrap_or(false) {
1268            Ok(insert_spaces(&wrd, decimal_word.unwrap_or(self.pointword())))
1269        } else {
1270            Ok(wrd)
1271        }
1272    }
1273
1274    /// `to_cardinal(float/Decimal, spaced=, decimal_word=)` with
1275    /// `precision=` already extracted into `precision_override` by the
1276    /// dispatcher. Full routing like [`Lang::cardinal_float_entry`], then
1277    /// `_insert_spaces` while the pointword is still the caller's
1278    /// decimal_word, so the custom word is recognized as its own token.
1279    fn to_cardinal_float_kw(
1280        &self,
1281        value: &FloatValue,
1282        precision_override: Option<u32>,
1283        kw: &Kwargs,
1284    ) -> Result<String> {
1285        if !kw.only(&["spaced", "decimal_word", "precision"]) {
1286            return Err(N2WError::Fallback("kwargs".into()));
1287        }
1288        // `precision=` normally arrives already extracted into
1289        // `precision_override` (the real dispatcher strips it before the Rust
1290        // call), but it can also ride in the kwargs bag; honour either,
1291        // preferring the bag. Python does `converter.precision =
1292        // int(precision_override)`, so a non-int is deferred to Python.
1293        let precision_override = match kw.get("precision") {
1294            None | Some(KwVal::None) => precision_override,
1295            Some(KwVal::Int(p)) if *p >= 0 => Some(*p as u32),
1296            Some(KwVal::Bool(b)) => Some(u32::from(*b)),
1297            Some(_) => return Err(N2WError::Fallback("kwargs".into())),
1298        };
1299        let pointword = tr_decimal_word(kw)?.unwrap_or(self.pointword());
1300        if self.float_exceeds_maxval(value) {
1301            return Err(N2WError::Overflow(errmsg_toobig_repr(
1302                &float_value_repr(value),
1303                &self.maxval,
1304            )));
1305        }
1306        let wrd = match value.as_whole_int() {
1307            Some(i) => self.to_cardinal_impl(&i)?,
1308            None => self.cardinal_float_impl(value, precision_override, pointword)?,
1309        };
1310        if kw.get("spaced").map(kw_truthy).unwrap_or(false) {
1311            Ok(insert_spaces(&wrd, pointword))
1312        } else {
1313            Ok(wrd)
1314        }
1315    }
1316
1317    /// `Num2Word_Base.to_year(value, **kwargs)`: every kwarg is swallowed
1318    /// unread and the result is plain `to_cardinal(value)` — `spaced=True`
1319    /// on to_year does NOT space the output.
1320    fn to_year_kw(&self, value: &BigInt, _kw: &Kwargs) -> Result<String> {
1321        self.to_cardinal(value)
1322    }
1323
1324    // ---- currency -------------------------------------------------------
1325    //
1326    // `Num2Word_TR` overrides only `pluralize` and the int fast-path of
1327    // `to_currency`. `to_cheque`, `_money_verbose`, `_cents_verbose` and
1328    // `_cents_terse` are inherited from `Num2Word_Base` unchanged, and
1329    // `CURRENCY_ADJECTIVES` / `CURRENCY_PRECISION` are the (empty) base class
1330    // dicts — confirmed empty at runtime, so `currency_adjective` (None) and
1331    // `currency_precision` (100 for every code, including KWD/BHD/JPY) stay at
1332    // their trait defaults. The default `money_verbose`/`cents_verbose` call
1333    // back through `to_cardinal`, which is TR's own digit-string reader, so
1334    // they need no override either.
1335
1336    fn lang_name(&self) -> &str {
1337        "Num2Word_TR"
1338    }
1339
1340    fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
1341        self.currency_forms.get(code)
1342    }
1343
1344    /// `Num2Word_TR.pluralize`: `forms[0] if number == 1 else forms[0]`.
1345    ///
1346    /// Both arms of Python's ternary are `forms[0]`, so `number` is unused —
1347    /// Turkish keeps one form for every count ("bir lira", "yüz lira").
1348    fn pluralize(&self, _n: &BigInt, forms: &[String]) -> Result<String> {
1349        if forms.len() >= 2 {
1350            return Ok(forms[0].clone());
1351        }
1352        // Python guards with `isinstance(forms, tuple) and len(forms) >= 2`
1353        // and otherwise `return forms` — handing the *tuple itself* back to a
1354        // caller that renders it with %s, i.e. its repr. Unreachable for TR:
1355        // all three CURRENCY_FORMS entries are 2-tuples, and CURRENCY_ADJECTIVES
1356        // is empty so prefix_currency (which would swap in a list) never runs.
1357        // Reproduced rather than turned into an exception Python never raises.
1358        Ok(format!(
1359            "({})",
1360            forms.iter().map(|f| format!("'{}',", f)).collect::<String>()
1361        ))
1362    }
1363
1364    /// `Num2Word_TR.to_currency`.
1365    ///
1366    /// The int arm is TR's own and deliberately unlike everything else: it
1367    /// drops `currency`, `cents`, `separator` and `adjective` on the floor,
1368    /// never looks at `CURRENCY_FORMS` (so `to_currency(7, "XXX")` is
1369    /// "yedilira", not a NotImplementedError), and glues the parts together
1370    /// with no spaces. Everything non-int defers to `Num2Word_Base`.
1371    fn to_currency(
1372        &self,
1373        val: &CurrencyValue,
1374        currency: &str,
1375        cents: bool,
1376        separator: Option<&str>,
1377        adjective: bool,
1378    ) -> Result<String> {
1379        if let CurrencyValue::Int(v) = val {
1380            // Python: `self.negword if val < 0 else ""` — the bare negword,
1381            // *not* Base's `"%s " % negword.strip()`. Hence "eksibeşlira".
1382            let minus_str = if v.is_negative() { NEGWORD } else { "" };
1383            let money_str = self.to_cardinal_impl(&v.abs())?;
1384            return Ok(format!("{}{}{}", minus_str, money_str, CURRENCY_UNIT));
1385        }
1386        crate::currency::default_to_currency(
1387            self,
1388            val,
1389            currency,
1390            cents,
1391            separator.unwrap_or(self.default_separator()),
1392            adjective,
1393        )
1394    }
1395}
1396
1397#[cfg(test)]
1398mod float_tests {
1399    use super::*;
1400    use bigdecimal::BigDecimal;
1401    use std::str::FromStr;
1402
1403    fn frac_prec(f: f64) -> u32 {
1404        let s = format!("{}", f);
1405        match s.split_once('.') {
1406            Some((_, fr)) if !fr.contains('e') => fr.len() as u32,
1407            _ => 0,
1408        }
1409    }
1410    fn cf(f: f64) -> String {
1411        let v = FloatValue::Float { value: f, precision: frac_prec(f) };
1412        LangTr::new().to_cardinal_float(&v, None).unwrap()
1413    }
1414    fn cd(s: &str) -> String {
1415        let bd = BigDecimal::from_str(s).unwrap();
1416        let prec = s.split_once('.').map(|(_, fr)| fr.len() as u32).unwrap_or(0);
1417        let v = FloatValue::Decimal { value: bd, precision: prec };
1418        LangTr::new().to_cardinal_float(&v, None).unwrap()
1419    }
1420
1421    #[test]
1422    fn corpus_float() {
1423        assert_eq!(cf(0.5), "sıfırvirgülbeş");
1424        assert_eq!(cf(1.5), "birvirgülbeş");
1425        assert_eq!(cf(2.25), "ikivirgülyirmibeş");
1426        assert_eq!(cf(3.14), "üçvirgülondört");
1427        assert_eq!(cf(0.01), "sıfırvirgülsıfırbir");
1428        assert_eq!(cf(0.1), "sıfırvirgülbir");
1429        assert_eq!(cf(0.99), "sıfırvirgüldoksandokuz");
1430        assert_eq!(cf(1.01), "birvirgülsıfırbir");
1431        assert_eq!(cf(12.34), "onikivirgülotuzdört");
1432        assert_eq!(cf(99.99), "doksandokuzvirgüldoksandokuz");
1433        assert_eq!(cf(100.5), "yüzvirgülbeş");
1434        assert_eq!(cf(1234.56), "binikiyüzotuzdörtvirgülellialtı");
1435        assert_eq!(cf(-0.5), "eksi sıfırvirgülbeş");
1436        assert_eq!(cf(-1.5), "eksi birvirgülbeş");
1437        assert_eq!(cf(-12.34), "eksi onikivirgülotuzdört");
1438        assert_eq!(cf(1.005), "birvirgül");
1439        assert_eq!(cf(2.675), "ikivirgülaltmışyedi");
1440    }
1441
1442    #[test]
1443    fn corpus_decimal() {
1444        assert_eq!(cd("0.01"), "sıfırvirgülsıfırbir");
1445        assert_eq!(cd("1.10"), "birvirgülon");
1446        assert_eq!(cd("12.345"), "onikivirgülotuzdört");
1447        assert_eq!(cd("98746251323029.99"),
1448            "doksansekiztrilyonyediyüzkırkaltımilyarikiyüzellibirmilyonüçyüzyirmiüçbinyirmidokuzvirgüldoksandokuz");
1449        assert_eq!(cd("0.001"), "sıfırvirgül");
1450        // Half-to-even ties on the *exact* decimal (unlike the f64 arm):
1451        // 2.675 -> 268 (even), not 267 like the float; 2.5 -> prec 1.
1452        assert_eq!(cd("2.675"), "ikivirgülaltmışsekiz");
1453        assert_eq!(cd("2.5"), "ikivirgülbeş");
1454    }
1455}
1456
1457#[cfg(test)]
1458mod entry_and_kwargs_tests {
1459    use super::*;
1460    use bigdecimal::BigDecimal;
1461    use std::str::FromStr;
1462
1463    fn fl(f: f64) -> FloatValue {
1464        let s = format!("{}", f);
1465        let prec = match s.split_once('.') {
1466            Some((_, fr)) if !fr.contains('e') => fr.len() as u32,
1467            _ => 0,
1468        };
1469        FloatValue::Float { value: f, precision: prec }
1470    }
1471    fn dc(s: &str) -> FloatValue {
1472        let bd = BigDecimal::from_str(s).unwrap();
1473        let prec = s.split_once('.').map(|(_, fr)| fr.len() as u32).unwrap_or(0);
1474        FloatValue::Decimal { value: bd, precision: prec }
1475    }
1476    fn kw(items: &[(&str, KwVal)]) -> Kwargs {
1477        Kwargs(items.iter().map(|(k, v)| (k.to_string(), v.clone())).collect())
1478    }
1479    fn l() -> LangTr {
1480        LangTr::new()
1481    }
1482
1483    #[test]
1484    fn ordinal_float_corpus() {
1485        assert_eq!(l().ordinal_float_entry(&fl(0.0)).unwrap(), "sıfırıncı");
1486        assert_eq!(l().ordinal_float_entry(&fl(-0.0)).unwrap(), "sıfırıncı");
1487        assert_eq!(l().ordinal_float_entry(&fl(1.0)).unwrap(), "birinci");
1488        assert_eq!(l().ordinal_float_entry(&fl(21.0)).unwrap(), "yirmibirinci");
1489        assert_eq!(
1490            l().ordinal_float_entry(&fl(1234.0)).unwrap(),
1491            "binikiyüzotuzdördüncü"
1492        );
1493        assert_eq!(l().ordinal_float_entry(&fl(1e16)).unwrap(), "onkatrilyonuncu");
1494        // ORDINAL_TRIPLETS[6] lacks its suffix: cardinal == ordinal.
1495        assert_eq!(l().ordinal_float_entry(&fl(1e20)).unwrap(), "yüzkentilyon");
1496        // Non-integral -> pristine "".
1497        assert_eq!(l().ordinal_float_entry(&fl(0.5)).unwrap(), "");
1498        assert_eq!(l().ordinal_float_entry(&fl(3.25)).unwrap(), "");
1499        // Numeric negatives -> TypeError(errmsg_nonnum).
1500        assert!(matches!(
1501            l().ordinal_float_entry(&fl(-2.0)),
1502            Err(N2WError::Type(m)) if m == ERRMSG_NONNUM
1503        ));
1504        assert!(matches!(
1505            l().ordinal_float_entry(&fl(-1.5)),
1506            Err(N2WError::Type(_))
1507        ));
1508        assert!(matches!(
1509            l().ordinal_float_entry(&dc("-3.0")),
1510            Err(N2WError::Type(_))
1511        ));
1512        // Decimal arm.
1513        assert_eq!(l().ordinal_float_entry(&dc("5.00")).unwrap(), "beşinci");
1514        assert_eq!(l().ordinal_float_entry(&dc("1E+2")).unwrap(), "yüzüncü");
1515        assert_eq!(
1516            l().ordinal_float_entry(&dc("12345.000")).unwrap(),
1517            "onikibinüçyüzkırkbeşinci"
1518        );
1519        assert_eq!(l().ordinal_float_entry(&dc("1E+20")).unwrap(), "yüzkentilyon");
1520        // MAXVAL applies to non-integral values too (OverflowError, not "").
1521        assert!(matches!(
1522            l().ordinal_float_entry(&dc("1000000000000000000000.5")),
1523            Err(N2WError::Overflow(_))
1524        ));
1525        assert!(matches!(
1526            l().ordinal_float_entry(&fl(1e21)),
1527            Err(N2WError::Overflow(_))
1528        ));
1529    }
1530
1531    #[test]
1532    fn ordinal_num_float_corpus() {
1533        let t = |v: FloatValue, r: &str| l().ordinal_num_float_entry(&v, r).unwrap();
1534        assert_eq!(t(fl(0.0), "0.0"), "0.0'ıncı");
1535        assert_eq!(t(fl(-0.0), "-0.0"), "-0.0'ıncı");
1536        assert_eq!(t(fl(1.0), "1.0"), "1.0'inci");
1537        assert_eq!(t(fl(2.0), "2.0"), "2.0'nci");
1538        assert_eq!(t(fl(3.0), "3.0"), "3.0'üncü");
1539        assert_eq!(t(fl(10.0), "10.0"), "10.0'uncu");
1540        assert_eq!(t(fl(1234.0), "1234.0"), "1234.0'üncü");
1541        assert_eq!(t(fl(1e16), "1e+16"), "1e+16'uncu");
1542        // Empty suffix: ORDINAL_TRIPLETS[6] quirk.
1543        assert_eq!(t(fl(1e20), "1e+20"), "1e+20'");
1544        // Non-integral: ordinal "" -> ""[-4:] -> "".
1545        assert_eq!(t(fl(0.5), "0.5"), "0.5'");
1546        assert_eq!(t(fl(3.25), "3.25"), "3.25'");
1547        assert_eq!(t(dc("5.00"), "5.00"), "5.00'inci");
1548        assert_eq!(t(dc("1E+2"), "1E+2"), "1E+2'üncü");
1549        assert_eq!(t(dc("1E+20"), "1E+20"), "1E+20'");
1550        assert!(matches!(
1551            l().ordinal_num_float_entry(&fl(-1.5), "-1.5"),
1552            Err(N2WError::Type(_))
1553        ));
1554        assert!(matches!(
1555            l().ordinal_num_float_entry(&fl(-1000000.0), "-1000000.0"),
1556            Err(N2WError::Type(_))
1557        ));
1558    }
1559
1560    #[test]
1561    fn cardinal_float_entry_maxval() {
1562        // Huge non-whole Decimal: OverflowError from verify_cardinal, not a
1563        // float-grammar reading.
1564        assert!(matches!(
1565            l().cardinal_float_entry(&dc("1000000000000000000000.5"), None),
1566            Err(N2WError::Overflow(_))
1567        ));
1568        // Whole values still route through the integer reader.
1569        assert_eq!(l().cardinal_float_entry(&fl(1e20), None).unwrap(), "yüzkentilyon");
1570        assert_eq!(l().cardinal_float_entry(&fl(-0.0), None).unwrap(), "sıfır");
1571        // precision= rides precision_override (corpus: {"precision": 3}).
1572        assert_eq!(
1573            l().cardinal_float_entry(&fl(1.5), Some(3)).unwrap(),
1574            "birvirgülbeşyüz"
1575        );
1576    }
1577
1578    #[test]
1579    fn str_nan_falls_back() {
1580        // NaN is now carried through natively (split per mode in `nan_result`),
1581        // so str_to_number succeeds rather than declining.
1582        assert!(l().str_to_number("NaN").is_ok());
1583        // Everything else stays on the generic parse.
1584        assert!(matches!(
1585            l().str_to_number("Infinity"),
1586            Ok(ParsedNumber::Inf { negative: false })
1587        ));
1588        assert!(matches!(l().str_to_number("5"), Ok(ParsedNumber::Dec(_))));
1589    }
1590
1591    #[test]
1592    fn kwargs_corpus() {
1593        let sp = kw(&[("spaced", KwVal::Bool(true))]);
1594        let unsp = kw(&[("spaced", KwVal::Bool(false))]);
1595        let c = |n: i64, k: &Kwargs| l().to_cardinal_kw(&BigInt::from(n), k).unwrap();
1596        assert_eq!(c(0, &sp), "sıfır");
1597        assert_eq!(c(11, &sp), "on bir");
1598        assert_eq!(c(21, &sp), "yirmi bir");
1599        assert_eq!(c(100, &sp), "yüz");
1600        assert_eq!(c(1234, &sp), "bin iki yüz otuz dört");
1601        // The negword is not a token: "eksi" survives via the unknown-char
1602        // branch, and the next token gets the space.
1603        assert_eq!(c(-5, &sp), "eksi beş");
1604        assert_eq!(c(1234, &unsp), "binikiyüzotuzdört");
1605        assert_eq!(c(-5, &unsp), "eksibeş");
1606
1607        let nokta = kw(&[("decimal_word", KwVal::Str("nokta".into()))]);
1608        let f = |v: FloatValue, k: &Kwargs| l().to_cardinal_float_kw(&v, None, k).unwrap();
1609        assert_eq!(f(fl(1.5), &nokta), "birnoktabeş");
1610        assert_eq!(f(fl(2.05), &nokta), "ikinoktasıfırbeş");
1611        assert_eq!(f(fl(1.5), &sp), "bir virgül beş");
1612        let both = kw(&[
1613            ("spaced", KwVal::Bool(true)),
1614            ("decimal_word", KwVal::Str("nokta".into())),
1615        ]);
1616        assert_eq!(f(fl(3.14), &both), "üç nokta on dört");
1617        // Whole float through the kw entry keeps the int reading.
1618        assert_eq!(f(fl(-5.0), &sp), "eksi beş");
1619        // Unknown kwarg -> Fallback (decline) -> Python raises its own TypeError.
1620        assert!(matches!(
1621            l().to_cardinal_kw(&BigInt::from(5), &kw(&[("case", KwVal::Str("x".into()))])),
1622            Err(N2WError::Fallback(_))
1623        ));
1624        // to_year swallows every kwarg unread.
1625        assert_eq!(
1626            l().to_year_kw(&BigInt::from(5), &sp).unwrap(),
1627            "beş"
1628        );
1629    }
1630}