num2words2_core/lang_sk.rs
1//! Port of `lang_SK.py` (Slovak). Registry key `"sk"` → `Num2Word_SK`
2//! (verified in `num2words2/__init__.py`: `"sk": lang_SK.Num2Word_SK()`).
3//!
4//! Shape: **self-contained**. `Num2Word_SK` subclasses `Num2Word_Base` but
5//! defines no `high_numwords`/`mid_numwords`/`low_numwords`, so Python never
6//! builds `self.cards` and never sets `MAXVAL`. `to_cardinal` is overridden
7//! outright and drives `_int2word` over 3-digit chunks. `cards`/`maxval`/
8//! `merge` therefore stay at their trait defaults, and there is **no overflow
9//! check**: the only ceiling is the `THOUSANDS` table, which raises `KeyError`
10//! rather than `OverflowError` (see bug 4 below).
11//!
12//! Inherited from `Num2Word_Base` (SK does not override either, so the trait
13//! defaults are exactly right):
14//! * `to_ordinal_num(value) -> value` → default `Ok(value.to_string())`.
15//! Corpus confirms the raw form, negatives included: `-1` → `"-1"`.
16//! * `to_year(value, **kwargs) -> self.to_cardinal(value)` → default
17//! delegates through `&self` and picks up the `to_cardinal` override
18//! below. Slovak has no era/century handling, so years are plain
19//! cardinals: `1999` → "tisíc deväťsto deväťdesiat deväť", `-44` →
20//! "mínus štyridsať štyri".
21//!
22//! `setup()` sets `negword = "mínus"` and `pointword = "celých"`.
23//!
24//! # Faithfully reproduced Python bugs
25//!
26//! This is a port, not a rewrite. Every item below is wrong-looking Slovak but
27//! is exactly what Python emits, confirmed against the frozen corpus:
28//!
29//! 1. **`to_ordinal` glues a bare "ý" onto the cardinal** for any number
30//! outside its 29-entry lookup table. The comment in Python calls this "a
31//! simplified implementation"; the results are not Slovak words:
32//! `to_ordinal(0)` == "nulaý", `to_ordinal(25)` == "dvadsať päťý",
33//! `to_ordinal(200)` == "dvestoý", `to_ordinal(10**6)` == "milióný",
34//! `to_ordinal(10**9)` == "miliardaý". The suffix is appended to the whole
35//! string, so a multi-word cardinal only inflects its *last* word.
36//! 2. **Negative ordinals produce "mínus …ý"** rather than raising. Unlike
37//! `lang_PL`, SK's `to_cardinal` strips the sign *before* the digits reach
38//! `splitbyx`/`get_digits`, so no `ValueError` is ever triggered:
39//! `to_ordinal(-1)` == "mínus jedený". Base's `verify_ordinal` (which would
40//! reject negatives with a `TypeError`) is never called.
41//! 3. **Thousands are written detached and mis-pluralized.** `_int2word`
42//! joins chunk words with spaces, so 2000 == "dve tisíc" (idiomatic Slovak
43//! is "dvetisíc"), and `pluralize` keys off the whole 3-digit chunk rather
44//! than its last digit, so 123 selects form 2: `123456789` ==
45//! "sto dvadsať tri **miliónov** …" where Slovak wants "milióny".
46//! 4. **`THOUSANDS` stops at key 10 (10^30).** A chunk index of 11 or more —
47//! i.e. any value >= 10^33 — is a `KeyError`, which is Slovak's de facto
48//! (and rather abrupt) MAXVAL. Modelled by [`LangSk::thousands_at`].
49//! 5. **Typo in `THOUSANDS[10]`**: the plural form is "kvintillióny" with a
50//! doubled `l`, while the singular "kvintilión" and genitive
51//! "kvintiliónov" both have one. Kept verbatim.
52//! 6. **`to_currency`'s int path mis-pluralizes every count except 1.** It
53//! picks `cr1[0]`/`cr1[1]` by hand instead of calling `self.pluralize`, so
54//! 0, 5+ and the teens all take the paucal (2-4) form where `pluralize`
55//! would pick the genitive `cr1[2]`. See [`LangSk::to_currency`].
56//! 7. **`to_currency` has no gender agreement.** The count comes from
57//! `to_cardinal`, which only ever emits the masculine "jeden"/"dva", so a
58//! feminine unit reads wrong: `1 CZK` is "jeden koruna" (Slovak wants
59//! "jedna koruna") and `2 CZK` is "dva koruny" (wants "dve koruny").
60//! `_int2word`'s feminine forms are keyed off the `THOUSANDS` scale index
61//! only (see [`ONES`]) and never off the currency, so nothing here can
62//! reach them.
63//!
64//! # The currency surface
65//!
66//! `Num2Word_SK` declares its own `CURRENCY_FORMS` in the class body and
67//! subclasses `Num2Word_Base`, **not** `Num2Word_EUR` — so the shared-class-dict
68//! mutation that `Num2Word_EN.__init__` performs on `Num2Word_EUR.CURRENCY_FORMS`
69//! (see PORTING_CURRENCY.md) cannot reach it: Base's own `CURRENCY_FORMS` is a
70//! different, empty dict, and SK's class attribute shadows it outright. Here the
71//! literal source *is* what runs. Verified against the live interpreter — SK
72//! sees exactly three codes (CZK/EUR/USD) and nothing else, so every other code
73//! raises `NotImplementedError`, the corpus-covered
74//! GBP/JPY/KWD/BHD/INR/CNY/CHF included.
75//!
76//! `CURRENCY_ADJECTIVES` and `CURRENCY_PRECISION` are Base's empty dicts (SK
77//! adds neither), which settles several hooks by inheritance:
78//! * `currency_adjective` — always absent, so `adjective=True` never prefixes
79//! anything on either path. The trait default `None` is correct.
80//! * `currency_precision` — `.get(code, 100)` is 100 for every code, so the
81//! trait default 100 is correct. **SK has no 3-decimal and no 0-decimal
82//! currency**: `default_to_currency`'s `divisor == 1` (JPY) and
83//! `divisor == 1000` (KWD/BHD) branches are unreachable, because those
84//! codes are absent from `CURRENCY_FORMS` and raise first.
85//! * `money_verbose` / `cents_verbose` — both are `self.to_cardinal(n)`, which
86//! the defaults already do through `&self`, so they pick up SK's
87//! `to_cardinal` override.
88//! * `cents_terse` — Base's zero-padded `"%0*d"`, mirrored by
89//! `currency::default_cents_terse`. `cents=False` at 12.05 USD gives
90//! "dvanásť dolárov, 05 centov".
91//! * `to_cheque` — Base's, unoverridden. It reads `cr1[-1]` (the genitive
92//! plural), calls `_money_verbose` for the whole part and upper-cases the
93//! lot: `cheque:USD` of 1234.56 → "TISÍC DVESTO TRIDSAŤ ŠTYRI AND 56/100
94//! DOLÁROV". `currency::default_to_cheque` matches it field for field.
95//! * `cardinal_from_decimal` — left at the default per PORTING_CURRENCY.md
96//! ("the float/Decimal cardinal path is a later phase"). This is the one
97//! known gap, and unlike most languages SK's is **not** benign:
98//!
99//! It backs `default_to_currency`'s fractional-cents branch, which Python
100//! reaches as `self.to_cardinal(float(right))` — i.e. through SK's
101//! `to_cardinal` *decimal* branch, not through `Num2Word_Base
102//! .to_cardinal_float`, which is what the default routes to. The two
103//! disagree whenever the fractional remainder has more than one digit,
104//! because SK reads it as a **whole number** while Base spells it
105//! **digit by digit**: `to_currency(3.14159, "USD")` is
106//! "tri doláre, štrnásť celých **sto päťdesiat deväť** centy" in Python but
107//! would be "...štrnásť celých **jeden päť deväť** centy" through the
108//! default. Verified against the live interpreter. No corpus row reaches
109//! it (every `arg` has at most two decimals, so `right` is always whole).
110//! Closing it means porting SK's `to_cardinal` decimal branch — the later
111//! float phase — not patching this hook.
112//!
113//! # Error variants
114//!
115//! `KeyError` (bug 4) is reachable via [`key_error`]. It is a Python crash
116//! rather than a deliberate raise, but the exception *type* is observable and
117//! callers may catch it, so parity means reproducing it rather than tidying it
118//! into an `OverflowError`. [`value_error`] models `int()` on a non-numeric
119//! token: on the integer path the sign is stripped before the digit helpers, so
120//! it never fires there, but the float path ([`LangSk::to_cardinal_float`])
121//! *does* reach it — a repr/str with no "." (scientific notation, `inf`, `nan`)
122//! or a fractional token carrying an `'e'` feeds `int()` a non-decimal token,
123//! exactly as Python's `to_cardinal` does.
124//!
125//! The currency surface adds two more, both routed through `&self` so they keep
126//! SK's semantics:
127//! * `NotImplementedError` for an unknown code, raised by
128//! `currency::default_to_currency`/`default_to_cheque` off
129//! [`Lang::currency_forms`] returning `None`. Message verified byte for
130//! byte: `Currency code "GBP" not implemented for "Num2Word_SK"`.
131//! * `KeyError` again — bug 4 is reachable *through* `to_currency`, since the
132//! int path calls `to_cardinal`: `to_currency(10**33, "EUR")` is
133//! `KeyError: 11` in Python, not an OverflowError. `to_currency(10**30)` is
134//! fine ("kvintilión eurá").
135
136use crate::base::{Lang, N2WError, Result};
137use crate::currency::{CurrencyForms, CurrencyValue};
138use crate::floatpath::FloatValue;
139use crate::strnum::{python_decimal_parse, ParsedNumber};
140use bigdecimal::BigDecimal;
141use num_bigint::BigInt;
142use num_traits::{FromPrimitive, One, Signed, Zero};
143use std::collections::HashMap;
144
145const ZERO: &str = "nula";
146const NEGWORD: &str = "mínus";
147const POINTWORD: &str = "celých";
148
149/// `ONES`: keys 1..=9, each `(masculine, feminine, scales_using_feminine)`.
150///
151/// Only key 2 carries a non-empty set: `{1, 3, 5, 7, 9}` are the chunk indices
152/// whose `THOUSANDS` noun is grammatically feminine (tisíc, miliarda,
153/// biliarda, triliarda, kvadriliarda), where Slovak wants "dve" over "dva".
154/// Index 0 is absent in Python and unreachable (guarded by `n1 > 0`).
155const ONES: [(&str, &str, &[usize]); 10] = [
156 ("", "", &[]), // absent in Python
157 ("jeden", "jeden", &[]),
158 ("dva", "dve", &[1, 3, 5, 7, 9]),
159 ("tri", "tri", &[]),
160 ("štyri", "štyri", &[]),
161 ("päť", "päť", &[]),
162 ("šesť", "šesť", &[]),
163 ("sedem", "sedem", &[]),
164 ("osem", "osem", &[]),
165 ("deväť", "deväť", &[]),
166];
167
168/// `TENS`: keys 0..=9 — the 10..=19 teens, keyed by the *units* digit.
169const TENS: [&str; 10] = [
170 "desať",
171 "jedenásť",
172 "dvanásť",
173 "trinásť",
174 "štrnásť",
175 "pätnásť",
176 "šestnásť",
177 "sedemnásť",
178 "osemnásť",
179 "devätnásť",
180];
181
182/// `TWENTIES`: keys 2..=9. Indices 0/1 are absent in Python (guarded `n2 > 1`).
183const TWENTIES: [&str; 10] = [
184 "",
185 "",
186 "dvadsať",
187 "tridsať",
188 "štyridsať",
189 "päťdesiat",
190 "šesťdesiat",
191 "sedemdesiat",
192 "osemdesiat",
193 "deväťdesiat",
194];
195
196/// `HUNDREDS`: keys 1..=9. Index 0 is absent in Python (guarded `n3 > 0`).
197const HUNDREDS: [&str; 10] = [
198 "",
199 "sto",
200 "dvesto",
201 "tristo",
202 "štyristo",
203 "päťsto",
204 "šesťsto",
205 "sedemsto",
206 "osemsto",
207 "deväťsto",
208];
209
210/// `THOUSANDS`: chunk index → the three plural forms (1 / 2..4 / 5+).
211///
212/// Keys 1..=10, i.e. 1000^1 (10^3) through 1000^10 (10^30). Index 0 is a
213/// placeholder — Python has no key 0 and never looks one up (guarded `i > 0`).
214/// A chunk index of 11 or more is a `KeyError`; see bug 4 in the module docs.
215///
216/// Note `THOUSANDS[10].1` == "kvintillióny" — a doubled `l` that the singular
217/// and genitive forms lack. Python's typo, preserved (bug 5).
218const THOUSANDS: [[&str; 3]; 11] = [
219 ["", "", ""], // absent in Python
220 ["tisíc", "tisíc", "tisíc"], // 10^3
221 ["milión", "milióny", "miliónov"], // 10^6
222 ["miliarda", "miliardy", "miliárd"], // 10^9
223 ["bilión", "bilióny", "biliónov"], // 10^12
224 ["biliarda", "biliardy", "biliárd"], // 10^15
225 ["trilión", "trilióny", "triliónov"], // 10^18
226 ["triliarda", "triliardy", "triliárd"], // 10^21
227 ["kvadrilión", "kvadrilióny", "kvadriliónov"], // 10^24
228 ["kvadriliarda", "kvadriliardy", "kvadriliárd"], // 10^27
229 ["kvintilión", "kvintillióny", "kvintiliónov"], // 10^30 — sic
230];
231
232/// The `ordinals` dict local to `Num2Word_SK.to_ordinal`.
233///
234/// 29 entries: 1..=20, the round tens 30..=90, 100 and 1000. Everything else
235/// falls through to the "cardinal + ý" path (bug 1). Keys are `u32` because
236/// the table's own domain is tiny; the *input* stays `BigInt` and is compared
237/// by value, so a huge or negative argument simply misses every entry — which
238/// is exactly Python's `num in ordinals`.
239const ORDINALS: [(u32, &str); 29] = [
240 (1, "prvý"),
241 (2, "druhý"),
242 (3, "tretí"),
243 (4, "štvrtý"),
244 (5, "piaty"),
245 (6, "šiesty"),
246 (7, "siedmy"),
247 (8, "ôsmy"),
248 (9, "deviaty"),
249 (10, "desiaty"),
250 (11, "jedenásty"),
251 (12, "dvanásty"),
252 (13, "trinásty"),
253 (14, "štrnásty"),
254 (15, "pätnásty"),
255 (16, "šestnásty"),
256 (17, "sedemnásty"),
257 (18, "osemnásty"),
258 (19, "devätnásty"),
259 (20, "dvadsiaty"),
260 (30, "tridsiaty"),
261 (40, "štyridsiaty"),
262 (50, "päťdesiaty"),
263 (60, "šesťdesiaty"),
264 (70, "sedemdesiaty"),
265 (80, "osemdesiaty"),
266 (90, "deväťdesiaty"),
267 (100, "stý"),
268 (1000, "tisíci"),
269];
270
271// --- Python exception encoding -------------------------------------------
272//
273// Encode the Python exception name in the message so a later phase can remap
274// without re-deriving the semantics.
275
276/// Python `KeyError` — the missing `THOUSANDS` entry past 10^30 (bug 4).
277fn key_error(key: String) -> N2WError {
278 N2WError::Key(key)
279}
280
281/// Python `ValueError` from `int()` on a non-numeric token. On the integer
282/// path it is unreachable (the sign never reaches the digit helpers), but the
283/// float path reaches it: a repr/str with no "." (scientific notation, `inf`,
284/// `nan`) or a fractional token carrying an `'e'` feeds `int()` a non-decimal
285/// token, exactly as Python's `Num2Word_SK.to_cardinal` does. Also kept so
286/// [`splitbyx`]/[`get_digits`] mirror `utils.py` rather than assuming clean
287/// input.
288fn value_error(msg: String) -> N2WError {
289 N2WError::Value(msg)
290}
291
292/// Python's `int(s)` for the digit-string shapes this module produces.
293/// `int("024")` == 24; `int("-")` and `int("")` raise `ValueError`.
294fn parse_int(s: &str) -> Result<BigInt> {
295 BigInt::parse_bytes(s.as_bytes(), 10)
296 .ok_or_else(|| value_error(format!("invalid literal for int() with base 10: '{}'", s)))
297}
298
299/// Port of `utils.splitbyx(n, x)` with `format_int=True`.
300///
301/// Splits a digit string into chunks of `x` from the right, so the *first*
302/// chunk is the short one: `"2024"` → `[2, 24]`, `"1000000"` → `[1, 0, 0]`.
303/// Indexes by `chars()`, never bytes — the caller only ever passes ASCII
304/// digits here, but the shared helper's contract is character-based.
305fn splitbyx(n: &str, x: usize) -> Result<Vec<BigInt>> {
306 let chars: Vec<char> = n.chars().collect();
307 let length = chars.len();
308 let slice = |i: usize, j: usize| -> String { chars[i..j.min(length)].iter().collect() };
309
310 let mut out: Vec<BigInt> = Vec::new();
311 if length > x {
312 let start = length % x;
313 if start > 0 {
314 out.push(parse_int(&slice(0, start))?);
315 }
316 let mut i = start;
317 while i < length {
318 out.push(parse_int(&slice(i, i + x))?);
319 i += x;
320 }
321 } else {
322 out.push(parse_int(n)?);
323 }
324 Ok(out)
325}
326
327/// Python's `"%03d" % n`: field width 3 **including** any sign.
328/// `1` → `"001"`, `24` → `"024"`, `123` → `"123"`, `1234` → `"1234"`.
329fn fmt_03(n: &BigInt) -> String {
330 let mag = n.abs().to_string();
331 if n.is_negative() {
332 let pad = 3usize.saturating_sub(1 + mag.len());
333 format!("-{}{}", "0".repeat(pad), mag)
334 } else {
335 let pad = 3usize.saturating_sub(mag.len());
336 format!("{}{}", "0".repeat(pad), mag)
337 }
338}
339
340/// Port of `utils.get_digits(n)`:
341/// `[int(x) for x in reversed(list(("%03d" % n)[-3:]))]` → `[n1, n2, n3]`
342/// (units, tens, hundreds).
343///
344/// Callers only ever pass a chunk in 0..=999, so the `[-3:]` slice is the
345/// whole formatted string and no digit is ever dropped.
346fn get_digits(n: &BigInt) -> Result<[usize; 3]> {
347 let s = fmt_03(n);
348 let chars: Vec<char> = s.chars().collect();
349 // fmt_03 always yields >= 3 chars, so the [-3:] slice is total.
350 let tail = &chars[chars.len() - 3..];
351 let mut a = [0usize; 3];
352 for (k, c) in tail.iter().rev().enumerate() {
353 a[k] = c.to_digit(10).ok_or_else(|| {
354 value_error(format!("invalid literal for int() with base 10: '{}'", c))
355 })? as usize;
356 }
357 Ok(a)
358}
359
360/// The form index `Num2Word_SK.pluralize` selects:
361///
362/// ```python
363/// if n == 1: form = 0
364/// elif 0 < n < 5: form = 1
365/// else: form = 2
366/// ```
367///
368/// Unlike most Slavic siblings (`lang_CS` keys off `n % 10` / `n % 100`), SK
369/// tests the number *whole*, which is bug 3: a 3-digit chunk of 123 is not
370/// `< 5`, so it takes form 2 and 123 millions reads "miliónov" where Slovak
371/// wants "milióny".
372///
373/// `n` is non-negative on every path that reaches here — `_int2word` passes a
374/// chunk in 0..=999, and the currency path passes unit/subunit counts that
375/// `parse_currency_parts`/`abs()` have already made positive — so the
376/// `0 < n` guard only ever excludes `n == 0` (which takes form 2).
377///
378/// Note: [`LangSk::pluralize`] spells the same rule out inline against a fixed
379/// 3-tuple, the shape `_int2word` needs for `THOUSANDS[i]`; that method is
380/// verified against the integer corpus and is deliberately left untouched here.
381/// The two must stay in agreement.
382fn plural_form_index(n: &BigInt) -> usize {
383 if n.is_one() {
384 0
385 } else if n.is_positive() && n < &BigInt::from(5) {
386 1
387 } else {
388 2
389 }
390}
391
392/// `Num2Word_SK.CURRENCY_FORMS`, transcribed from the class body.
393///
394/// Three codes, no more: SK descends from `Num2Word_Base`, so it inherits none
395/// of the ~24 extra codes `Num2Word_EN.__init__` writes into
396/// `Num2Word_EUR.CURRENCY_FORMS`. Confirmed against the live interpreter.
397///
398/// All six tuples carry three forms — nominative singular / paucal (2-4) /
399/// genitive plural — and the arity is load-bearing: `pluralize` indexes 0/1/2,
400/// `to_cheque` takes `cr1[-1]`, and `to_currency`'s int path takes `cr1[1]`.
401fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
402 const CENTS: [&str; 3] = ["cent", "centy", "centov"];
403
404 let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
405 m.insert(
406 "EUR",
407 CurrencyForms::new(&["euro", "eurá", "eur"], &CENTS),
408 );
409 m.insert(
410 "CZK",
411 CurrencyForms::new(
412 &["koruna", "koruny", "korún"],
413 &["halier", "haliere", "halierov"],
414 ),
415 );
416 m.insert(
417 "USD",
418 CurrencyForms::new(&["dolár", "doláre", "dolárov"], &CENTS),
419 );
420 m
421}
422
423/// Reproduce CPython's `repr(float)` for a finite-or-not `f64`.
424///
425/// `Num2Word_SK.to_cardinal` starts with `n = str(number)` and then *string-
426/// splits* on `.` — it never calls `float2tuple`, so the f64-artefact heuristic
427/// in `floatpath` is irrelevant here. What matters is that `str(number)` for a
428/// Python float is byte-identical to `repr(number)` (shortest round-trip), and
429/// the SK algorithm reads the characters of that string. So the port must
430/// rebuild that exact string.
431///
432/// CPython uses David Gay's shortest `dtoa` (mode 0): a digit string plus a
433/// decimal-point position `decpt`. It prints fixed notation iff
434/// `-4 < decpt <= 16` and scientific otherwise, appending `.0` in fixed
435/// notation when nothing follows the point (`Py_DTSF_ADD_DOT_0`) and padding the
436/// scientific exponent to at least two digits (`1e-05`, `1e+16`). Rust's `{:e}`
437/// emits the same shortest digit string with `decpt == exp + 1`, so the two
438/// reconstruct identically. Mirrors `lang_cs.rs`'s `python_repr_f64`.
439fn python_repr_f64(f: f64) -> String {
440 if f.is_nan() {
441 // repr(float('nan')) == 'nan', sign dropped. Feeds the no-'.' branch,
442 // where int('nan') raises ValueError — reproduced downstream.
443 return "nan".to_string();
444 }
445 if f.is_infinite() {
446 return if f < 0.0 { "-inf" } else { "inf" }.to_string();
447 }
448
449 // `is_sign_negative` captures -0.0, whose repr is '-0.0'.
450 let neg = f.is_sign_negative();
451 let s = format!("{:e}", f.abs()); // e.g. "1.234e1", "5e-1", "0e0"
452 let (mantissa, exp_s) = match s.split_once('e') {
453 Some(parts) => parts,
454 None => return s, // unreachable for finite f64
455 };
456 let exp: i32 = exp_s.parse().unwrap_or(0);
457 let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
458 let ndigits = digits.len() as i32;
459 let decpt = exp + 1;
460
461 let body = if decpt <= -4 || decpt > 16 {
462 // Scientific: first digit, optional ".rest", then "e±NN".
463 let mut m = String::new();
464 m.push_str(&digits[..1]);
465 if ndigits > 1 {
466 m.push('.');
467 m.push_str(&digits[1..]);
468 }
469 let e = decpt - 1;
470 let (esign, eabs) = if e < 0 { ('-', -e) } else { ('+', e) };
471 format!("{}e{}{:02}", m, esign, eabs)
472 } else if decpt <= 0 {
473 // 0.00…digits
474 format!("0.{}{}", "0".repeat((-decpt) as usize), digits)
475 } else if decpt >= ndigits {
476 // digits, trailing zeros, then the ADD_DOT_0 ".0".
477 format!("{}{}.0", digits, "0".repeat((decpt - ndigits) as usize))
478 } else {
479 // digits[:decpt] "." digits[decpt:]
480 let dp = decpt as usize;
481 format!("{}.{}", &digits[..dp], &digits[dp..])
482 };
483
484 if neg {
485 format!("-{}", body)
486 } else {
487 body
488 }
489}
490
491/// Reproduce CPython's `str(Decimal)` for the value carried on
492/// `FloatValue::Decimal`.
493///
494/// `Num2Word_SK.to_cardinal` sees `n = str(number)` for a `Decimal` too, and
495/// again string-splits it. `str(Decimal)` is `_pydecimal.__str__`'s
496/// to-scientific-string: it keeps the coefficient's trailing zeros (they live
497/// in the exponent, not the digits), so `Decimal('1.10')` prints `"1.10"`, and
498/// it switches to `E`-notation exactly when `exp > 0` or the adjusted exponent
499/// `< -6`. `BigDecimal::as_bigint_and_exponent` gives `(coefficient, scale)`
500/// with `value == coefficient * 10^-scale`, i.e. Python's `_exp == -scale` and
501/// `_int == str(abs(coefficient))`, so the same algorithm reconstructs it.
502/// Preserving trailing zeros is load-bearing: `"1.10"` must stay `"1.10"` (SK
503/// reads the fraction as the whole number `10` → "desať"), never `"1.1"`.
504/// Mirrors `lang_cs.rs`'s `python_str_decimal`.
505///
506/// Default context is non-engineering with `capitals == 1`, hence upper-case
507/// `E` and an unpadded `%+d` exponent (`1E-7`, not `1e-07`).
508fn python_str_decimal(bd: &BigDecimal) -> String {
509 let (coeff, scale) = bd.as_bigint_and_exponent();
510 let py_exp: i64 = -scale; // Decimal._exp
511 let neg = coeff.is_negative(); // BigInt drops the sign of a negative zero
512 let int_str = coeff.abs().to_string(); // Decimal._int
513 let ndigits = int_str.len() as i64;
514 let leftdigits = py_exp + ndigits;
515
516 // dotplace, non-engineering branch of _pydecimal.__str__.
517 let dotplace: i64 = if py_exp <= 0 && leftdigits > -6 {
518 leftdigits
519 } else {
520 1
521 };
522
523 let (intpart, fracpart) = if dotplace <= 0 {
524 (
525 "0".to_string(),
526 format!(".{}{}", "0".repeat((-dotplace) as usize), int_str),
527 )
528 } else if dotplace >= ndigits {
529 (
530 format!("{}{}", int_str, "0".repeat((dotplace - ndigits) as usize)),
531 String::new(),
532 )
533 } else {
534 let dp = dotplace as usize;
535 (int_str[..dp].to_string(), format!(".{}", &int_str[dp..]))
536 };
537
538 let exp = if leftdigits == dotplace {
539 String::new()
540 } else {
541 // "%+d" — a sign but no zero-padding, unlike float repr.
542 format!("E{:+}", leftdigits - dotplace)
543 };
544
545 let sign = if neg { "-" } else { "" };
546 format!("{}{}{}{}", sign, intpart, fracpart, exp)
547}
548
549pub struct LangSk {
550 currency_forms: HashMap<&'static str, CurrencyForms>,
551}
552
553impl Default for LangSk {
554 fn default() -> Self {
555 Self::new()
556 }
557}
558
559impl LangSk {
560 pub fn new() -> Self {
561 LangSk {
562 // Built once here, never per call. `to_currency`/`to_cheque` only
563 // ever read this table, and rebuilding it on each call is what made
564 // an earlier revision of this port slower than the Python it
565 // replaces. `lib.rs` holds the instance in a `OnceLock`, so this
566 // runs at most once per process.
567 currency_forms: build_currency_forms(),
568 }
569 }
570
571 /// `THOUSANDS[i]`, raising `KeyError` past 10 — see bug 4.
572 fn thousands_at(&self, i: usize) -> Result<&'static [&'static str; 3]> {
573 if (1..=10).contains(&i) {
574 Ok(&THOUSANDS[i])
575 } else {
576 Err(key_error(i.to_string()))
577 }
578 }
579
580 /// Port of `Num2Word_SK.pluralize`.
581 ///
582 /// ```python
583 /// if n == 1: form = 0
584 /// elif 0 < n < 5: form = 1
585 /// else: form = 2
586 /// ```
587 /// `n` is the whole 3-digit chunk, not its last digit — which is why 123
588 /// millions comes out as "miliónov" (form 2) rather than "milióny"
589 /// (bug 3). `n` is never 0 here (`_int2word` skips zero chunks) and never
590 /// negative (`to_cardinal` strips the sign first).
591 fn pluralize(&self, n: &BigInt, forms: &[&'static str; 3]) -> &'static str {
592 let form = if n.is_one() {
593 0usize
594 } else if n.is_positive() && n < &BigInt::from(5) {
595 1
596 } else {
597 2
598 };
599 forms[form]
600 }
601
602 /// Port of `Num2Word_SK._int2word`. Only ever called with a non-negative
603 /// `n` — `to_cardinal` removes the sign before delegating here.
604 fn int2word(&self, n: &BigInt) -> Result<String> {
605 if n.is_zero() {
606 return Ok(ZERO.to_string());
607 }
608
609 let mut words: Vec<String> = Vec::new();
610 let chunks = splitbyx(&n.to_string(), 3)?;
611 let mut i = chunks.len();
612 for x in chunks.iter() {
613 i -= 1;
614
615 if x.is_zero() {
616 continue;
617 }
618
619 let [n1, n2, n3] = get_digits(x)?;
620
621 // Python builds a per-chunk list and joins it, then joins the
622 // chunk strings again with " ". Both joins use a single space, so
623 // the nesting is invisible in the output — but it is why an empty
624 // chunk would contribute an empty string rather than being
625 // skipped. Zero chunks `continue` above, so that never happens.
626 let mut word_chunk: Vec<&'static str> = Vec::new();
627
628 if n3 > 0 {
629 word_chunk.push(HUNDREDS[n3]);
630 }
631
632 if n2 > 1 {
633 word_chunk.push(TWENTIES[n2]);
634 }
635
636 if n2 == 1 {
637 word_chunk.push(TENS[n1]);
638 } else if n1 > 0 && !(i > 0 && x.is_one()) {
639 // A leading "jeden" is suppressed on scale chunks, so 1000 is
640 // "tisíc" and 10^6 is "milión" — not "jeden tisíc".
641 if n2 == 0 && n3 == 0 && ONES[n1].2.contains(&i) {
642 word_chunk.push(ONES[n1].1);
643 } else {
644 word_chunk.push(ONES[n1].0);
645 }
646 }
647
648 if i > 0 {
649 word_chunk.push(self.pluralize(x, self.thousands_at(i)?));
650 }
651
652 words.push(word_chunk.join(" "));
653 }
654
655 Ok(words.join(" "))
656 }
657}
658
659impl Lang for LangSk {
660
661 fn cardinal_float_entry(
662 &self,
663 value: &crate::floatpath::FloatValue,
664 precision_override: Option<u32>,
665 ) -> crate::base::Result<String> {
666 // Python's to_cardinal routes every float/Decimal through this
667 // language's own decimal grammar — 5.0 keeps its ".0" tail
668 // ("comma nulla"), unlike Base's whole-value integer route.
669 self.to_cardinal_float(value, precision_override)
670 }
671
672 /// `to_ordinal(float/Decimal)` — **not** the cardinal float grammar.
673 ///
674 /// ```text
675 /// try:
676 /// num = int(number)
677 /// except (ValueError, TypeError):
678 /// return str(number)
679 /// ```
680 ///
681 /// `int()` truncates toward zero, so `to_ordinal(2.5)` == "druhý",
682 /// `to_ordinal(-1.5)` == "mínus jedený" (int is -1), and `-0.0` -> 0 ->
683 /// "nulaý" with **no** minus. Exponent forms convert fine — `int(1e16)`
684 /// == 10**16, whose cardinal + "ý" is "desať biliárdý", and
685 /// `Decimal("1E+2")` -> 100 -> "stý" — unlike the cardinal path, where
686 /// the string algorithm raises ValueError on them. The `except` guard
687 /// never fires for a real float/Decimal (int() succeeds), so it is not
688 /// modelled here.
689 fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
690 let num = match value {
691 // Truncation toward zero; every whole f64 is exactly
692 // representable, and NaN/±inf floats never reach this entry
693 // (the dispatcher keeps them on the Python side).
694 FloatValue::Float { value, .. } => {
695 BigInt::from_f64(value.trunc()).unwrap_or_else(BigInt::zero)
696 }
697 // `int(Decimal)` — with_scale(0) truncates toward zero, same as
698 // `floatpath::float2tuple`'s `pre`.
699 FloatValue::Decimal { value, .. } => {
700 value.with_scale(0).as_bigint_and_exponent().0
701 }
702 };
703 self.to_ordinal(&num)
704 }
705
706 // to_ordinal_num / to_year float entries stay at the trait defaults:
707 // SK overrides neither method, so Base's to_ordinal_num echoes
708 // str(number) (the default hook) and Base's to_year routes through
709 // to_cardinal (the default hook lands in cardinal_float_entry above).
710
711 /// Base's `str_to_number` parses "Infinity" *successfully*; SK's
712 /// ValueError comes later, from `int("Infinity")` inside `to_cardinal`
713 /// (no "." in the string, so the else arm feeds the whole token to
714 /// int()). The Rust dispatcher hard-codes Base's OverflowError for
715 /// `ParsedNumber::Inf`, which SK never executes — so Inf parses punt to
716 /// the Python fallback, which reproduces every mode byte for byte. NaN
717 /// stays on the dispatcher path (its hard-coded ValueError matches
718 /// `int("NaN")` on the cardinal path). Same shape as `lang_as.rs`.
719 fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
720 let parsed = python_decimal_parse(s)?;
721 Ok(parsed)
722 }
723
724 /// `Decimal('Infinity')` / `-Infinity` from a string arg. SK's failure mode
725 /// depends on `to` — verified live against the pure-Python oracle:
726 ///
727 /// * `to_cardinal` / `to_year` reach `int(str(number))` == `int("Infinity")`
728 /// through the else arm (no "." in the string) → `ValueError`. `-Infinity`
729 /// strips its sign first, so the message quotes `'Infinity'`.
730 /// * `to_ordinal` does `int(number)` == `int(Decimal('Infinity'))` first,
731 /// whose `OverflowError` the `except (ValueError, TypeError)` does **not**
732 /// catch → `OverflowError` (Base's default here).
733 /// * `to_ordinal_num` returns the `Decimal` object unchanged (untested, and
734 /// not representable as a string) — left on Base's `OverflowError`.
735 ///
736 /// NaN is *not* overridden: Base's `nan_result` already yields `ValueError`,
737 /// which matches `int("NaN")` on the corpus-covered cardinal path.
738 fn inf_result(&self, _negative: bool, to: &str) -> Result<String> {
739 match to {
740 "cardinal" | "year" => Err(N2WError::Value(
741 "invalid literal for int() with base 10: 'Infinity'".into(),
742 )),
743 // ordinal (OverflowError) and the un-representable ordinal_num keep
744 // Base's OverflowError.
745 _ => Err(N2WError::Overflow(
746 "cannot convert Infinity to integer".into(),
747 )),
748 }
749 }
750 /// This language's own `to_currency(currency=...)` default,
751 /// read from the live Python signature. Only 44 of 156 use EUR.
752 fn default_currency(&self) -> &str {
753 "EUR"
754 }
755
756 /// This language's own `to_currency(separator=...)` default,
757 /// read from the live Python signature. Base's is ",", but only
758 /// 36 of 149 languages actually use it — most default to " " or a
759 /// conjunction, so inheriting Base's comma silently corrupts them.
760 fn default_separator(&self) -> &str {
761 ","
762 }
763
764 fn negword(&self) -> &str {
765 NEGWORD
766 }
767
768 fn pointword(&self) -> &str {
769 "celých"
770 }
771
772 /// Port of `Num2Word_SK.to_cardinal`, integer path only.
773 ///
774 /// Python stringifies the input and looks for `"."` after a `","` → `"."`
775 /// swap; `str(int)` contains neither, so integers always take the `else`
776 /// branch. The float branch (`pointword`, leading-zero padding) is out of
777 /// scope.
778 ///
779 /// Python's `int(str(value)[1:])` for the negative case is exactly
780 /// `value.abs()` — `BigInt` never stringifies as `"-0"`, so there is no
781 /// edge case where the two diverge.
782 fn to_cardinal(&self, value: &BigInt) -> Result<String> {
783 if value.is_negative() {
784 let result = self.int2word(&value.abs())?;
785 Ok(format!("{} {}", NEGWORD, result))
786 } else {
787 self.int2word(value)
788 }
789 }
790
791 /// Port of `Num2Word_SK.to_ordinal`.
792 ///
793 /// The `try: int(number) except (ValueError, TypeError): return str(number)`
794 /// guard cannot fire for an integer argument and is not modelled.
795 ///
796 /// Table hit → the proper Slovak ordinal. Table miss → cardinal + "ý",
797 /// which is what Python calls "a simplified implementation" and what the
798 /// corpus pins: "nulaý", "dvadsať päťý", "milióný", "mínus jedený"
799 /// (bugs 1 and 2).
800 fn to_ordinal(&self, value: &BigInt) -> Result<String> {
801 for (k, word) in ORDINALS.iter() {
802 if value == &BigInt::from(*k) {
803 return Ok(word.to_string());
804 }
805 }
806 let cardinal = self.to_cardinal(value)?;
807 Ok(format!("{}ý", cardinal))
808 }
809
810 /// Port of the non-integer path of `Num2Word_SK.to_cardinal`.
811 ///
812 /// SK overrides `to_cardinal` outright and handles floats/Decimals **inline
813 /// off the string form** — `n = str(number)` — never through
814 /// `Num2Word_Base.to_cardinal_float` and never through `float2tuple`. So this
815 /// override does NOT call the shared `floatpath` code: it rebuilds
816 /// `n = str(number)` exactly (`python_repr_f64` for a float,
817 /// `python_str_decimal` for a Decimal) and runs the Python algorithm:
818 ///
819 /// ```python
820 /// n = str(number).replace(",", ".")
821 /// if "." in n:
822 /// is_negative = n.startswith("-")
823 /// abs_n = n[1:] if is_negative else n
824 /// left, right = abs_n.split(".")
825 /// leading_zero_count = len(right) - len(right.lstrip("0"))
826 /// decimal_part = (ZERO[0] + " ") * leading_zero_count + self._int2word(int(right))
827 /// result = "%s %s %s" % (self._int2word(int(left)), self.pointword, decimal_part)
828 /// if is_negative: result = self.negword + " " + result
829 /// return result
830 /// else: # no "." — scientific/inf/nan, or an integral Decimal str
831 /// is_negative = n.startswith("-")
832 /// if is_negative: return self.negword + " " + self._int2word(int(n[1:]))
833 /// else: return self._int2word(int(n))
834 /// ```
835 ///
836 /// Consequences reproduced verbatim:
837 /// * **The f64-artefact trap does not apply.** SK reads the shortest-
838 /// round-trip repr, not `abs(value-pre)*10**precision`: `2.675` reads
839 /// "675" straight off `"2.675"` → "dva celých šesťsto sedemdesiat päť",
840 /// `1.005` reads "005" off `"1.005"` → "jeden celých nula nula päť".
841 /// * **The fraction is spelled as a whole number, not digit by digit.**
842 /// `int(right)` runs through `_int2word`, so `Decimal("1.10")` →
843 /// "jeden celých desať" (ten), not "…jeden nula". Each leading zero of
844 /// `right` prepends a bare "nula "; SK does **not** rstrip trailing zeros
845 /// (unlike CS), so a float `1.0` (repr "1.0") → "jeden celých nula nula"
846 /// (one leading-zero "nula" + `int2word(0)` "nula") and `Decimal("1.00")`
847 /// → "jeden celých nula nula nula".
848 /// * `precision` / `precision_override` are **ignored**. SK's Python
849 /// `to_cardinal` takes no `precision` kwarg and never reads
850 /// `self.precision`, so the `precision=` override the dispatcher stashes
851 /// is dead for SK. The parameter is accepted and dropped.
852 /// * A repr/str with **no "."** (scientific notation, `inf`, `nan`) feeds a
853 /// non-decimal token to `int()`, which raises **ValueError** via
854 /// [`parse_int`]. An integral Decimal like `Decimal('5')` prints `"5"`
855 /// and the else arm returns "päť"; `Decimal('-3')` returns "mínus tri".
856 /// * A "." repr that also carries an `'e'` (`1.5e-05` → `"1.5e-05"`) enters
857 /// the decimal arm and hits `int("5e-05")` → ValueError, same as Python.
858 ///
859 /// negword is used **unstripped** (`self.negword + " " + result`), exactly as
860 /// the Python source; "mínus" has no surrounding whitespace so the bytes
861 /// match. `is_negative` keys off the reconstructed string's leading '-', so a
862 /// float `-0.0` (repr "-0.0") reads "mínus …" just as Python does.
863 fn to_cardinal_float(
864 &self,
865 value: &FloatValue,
866 _precision_override: Option<u32>,
867 ) -> Result<String> {
868 // n = str(number).replace(",", ".") — a float repr / Decimal str never
869 // carries a comma, so the replace is a no-op, kept for faithfulness.
870 let n_raw = match value {
871 FloatValue::Float { value, .. } => python_repr_f64(*value),
872 FloatValue::Decimal { value, .. } => python_str_decimal(value),
873 };
874 let n = n_raw.replace(',', ".");
875
876 if n.contains('.') {
877 let is_negative = n.starts_with('-');
878 // abs_n = n[1:] if is_negative else n. '-' is one ASCII byte, so
879 // slicing at 1 lands on a char boundary.
880 let abs_n: &str = if is_negative { &n[1..] } else { &n };
881
882 // left, right = abs_n.split(".")
883 let (left, right) = match abs_n.split_once('.') {
884 Some(pair) => pair,
885 // Unreachable: `n` contains '.', and it is never the leading char.
886 None => {
887 return Err(value_error(format!(
888 "invalid literal for int() with base 10: '{}'",
889 abs_n
890 )))
891 }
892 };
893
894 // leading_zero_count = len(right) - len(right.lstrip("0")) — the
895 // count of leading '0' chars. SK does not rstrip the fraction.
896 let leading_zero_count = right.chars().take_while(|c| *c == '0').count();
897
898 // decimal_part = (ZERO[0] + " ") * leading_zero_count
899 // + self._int2word(int(right))
900 // int(right) is evaluated and _int2word run here, before int(left)
901 // below — matching Python's statement order.
902 let right_int = parse_int(right)?;
903 let right_word = self.int2word(&right_int)?;
904 let mut decimal_part = String::new();
905 for _ in 0..leading_zero_count {
906 decimal_part.push_str(ZERO);
907 decimal_part.push(' ');
908 }
909 decimal_part.push_str(&right_word);
910
911 // result = "%s %s %s" % (self._int2word(int(left)), pointword, decimal_part)
912 let left_int = parse_int(left)?;
913 let result = format!(
914 "{} {} {}",
915 self.int2word(&left_int)?,
916 POINTWORD,
917 decimal_part
918 );
919
920 if is_negative {
921 Ok(format!("{} {}", NEGWORD, result))
922 } else {
923 Ok(result)
924 }
925 } else {
926 // else arm: no "." — reachable only for scientific notation / inf /
927 // nan (all raise ValueError through parse_int) or an integral
928 // Decimal str like "5".
929 let is_negative = n.starts_with('-');
930 if is_negative {
931 let abs_n = &n[1..];
932 let m = parse_int(abs_n)?;
933 Ok(format!("{} {}", NEGWORD, self.int2word(&m)?))
934 } else {
935 let m = parse_int(&n)?;
936 self.int2word(&m)
937 }
938 }
939 }
940
941 // to_ordinal_num: SK does not override Num2Word_Base.to_ordinal_num, which
942 // returns the value unchanged → the trait default is correct.
943 //
944 // to_year: SK does not override Num2Word_Base.to_year, which delegates to
945 // to_cardinal → the trait default is correct.
946
947 // ---- currency -------------------------------------------------------
948 //
949 // SK supplies `CURRENCY_FORMS`, `pluralize` and an int-only `to_currency`.
950 // Everything else on the currency path is `Num2Word_Base`'s and is left at
951 // the trait default on purpose — see "The currency surface" in the module
952 // docs for why each one is already right.
953
954 fn lang_name(&self) -> &str {
955 "Num2Word_SK"
956 }
957
958 fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
959 self.currency_forms.get(code)
960 }
961
962 /// Port of `Num2Word_SK.pluralize(n, forms)`.
963 ///
964 /// Python indexes the tuple directly, so a form list shorter than the
965 /// selected index raises IndexError. Every SK entry has three forms, so
966 /// that is unreachable — but it is mapped to `Index` rather than panicking
967 /// so the exception type survives if the table ever changes.
968 ///
969 /// Reached only from `Num2Word_Base.to_currency`'s float path; SK's own int
970 /// path pointedly does *not* call this (see [`LangSk::to_currency`]).
971 fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
972 forms
973 .get(plural_form_index(n))
974 .cloned()
975 .ok_or_else(|| N2WError::Index("tuple index out of range".into()))
976 }
977
978 /// Port of `Num2Word_SK.to_currency`.
979 ///
980 /// SK intercepts **`isinstance(val, int)`** — a true Python `int`, never a
981 /// whole float — and hands everything else to `Num2Word_Base.to_currency`.
982 /// The split is why `currency:EUR` of `1` is "jeden euro" (no cents) while
983 /// `1.0` is "jeden euro, nula centov".
984 ///
985 /// # The int path's plural bug, reproduced (bug 6)
986 ///
987 /// Python picks the currency word by hand here instead of calling
988 /// `self.pluralize`:
989 ///
990 /// ```python
991 /// if abs_val == 1:
992 /// currency_str = cr1[0]
993 /// else:
994 /// currency_str = cr1[1] if len(cr1) > 1 else cr1[0]
995 /// ```
996 ///
997 /// So *every* count other than 1 takes `cr1[1]`, the paucal (2-4) form,
998 /// including 0, 5+ and the teens — where `pluralize` would correctly pick
999 /// the genitive `cr1[2]`. The corpus locks the wrong answers in:
1000 /// `currency:USD` of `0` is "nula doláre", of `100` is "sto doláre", of
1001 /// `1000000` is "milión doláre" — all should be "dolárov", and the float
1002 /// path one line below *does* say "dolárov" for the same magnitudes ("nula
1003 /// dolárov, päťdesiat centov" at 0.5). EUR shows it just as plainly:
1004 /// "nula eurá" (int 0) against "nula eur" (float 0.5). Do not "fix" this
1005 /// into a `pluralize` call.
1006 ///
1007 /// `abs(val)` is taken before the comparison, so `-1` is singular too:
1008 /// "mínus jeden euro".
1009 ///
1010 /// Bug 4 reaches through here: `to_cardinal` is called unguarded, so
1011 /// `to_currency(10**33, "EUR")` propagates `KeyError: 11` rather than
1012 /// raising OverflowError or rendering.
1013 fn to_currency(
1014 &self,
1015 val: &CurrencyValue,
1016 currency: &str,
1017 cents: bool,
1018 separator: Option<&str>,
1019 adjective: bool,
1020 ) -> Result<String> {
1021 if let CurrencyValue::Int(v) = val {
1022 // Python: `try: cr1, cr2 = self.CURRENCY_FORMS[currency]
1023 // except (KeyError, AttributeError): return super()...`
1024 // The AttributeError arm is dead — `CURRENCY_FORMS` always exists —
1025 // so only a missing code falls through, and Base then hits the same
1026 // KeyError and raises NotImplementedError. Delegating is a longer
1027 // route to the same error, but it is the route Python takes.
1028 if let Some(forms) = self.currency_forms.get(currency) {
1029 // `self.negword`, used *unstripped* — unlike Base, which does
1030 // `"%s " % self.negword.strip()`. "mínus" has no surrounding
1031 // whitespace, so the two agree, but the shapes differ.
1032 let minus_str = if v.is_negative() { NEGWORD } else { "" };
1033 let abs_val = v.abs();
1034 let money_str = self.to_cardinal(&abs_val)?;
1035
1036 let currency_str = if abs_val.is_one() {
1037 forms.unit.first().cloned()
1038 } else {
1039 forms.unit.get(1).or_else(|| forms.unit.first()).cloned()
1040 }
1041 .ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
1042
1043 // Python: ("%s %s %s" % (minus_str, money_str, currency_str))
1044 // .strip()
1045 // The `.strip()` exists to eat the leading space that an empty
1046 // `minus_str` leaves behind; neither of the other two fields can
1047 // carry outer whitespace, so `.trim()` is equivalent.
1048 //
1049 // `cents`, `separator` and `adjective` are all ignored on this
1050 // path — Python drops them too. `adjective=True` is a no-op for
1051 // SK either way, since `CURRENCY_ADJECTIVES` is empty.
1052 return Ok(format!("{} {} {}", minus_str, money_str, currency_str)
1053 .trim()
1054 .to_string());
1055 }
1056 }
1057
1058 // Floats and Decimals — plus ints whose code SK does not know — go to
1059 // `Num2Word_Base.to_currency`, which *does* use `pluralize`.
1060 crate::currency::default_to_currency(
1061 self,
1062 val,
1063 currency,
1064 cents,
1065 separator.unwrap_or(self.default_separator()),
1066 adjective,
1067 )
1068 }
1069}