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}