num2words2_core/lang_ja.rs
1//! Port of `lang_JA.py` (Japanese).
2//!
3//! Shape: **engine**. `Num2Word_JA` subclasses `Num2Word_Base`, defines
4//! `high_numwords`/`mid_numwords`/`low_numwords` + `merge`, and lets the
5//! inherited `splitnum`/`clean` machinery drive the conversion. So `cards`,
6//! `maxval` and `merge` are all supplied here and `base::splitnum`/
7//! `base::clean` do the folding.
8//!
9//! `to_cardinal` is nevertheless overridden below rather than left to
10//! `base::default_to_cardinal`, because JA ships its own copy that differs in
11//! exactly one observable way: the negative prefix. Base does
12//! `out = "%s " % self.negword.strip()` (word + space); JA does
13//! `out = self.negword` verbatim, so there is **no space**:
14//! `to_cardinal(-1) == "マイナス一"`, not "マイナス 一".
15//!
16//! # The `reading` / `prefer` collapse
17//!
18//! Python's JA module threads two extra keyword arguments — `reading` (kanji
19//! vs. hiragana) and `prefer` (disambiguates alternative readings) — through
20//! `to_cardinal`/`to_ordinal`/`to_year`, and its `splitnum` funnels every card
21//! lookup through `select_text(text, reading, prefer)`. The plain `Lang`
22//! methods expose no such parameters, so on that surface the only reachable
23//! configuration is the default `reading=False, prefer=None`. (The `*_kw`
24//! hooks DO reach the other configurations; see "Grammatical kwargs" below —
25//! they run on a separate, unresolved card table.) Under the default
26//! configuration `select_text` is a pure, input-independent projection:
27//!
28//! * `reading=False` picks `text[0]`, i.e. the kanji half of every entry.
29//! * For entries whose kanji half is itself a tuple of alternatives — only
30//! `cards[0] == (("零", "〇"), ("ゼロ", "れい"))` — `prefer=None` makes
31//! `common` empty, so `len(common) == 1` is false and it falls back to
32//! `text[0]` again: **"零"** (never "〇").
33//! * The seven/four alternatives `("七", ("なな", "しち"))` and
34//! `("四", ("よん", "し"))` only branch on the *reading* half, so the
35//! kanji path takes "七"/"四" unconditionally.
36//!
37//! Because the projection is constant, the cards are pre-resolved to plain
38//! kanji strings in `new()` and `base::splitnum` (which has no `select_text`
39//! hook) becomes exactly equivalent. This was verified against the Python
40//! interpreter over 12,060 values — every integer in -2000..5000, every power
41//! of ten up to 10^51, the overflow boundary, and 5,000 random values drawn
42//! from the full 0..10^51 range — with zero mismatches.
43//!
44//! # Grammatical kwargs (`reading=`, `prefer=`, `counter=`, `era=`)
45//!
46//! The `*_kw` hooks port the full Python surface:
47//!
48//! * `to_cardinal(value, reading=False, prefer=None)` — `reading` truthy
49//! (including the string `"arabic"`) selects the hiragana half of every
50//! card; `prefer` disambiguates multi-alternative entries (0 kanji
51//! `("零", "〇")`; 0/4/7 kana `("ゼロ", "れい")`/`("よん", "し")`/
52//! `("なな", "しち")`) — a match count of exactly 1 wins, otherwise the
53//! first alternative. The kana path runs through `rendaku_merge_pairs`,
54//! whose rules genuinely fire there (さん+ひゃく → さんびゃく,
55//! いち+ちょう → いっちょう, ...).
56//! * `to_ordinal(value, reading=False, prefer=None, counter="番")` —
57//! `verify_ordinal` FIRST (so a negative raises TypeError even when the
58//! suffix would raise), then `_ordinal_suffix`: reading + any counter
59//! other than "番" raises `NotImplementedError("Reading not implemented
60//! for %s" % counter)`; non-reading appends `counter + "目"` verbatim
61//! (no counter validation: "つ" gives 十一つ目).
62//! * `to_ordinal_num(value, reading=False, counter="番")` — no `prefer`,
63//! no verify; `"-5個目"` is real.
64//! * `to_year(val, suffix=None, longval=True, reading=False, prefer=None,
65//! era=True)` — `suffix`/`longval` are accepted and never read. `era`
66//! falsy (including an explicit `era=None`) takes the Gregorian branch:
67//! `紀元前`/`きげんぜん` prefix for negatives, and on the reading path a
68//! year ending in 9 swaps its last three chars (きゅう) for く. The era
69//! branch renders year 1 as 元/がん, applies the same …九 → …く fix, and
70//! `reading == "arabic"` (exact string compare) keeps the kanji era name
71//! with an Arabic era-year. `prefer` also participates in the era search:
72//! on a duplicate start year it backtracks to an era whose kanji or kana
73//! name is preferred.
74//! * `to_currency(..., reading=False, prefer=None)` — slot 1 of
75//! CURRENCY_FORMS (the reading) replaces slot 0, cardinals go kana, but
76//! `minus_str` stays `self.negword` ("マイナスごえん").
77//! * `to_cardinal_float(value, reading=False, prefer=None)` — defaults
78//! `prefer` to `["れい"]`, so a kana fractional digit 0 reads れい while
79//! the kanji one stays 零 (れい matches nothing on the kanji side).
80//!
81//! The kwargs engine (`splitnum_kw`/`clean_kw`/`merge_kw`) is a verbatim
82//! port of Python's `splitnum` + `Num2Word_Base.clean` over the *unresolved*
83//! card table; with `reading=False, prefer=None` it reduces to the same
84//! output as the pre-resolved fast path (the projection argument above).
85//! The plain trait methods keep using the fast path.
86//!
87//! # Float / Decimal entry routing
88//!
89//! `to_ordinal`, `to_ordinal_num` and `to_year` accept floats/Decimals in
90//! Python, and each does something different (the base defaults match none
91//! of them):
92//!
93//! * `to_ordinal(float)` → `Num2Word_Base.verify_ordinal`: non-whole →
94//! `TypeError("Cannot treat float %s as ordinal.")`; whole-negative →
95//! `TypeError("Cannot treat negative num %s as ordinal.")`; whole
96//! non-negative → integer cardinal + 番目. `-0.0` passes both checks
97//! (`int(-0.0) == -0.0`, `abs(-0.0) == -0.0`) → 零番目.
98//! * `to_ordinal_num(float)` → `"%s%s" % (value, "番目")` — the raw
99//! `str(value)`, so `"-1000000.0番目"`, `"1e+16番目"`, `"5.00番目"`.
100//! * `to_year(float)` → the era search runs on the raw number.
101//! Anything below 645 — all negatives, ±0.0, fractions — raises
102//! `ValueError("Can't convert years less than 645 to era")`. Above it,
103//! `era_year = year - start + 1` is computed in the value's own
104//! arithmetic: f64 for floats (so `to_year(1e16)` is 令和…七千九百八十年,
105//! the double rounding of `1e16 - 2019 + 1`, while `Decimal("1E+16")`
106//! would be exact) and BigDecimal for Decimals
107//! (`Decimal("1E+20")` → 令和九千九百九十九京…七千九百八十二年).
108//!
109//! # Rendaku is dead code on the plain-trait path
110//!
111//! `rendaku_merge_pairs` applies sequential-voicing euphony (三+ひゃく →
112//! さんびゃく, 六+ひゃく → ろっぴゃく, ...). Every one of its rules is
113//! gated on an equality test against a **hiragana** pair literal —
114//! `rpair == ("ひゃく", 100)`, `lpair == ("さん", 3)`, and so on. On the
115//! kanji path the pairs are `("百", 100)` / `("三", 3)`, which never compare
116//! equal, so the function degrades to its fall-through
117//! `("%s%s" % (ltext, rtext), lnum * rnum)`. Its `if lnum > rnum: raise
118//! ValueError` guard is likewise unreachable, because the sole caller
119//! (`merge`) only invokes it on the `lnum < rnum` branch. Verified
120//! exhaustively against the interpreter for every kanji pair that can reach
121//! it. `merge` below therefore inlines the fall-through and notes the
122//! omission rather than porting rules that can never fire. (The kwargs
123//! engine's `merge_kw` DOES port the full rule set — the kana path reaches
124//! them.)
125//!
126//! # Inherited from `Num2Word_Base`
127//!
128//! * `is_title` is False (JA never sets it), so `title()` is the identity.
129//! JA does populate `exclude_title = ["点", "マイナス"]`, but it is only
130//! consulted by `title()` when `is_title` is True — i.e. never. It is
131//! omitted here.
132//! * `verify_ordinal` supplies the negative-ordinal `TypeError`.
133//! * `MAXVAL = 1000 * list(self.cards.keys())[0]`. The OrderedDict's first
134//! *inserted* key is the first high numword, 10^48, so MAXVAL is 10^51 —
135//! which coincides with `Cards::highest()` here since the highs are both
136//! inserted first and the largest.
137//!
138//! # Notable faithful behaviours
139//!
140//! 1. `merge`'s `lnum == 1 and rnum < 10000` short-circuit is what makes
141//! Japanese drop the leading 一 below a myriad but keep it at and above
142//! one: `1000 -> "千"` (not "一千") yet `10000 -> "一万"`. The cutoff is
143//! the literal `10000`, not the card's own magnitude.
144//! 2. `to_ordinal_num` never calls `verify_ordinal` and never calls
145//! `to_cardinal` — it string-formats the raw integer. So negatives pass
146//! straight through: `to_ordinal_num(-1) == "-1番目"`, while
147//! `to_ordinal(-1)` raises `TypeError`. That asymmetry is real.
148//! 3. `to_year` raises `ValueError` (not `OverflowError`) for any year below
149//! 645, the start of the first recorded era — including all negative
150//! years. There is no BCE handling on the reachable path.
151//! 4. `to_year` past the last era (令和, 2019) keeps counting within it
152//! rather than failing: `to_year(9999) == "令和七千九百八十一年"`.
153//! 5. Era year 1 renders as 元 ("gan", the origin year), not 一:
154//! `to_year(999) == "長保元年"`.
155//!
156//! # Currency
157//!
158//! `Num2Word_JA` overrides `to_currency` **wholesale** and shares almost
159//! nothing with `Num2Word_Base.to_currency`. The differences are all
160//! observable:
161//!
162//! 1. **`CURRENCY_FORMS` holds `(kanji, reading)`, not `(singular, plural)`.**
163//! Every other language's tuple is a plural table that `pluralize` indexes;
164//! JA's second slot is the *hiragana reading*, selected by the `reading=`
165//! kwarg. Only JPY actually differs across the two slots —
166//! `("円", "えん")` — the other four repeat themselves. This collision is
167//! load-bearing for `to_cheque`; see below.
168//! 2. **`pluralize` is never called.** JA concatenates `cr1[0]` directly, so
169//! the abstract `Num2Word_Base.pluralize` (which raises) is never reached
170//! and is correctly left at the trait default here.
171//! 3. **No separator, and no space anywhere.** The pieces are glued with
172//! `"%s%s%s%s%s"`. `separator=` is accepted and then never read — verified
173//! against the interpreter, `separator="XX"` changes nothing.
174//! 4. **`minus_str` is `self.negword` verbatim**, not Base's
175//! `"%s " % negword.strip()`: `-12.34 EUR -> "マイナス十二ユーロ三十四セント"`.
176//! 5. **No `has_decimal` guard.** Base prints a zero-cent segment for a float
177//! like `1.0`; JA gates the cents purely on `cr2 and right`, so
178//! `1.0 EUR -> "一ユーロ"` — same as the int `1`.
179//! 6. **The divisor is the literal `100`, always.** JA defines no
180//! `CURRENCY_PRECISION`, so `currency_precision()` stays at the trait
181//! default of 100 and is deliberately not overridden. JPY is therefore a
182//! *2-decimal* currency here, with 銭 (sen) as its subunit:
183//! `12.34 JPY -> "十二円三十四銭"`. There is no `divisor == 1` or
184//! `divisor == 1000` path in this language, and KWD/BHD are simply absent.
185//!
186//! ## `cents=` and `adjective=` are inert
187//!
188//! `cents` reaches only `parse_currency_parts(val, is_int_with_cents=cents)`,
189//! and by then `val` has been coerced to a float — so the flag lands in a
190//! branch that is never taken. The one other reader,
191//! `if (cents or abs(val) != left) and not cr2`, is guarded by `not cr2`, and
192//! every `cr2` in JA's table is a non-empty tuple. `adjective` is likewise
193//! dead: `CURRENCY_ADJECTIVES` is `{}`. All three were confirmed inert against
194//! the interpreter over the full corpus argument set. This matters because the
195//! corpus rows were generated with JA's own `cents=False` default while the
196//! diff harness passes `cents=True`; for JA the two agree.
197//!
198//! ## `int -> float` is load-bearing
199//!
200//! `if isinstance(val, int): val = float(val)` runs *before*
201//! `parse_currency_parts`, forcing it down its non-int branch. Handing a
202//! `CurrencyValue::Int` straight to `parse_currency_parts` with
203//! `is_int_with_cents=true` would instead split it into units+cents and render
204//! `1000000 EUR` as "一万ユーロ" rather than "百万ユーロ".
205//!
206//! ## `to_cheque` is inherited, and inherits a bug
207//!
208//! `Num2Word_Base.to_cheque` takes `unit = cr1[-1]`, commented "cheque
209//! convention always uses the plural currency name". JA's last slot is the
210//! *reading*, so JPY cheques come out in hiragana while the amount stays in
211//! kanji: `to_cheque(1234.56, "JPY") == "千二百三十四 AND 56/100 えん"`. The
212//! corpus freezes exactly that, so `to_cheque` is left at the trait default —
213//! `default_to_cheque` reproduces it once `currency_forms` carries the real
214//! 2-arity tuples. (`.upper()` is a no-op on kana/kanji; only the literal
215//! "AND" is ASCII.)
216
217use crate::base::{
218 clean, set_low_numwords, set_mid_numwords, splitnum, Cards, KwVal, Kwargs, Lang, N2WError,
219 Node, Result,
220};
221use crate::currency::{parse_currency_parts, prefix_currency, CurrencyForms, CurrencyValue};
222use crate::floatpath::{float2tuple, FloatValue};
223use bigdecimal::BigDecimal;
224use num_bigint::BigInt;
225use num_traits::{One, Signed, ToPrimitive, Zero};
226use std::cmp::Ordering;
227use std::collections::HashMap;
228
229/// `ERA_START` from `lang_JA.py`, source https://www.sljfaq.org/afaq/era-list.html
230///
231/// Ascending by start year, with **intentional duplicate years** — the module
232/// comments "if there are multiple eras for the same year, use the last one",
233/// and the binary search in [`LangJa::to_year`] does exactly that by walking
234/// right past equal keys (see there).
235///
236/// Python stores `(year, (kanji, hiragana))`; both halves are carried here as
237/// `(year, kanji, hiragana)`. The hiragana reading is reachable through the
238/// `*_kw` hooks (`to_year(..., reading=True)` renders the era name in kana,
239/// and the `prefer=` walk in the era search matches either half). See the
240/// `reading`/`prefer` note in the module docs.
241const ERA_START: &[(i64, &str, &str)] = &[
242 (645, "大化", "たいか"),
243 (650, "白雉", "はくち"),
244 (686, "朱鳥", "しゅちょう"),
245 (701, "大宝", "たいほう"),
246 (704, "慶雲", "けいうん"),
247 (708, "和銅", "わどう"),
248 (715, "霊亀", "れいき"),
249 (717, "養老", "ようろう"),
250 (724, "神亀", "じんき"),
251 (729, "天平", "てんぴょう"),
252 (749, "天平感宝", "てんぴょうかんぽう"),
253 (749, "天平勝宝", "てんぴょうしょうほう"),
254 (757, "天平宝字", "てんぴょうじょうじ"),
255 (765, "天平神護", "てんぴょうじんご"),
256 (767, "神護景雲", "じんごけいうん"),
257 (770, "宝亀", "ほうき"),
258 (781, "天応", "てんおう"),
259 (782, "延暦", "えんりゃく"),
260 (806, "大同", "だいどう"),
261 (810, "弘仁", "こうにん"),
262 (823, "天長", "てんちょう"),
263 (834, "承和", "じょうわ"),
264 (848, "嘉祥", "かしょう"),
265 (851, "仁寿", "にんじゅ"),
266 (855, "斉衡", "さいこう"),
267 (857, "天安", "てんあん"),
268 (859, "貞観", "じょうがん"),
269 (877, "元慶", "がんぎょう"),
270 (885, "仁和", "にんな"),
271 (889, "寛平", "かんぴょう"),
272 (898, "昌泰", "しょうたい"),
273 (901, "延喜", "えんぎ"),
274 (923, "延長", "えんちょう"),
275 (931, "承平", "じょうへい"),
276 (938, "天慶", "てんぎょう"),
277 (947, "天暦", "てんりゃく"),
278 (957, "天徳", "てんとく"),
279 (961, "応和", "おうわ"),
280 (964, "康保", "こうほう"),
281 (968, "安和", "あんな"),
282 (970, "天禄", "てんろく"),
283 (974, "天延", "てんえん"),
284 (976, "貞元", "じょうげん"),
285 (979, "天元", "てんげん"),
286 (983, "永観", "えいかん"),
287 (985, "寛和", "かんな"),
288 (987, "永延", "えいえん"),
289 (989, "永祚", "えいそ"),
290 (990, "正暦", "しょうりゃく"),
291 (995, "長徳", "ちょうとく"),
292 (999, "長保", "ちょうほう"),
293 (1004, "寛弘", "かんこう"),
294 (1013, "長和", "ちょうわ"),
295 (1017, "寛仁", "かんにん"),
296 (1021, "治安", "じあん"),
297 (1024, "万寿", "まんじゅ"),
298 (1028, "長元", "ちょうげん"),
299 (1037, "長暦", "ちょうりゃく"),
300 (1040, "長久", "ちょうきゅう"),
301 (1045, "寛徳", "かんとく"),
302 (1046, "永承", "えいしょう"),
303 (1053, "天喜", "てんぎ"),
304 (1058, "康平", "こうへい"),
305 (1065, "治暦", "じりゃく"),
306 (1069, "延久", "えんきゅう"),
307 (1074, "承保", "じょうほう"),
308 (1078, "承暦", "じょうりゃく"),
309 (1081, "永保", "えいほう"),
310 (1084, "応徳", "おうとく"),
311 (1087, "寛治", "かんじ"),
312 (1095, "嘉保", "かほう"),
313 (1097, "永長", "えいちょう"),
314 (1098, "承徳", "じょうとく"),
315 (1099, "康和", "こうわ"),
316 (1104, "長治", "ちょうじ"),
317 (1106, "嘉承", "かじょう"),
318 (1108, "天仁", "てんにん"),
319 (1110, "天永", "てんねい"),
320 (1113, "永久", "えいきゅう"),
321 (1118, "元永", "げんえい"),
322 (1120, "保安", "ほうあん"),
323 (1124, "天治", "てんじ"),
324 (1126, "大治", "だいじ"),
325 (1131, "天承", "てんしょう"),
326 (1132, "長承", "ちょうしょう"),
327 (1135, "保延", "ほうえん"),
328 (1141, "永治", "えいじ"),
329 (1142, "康治", "こうじ"),
330 (1144, "天養", "てんよう"),
331 (1145, "久安", "きゅうあん"),
332 (1151, "仁平", "にんぺい"),
333 (1154, "久寿", "きゅうじゅ"),
334 (1156, "保元", "ほうげん"),
335 (1159, "平治", "へいじ"),
336 (1160, "永暦", "えいりゃく"),
337 (1161, "応保", "おうほう"),
338 (1163, "長寛", "ちょうかん"),
339 (1165, "永万", "えいまん"),
340 (1166, "仁安", "にんあん"),
341 (1169, "嘉応", "かおう"),
342 (1171, "承安", "しょうあん"),
343 (1175, "安元", "あんげん"),
344 (1177, "治承", "じしょう"),
345 (1181, "養和", "ようわ"),
346 (1182, "寿永", "じゅえい"),
347 (1184, "元暦", "げんりゃく"),
348 (1185, "文治", "ぶんじ"),
349 (1190, "建久", "けんきゅう"),
350 (1199, "正治", "しょうじ"),
351 (1201, "建仁", "けんにん"),
352 (1204, "元久", "げんきゅう"),
353 (1206, "建永", "けんえい"),
354 (1207, "承元", "じょうげん"),
355 (1211, "建暦", "けんりゃく"),
356 (1214, "建保", "けんぽう"),
357 (1219, "承久", "じょうきゅう"),
358 (1222, "貞応", "じょうおう"),
359 (1225, "元仁", "げんにん"),
360 (1225, "嘉禄", "かろく"),
361 (1228, "安貞", "あんてい"),
362 (1229, "寛喜", "かんき"),
363 (1232, "貞永", "じょうえい"),
364 (1233, "天福", "てんぷく"),
365 (1235, "文暦", "ぶんりゃく"),
366 (1235, "嘉禎", "かてい"),
367 (1239, "暦仁", "りゃくにん"),
368 (1239, "延応", "えんおう"),
369 (1240, "仁治", "にんじ"),
370 (1243, "寛元", "かんげん"),
371 (1247, "宝治", "ほうじ"),
372 (1249, "建長", "けんちょう"),
373 (1256, "康元", "こうげん"),
374 (1257, "正嘉", "しょうか"),
375 (1259, "正元", "しょうげん"),
376 (1260, "文応", "ぶんおう"),
377 (1261, "弘長", "こうちょう"),
378 (1264, "文永", "ぶんえい"),
379 (1275, "健治", "けんじ"),
380 (1278, "弘安", "こうあん"),
381 (1288, "正応", "しょうおう"),
382 (1293, "永仁", "えいにん"),
383 (1299, "正安", "しょうあん"),
384 (1303, "乾元", "けんげん"),
385 (1303, "嘉元", "かげん"),
386 (1307, "徳治", "とくじ"),
387 (1308, "延慶", "えんきょう"),
388 (1311, "応長", "おうちょう"),
389 (1312, "正和", "しょうわ"),
390 (1317, "文保", "ぶんぽう"),
391 (1319, "元応", "げんおう"),
392 (1321, "元亨", "げんこう"),
393 (1325, "正中", "しょうちゅ"),
394 (1326, "嘉暦", "かりゃく"),
395 (1329, "元徳", "げんとく"),
396 (1331, "元弘", "げんこう"),
397 (1332, "正慶", "しょうけい"),
398 (1334, "建武", "けんむ"),
399 (1336, "延元", "えいげん"),
400 (1338, "暦応", "りゃくおう"),
401 (1340, "興国", "こうこく"),
402 (1342, "康永", "こうえい"),
403 (1345, "貞和", "じょうわ"),
404 (1347, "正平", "しょうへい"),
405 (1350, "観応", "かんおう"),
406 (1352, "文和", "ぶんな"),
407 (1356, "延文", "えんぶん"),
408 (1361, "康安", "こうあん"),
409 (1362, "貞治", "じょうじ"),
410 (1368, "応安", "おうあん"),
411 (1370, "建徳", "けんとく"),
412 (1372, "文中", "ぶんちゅう"),
413 (1375, "永和", "えいわ"),
414 (1375, "天授", "てんじゅ"),
415 (1379, "康暦", "こうりゃく"),
416 (1381, "永徳", "えいとく"),
417 (1381, "弘和", "こうわ"),
418 (1384, "至徳", "しとく"),
419 (1384, "元中", "げんちゅう"),
420 (1387, "嘉慶", "かけい"),
421 (1389, "康応", "こうおう"),
422 (1390, "明徳", "めいとく"),
423 (1394, "応永", "おうえい"),
424 (1428, "正長", "しょうちょう"),
425 (1429, "永享", "えいきょう"),
426 (1441, "嘉吉", "かきつ"),
427 (1444, "文安", "ぶんあん"),
428 (1449, "宝徳", "ほうとく"),
429 (1452, "享徳", "きょうとく"),
430 (1455, "康正", "こうしょう"),
431 (1457, "長禄", "ちょうろく"),
432 (1461, "寛正", "かんしょう"),
433 (1466, "文正", "ぶんしょう"),
434 (1467, "応仁", "おうにん"),
435 (1469, "文明", "ぶんめい"),
436 (1487, "長享", "ちょうきょう"),
437 (1489, "延徳", "えんとく"),
438 (1492, "明応", "めいおう"),
439 (1501, "文亀", "ぶんき"),
440 (1504, "永正", "えいしょう"),
441 (1521, "大永", "だいえい"),
442 (1528, "享禄", "きょうろく"),
443 (1532, "天文", "てんぶん"),
444 (1555, "弘治", "こうじ"),
445 (1558, "永禄", "えいろく"),
446 (1570, "元亀", "げんき"),
447 (1573, "天正", "てんしょう"),
448 (1593, "文禄", "ぶんろく"),
449 (1596, "慶長", "けいちょう"),
450 (1615, "元和", "げんな"),
451 (1624, "寛永", "かんえい"),
452 (1645, "正保", "しょうほう"),
453 (1648, "慶安", "けいあん"),
454 (1652, "承応", "じょうおう"),
455 (1655, "明暦", "めいれき"),
456 (1658, "万治", "まんじ"),
457 (1661, "寛文", "かんぶん"),
458 (1673, "延宝", "えんぽう"),
459 (1681, "天和", "てんな"),
460 (1684, "貞享", "じょうきょう"),
461 (1688, "元禄", "げんろく"),
462 (1704, "宝永", "ほうえい"),
463 (1711, "正徳", "しょうとく"),
464 (1716, "享保", "きょうほう"),
465 (1736, "元文", "げんぶん"),
466 (1741, "寛保", "かんぽう"),
467 (1744, "延享", "えんきょう"),
468 (1748, "寛延", "かんえん"),
469 (1751, "宝暦", "ほうれき"),
470 (1764, "明和", "めいわ"),
471 (1773, "安永", "あんえい"),
472 (1781, "天明", "てんめい"),
473 (1801, "寛政", "かんせい"),
474 (1802, "享和", "きょうわ"),
475 (1804, "文化", "ぶんか"),
476 (1818, "文政", "ぶんせい"),
477 (1831, "天保", "てんぽう"),
478 (1845, "弘化", "こうか"),
479 (1848, "嘉永", "かえい"),
480 (1855, "安政", "あんせい"),
481 (1860, "万延", "まんえい"),
482 (1861, "文久", "ぶんきゅう"),
483 (1864, "元治", "げんじ"),
484 (1865, "慶応", "けいおう"),
485 (1868, "明治", "めいじ"),
486 (1912, "大正", "たいしょう"),
487 (1926, "昭和", "しょうわ"),
488 (1989, "平成", "へいせい"),
489 (2019, "令和", "れいわ"),
490];
491
492/// `Num2Word_JA.CURRENCY_FORMS`, verbatim — all five codes, no inheritance.
493///
494/// **The two slots are `(kanji, reading)`, not `(singular, plural)`.** JA is
495/// the odd one out: every other language stores a plural table that
496/// `pluralize` indexes, whereas JA's second slot is what `reading=True`
497/// selects. `to_currency` only ever reads slot 0 (`reading` is unreachable
498/// through the trait), so the second slot looks dead — but
499/// `Num2Word_Base.to_cheque` takes `cr1[-1]` believing it is a plural, which
500/// is how "えん" reaches the JPY cheque output. Keep the 2-arity.
501///
502/// Nothing is inherited here: `Num2Word_JA` subclasses `Num2Word_Base`
503/// directly, not `Num2Word_EUR`, so the shared dict that `Num2Word_EN.__init__`
504/// mutates (adding AUD/CAD/CHF/KWD/... to EUR's table) never reaches JA. Every
505/// code outside these five — including KWD, BHD, INR and CHF — raises
506/// NotImplementedError, and the corpus freezes that.
507fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
508 let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
509 // Adding sen (1/100 yen) support — hence a 2-decimal JPY, not 0-decimal.
510 m.insert("JPY", CurrencyForms::new(&["円", "えん"], &["銭", "銭"]));
511 m.insert("EUR", CurrencyForms::new(&["ユーロ", "ユーロ"], &["セント", "セント"]));
512 m.insert("USD", CurrencyForms::new(&["ドル", "ドル"], &["セント", "セント"]));
513 m.insert("GBP", CurrencyForms::new(&["ポンド", "ポンド"], &["ペンス", "ペンス"]));
514 // Chinese yuan with fen (1/100 yuan)
515 m.insert("CNY", CurrencyForms::new(&["元", "元"], &["分", "分"]));
516 m
517}
518
519/// `abs(Decimal(str(f)).as_tuple().exponent)` for an f64 — the number of
520/// fractional digits in the shortest round-tripping decimal form.
521///
522/// `Num2Word_JA.to_cardinal_float` recomputes the precision from
523/// `float(value)` (see the override), so a Decimal's own exponent is *not*
524/// what is used: `Decimal("1.10")` yields precision 1 (from `repr(1.1)`),
525/// not 2. Rust's `{}` for f64 is shortest-round-trip like Python's `repr`,
526/// so counting the digits after the point matches for every reachable value.
527/// (Rust never renders f64 in exponent form, so an extreme magnitude where
528/// Python's `repr` switches to `1e21` would diverge — but no JA value reaches
529/// that range, and for the common `FloatValue::Float` arm the carried,
530/// Python-derived precision is used instead of this function.) This mirrors
531/// the private `floatpath::float_repr_precision`, which is not re-exported.
532fn float_repr_precision(f: f64) -> u32 {
533 let s = format!("{}", f);
534 match s.split_once('.') {
535 Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
536 _ => 0,
537 }
538}
539
540/// One kwargs-path card: the *unresolved* Python tuple entry, before
541/// `select_text` collapses it. `kanji`/`kana` list the alternatives in
542/// source order (first = the `text[0]` fallback).
543struct KwCard {
544 num: BigInt,
545 kanji: Vec<&'static str>,
546 kana: Vec<&'static str>,
547}
548
549/// `select_text(text, reading, prefer)` — reading picks the half, then a
550/// multi-alternative half is disambiguated by `prefer`: Python takes
551/// `set(text) & set(prefer)` and uses it only when the intersection has
552/// exactly one element, else `text[0]`. The alternatives are distinct, so
553/// counting which of them appear in `prefer` is the same set arithmetic.
554fn select_text(card: &KwCard, reading: bool, prefer: &[String]) -> &'static str {
555 let alts = if reading { &card.kana } else { &card.kanji };
556 if alts.len() > 1 {
557 let common: Vec<&'static str> = alts
558 .iter()
559 .copied()
560 .filter(|a| prefer.iter().any(|p| p == a))
561 .collect();
562 if common.len() == 1 {
563 return common[0];
564 }
565 }
566 alts[0]
567}
568
569/// A node in the kwargs engine's `splitnum` tree — Python's tuple-vs-list.
570#[derive(Clone)]
571enum JaNode {
572 Pair(String, BigInt),
573 List(Vec<JaNode>),
574}
575
576/// `rendaku_merge_pairs`, in full. Only the kana path can satisfy the pair
577/// literals; on the kanji path every rule misses and this is the plain
578/// fall-through `("%s%s", lnum * rnum)` (which is what the fast path's
579/// `merge` inlines). The `if lnum > rnum: raise ValueError` guard is not
580/// ported: the sole caller only takes the `lnum < rnum` branch.
581fn rendaku_merge_pairs(l: (String, BigInt), r: (String, BigInt)) -> (String, BigInt) {
582 let (mut lt, ln) = l;
583 let (mut rt, rn) = r;
584 if rt == "ひゃく" && rn == BigInt::from(100) {
585 if lt == "さん" && ln == BigInt::from(3) {
586 rt = "びゃく".to_string();
587 } else if lt == "ろく" && ln == BigInt::from(6) {
588 lt = "ろっ".to_string();
589 rt = "ぴゃく".to_string();
590 } else if lt == "はち" && ln == BigInt::from(8) {
591 lt = "はっ".to_string();
592 rt = "ぴゃく".to_string();
593 }
594 } else if rt == "せん" && rn == BigInt::from(1000) {
595 if lt == "さん" && ln == BigInt::from(3) {
596 rt = "ぜん".to_string();
597 } else if lt == "はち" && ln == BigInt::from(8) {
598 lt = "はっ".to_string();
599 }
600 } else if rt == "ちょう" && rn == BigInt::from(10u64).pow(12) {
601 if lt == "いち" && ln.is_one() {
602 lt = "いっ".to_string();
603 } else if lt == "はち" && ln == BigInt::from(8) {
604 lt = "はっ".to_string();
605 } else if lt == "じゅう" && ln == BigInt::from(10) {
606 lt = "じゅっ".to_string();
607 }
608 } else if rt == "けい" && rn == BigInt::from(10u64).pow(16) {
609 if lt == "いち" && ln.is_one() {
610 lt = "いっ".to_string();
611 } else if lt == "ろく" && ln == BigInt::from(6) {
612 lt = "ろっ".to_string();
613 } else if lt == "はち" && ln == BigInt::from(8) {
614 lt = "はっ".to_string();
615 } else if lt == "じゅう" && ln == BigInt::from(10) {
616 lt = "じゅっ".to_string();
617 } else if lt == "ひゃく" && ln == BigInt::from(100) {
618 lt = "ひゃっ".to_string();
619 }
620 }
621 (format!("{}{}", lt, rt), ln * rn)
622}
623
624/// The era binary search, shared by every `to_year` entry. `cmp(start)` is
625/// `year <=> start` in whichever arithmetic the caller's year lives in
626/// (BigInt / f64 / BigDecimal — Python compares the raw value against the
627/// int table). `prefer` is the duplicate-start-year backtrack: Python guards
628/// it with `if prefer:` (truthy), and a preferred name may match either the
629/// kanji or the hiragana half.
630///
631/// Ported verbatim, including the quirk that first/last are still updated on
632/// the iteration that sets era_idx — harmless, since the `while era_idx is
633/// None` guard is re-checked immediately after. Signed indices mirror
634/// Python: `last = mid - 1` could in principle reach -1 and wrap into
635/// Python's negative indexing, but cannot here — that branch needs
636/// `year < ERA_START[mid][0]`, which is false at mid == 0 because
637/// `year >= 645 == ERA_START[0][0]` is enforced by every caller.
638fn era_index(cmp: &dyn Fn(i64) -> Ordering, prefer: &[String]) -> usize {
639 let last_era_idx = ERA_START.len() as i64 - 1;
640 let mut first: i64 = 0;
641 let mut last: i64 = last_era_idx;
642 let mut era_idx: Option<i64> = None;
643 while era_idx.is_none() {
644 let mid = (first + last).div_euclid(2);
645 let m = mid as usize;
646 // `||` short-circuits exactly as Python's `or` does, so
647 // ERA_START[mid + 1] is never touched when mid == last_era_idx.
648 if mid == last_era_idx
649 || (cmp(ERA_START[m].0) != Ordering::Less
650 && cmp(ERA_START[m + 1].0) == Ordering::Less)
651 {
652 era_idx = Some(mid);
653 // "if an era lasting less than a year is preferred, choose it"
654 if !prefer.is_empty() {
655 let mut i = mid - 1;
656 while i >= 0 && cmp(ERA_START[i as usize].0) == Ordering::Equal {
657 let e = ERA_START[i as usize];
658 // set(ERA_START[i][1]) & set(prefer): non-empty wins.
659 if prefer.iter().any(|p| p == e.1 || p == e.2) {
660 era_idx = Some(i);
661 break;
662 }
663 i -= 1;
664 }
665 }
666 }
667
668 // Ends up at the last index where year >= ERA_START[mid][0]. On a
669 // duplicate year the `else` arm keeps walking right, which is how
670 // "use the last one" falls out.
671 if cmp(ERA_START[m].0) == Ordering::Less {
672 last = mid - 1;
673 } else {
674 first = mid + 1;
675 }
676 }
677 era_idx.unwrap() as usize
678}
679
680/// The JA `reading=` kwarg, tri-state because `to_year` string-compares it:
681/// `reading == "arabic"` takes the Arabic-era-year branch, any other truthy
682/// value is the kana path, falsy (False/None/0/""/[]) is kanji.
683#[derive(PartialEq)]
684enum JaReading {
685 Off,
686 Kana,
687 Arabic,
688}
689
690impl JaReading {
691 /// Python truthiness — everywhere except the `== "arabic"` compare,
692 /// "arabic" is just another truthy string.
693 fn is_on(&self) -> bool {
694 !matches!(self, JaReading::Off)
695 }
696}
697
698/// Python truthiness of a kwarg value.
699fn kw_truthy(v: &KwVal) -> bool {
700 match v {
701 KwVal::Bool(b) => *b,
702 KwVal::Int(i) => *i != 0,
703 KwVal::Str(s) => !s.is_empty(),
704 KwVal::List(l) => !l.is_empty(),
705 KwVal::None => false,
706 }
707}
708
709fn parse_reading(kw: &Kwargs) -> JaReading {
710 match kw.get("reading") {
711 Option::None | Some(KwVal::None) => JaReading::Off,
712 Some(KwVal::Str(s)) if s == "arabic" => JaReading::Arabic,
713 Some(v) => {
714 if kw_truthy(v) {
715 JaReading::Kana
716 } else {
717 JaReading::Off
718 }
719 }
720 }
721}
722
723/// `prefer=` as the item set Python builds. A list is itself; a string is
724/// its characters (`set("〇")`); falsy scalars behave like `prefer or set()`
725/// -> empty. A *truthy* non-iterable (True, 5) would raise TypeError only
726/// if/when `set(prefer)` is evaluated inside `select_text` — too
727/// value-dependent to model, so it defers to Python via NotImplemented.
728fn parse_prefer(kw: &Kwargs) -> Result<Vec<String>> {
729 match kw.get("prefer") {
730 Option::None | Some(KwVal::None) => Ok(Vec::new()),
731 Some(KwVal::List(l)) => Ok(l.clone()),
732 Some(KwVal::Str(s)) => Ok(s.chars().map(String::from).collect()),
733 Some(KwVal::Bool(false)) | Some(KwVal::Int(0)) => Ok(Vec::new()),
734 Some(_) => Err(N2WError::Fallback("kwargs".into())),
735 }
736}
737
738/// Python's `s[:-3]`, by characters (the …きゅう → …く year fix).
739fn chop_last3(s: &str) -> String {
740 let chars: Vec<char> = s.chars().collect();
741 let keep = chars.len().saturating_sub(3);
742 chars[..keep].iter().collect()
743}
744
745/// `str(value)` for the verify_ordinal error messages, python-style. Only
746/// the exception *type* is corpus-checked; the message mirrors the source's
747/// `%s` interpolation as closely as Rust formatting allows.
748fn py_value_str(value: &FloatValue) -> String {
749 match value {
750 FloatValue::Float { value: f, .. } => {
751 if f.abs() >= 1e16 {
752 // repr picks exponent form: 1e+16, 1.5e+20, ...
753 let s = format!("{:e}", f);
754 match s.split_once('e') {
755 Some((m, e)) if !e.starts_with('-') => format!("{}e+{}", m, e),
756 _ => s,
757 }
758 } else if f.fract() == 0.0 {
759 format!("{:.1}", f) // "5.0", "-0.0"
760 } else {
761 format!("{}", f)
762 }
763 }
764 FloatValue::Decimal { value: d, .. } => crate::strnum::python_decimal_str(d),
765 }
766}
767
768pub struct LangJa {
769 cards: Cards,
770 maxval: BigInt,
771 /// The unresolved card table the kwargs engine selects from at call time
772 /// (descending, insertion order of Python's OrderedDict).
773 kw_cards: Vec<KwCard>,
774 /// Built once in `new()`. `to_currency` and `to_cheque` only read it, and
775 /// rebuilding it per call is what made an earlier revision of this port
776 /// slower than the Python it replaces.
777 currency_forms: HashMap<&'static str, CurrencyForms>,
778}
779
780impl Default for LangJa {
781 fn default() -> Self {
782 Self::new()
783 }
784}
785
786impl LangJa {
787 pub fn new() -> Self {
788 let mut cards = Cards::new();
789
790 // setup(): high_numwords listed low->high, then `.reverse()`d.
791 // Reversed order, which is what set_high_numwords consumes:
792 let high = [
793 "極", // 10**48 goku
794 "載", // 10**44 sai
795 "正", // 10**40 sei
796 "澗", // 10**36 kan
797 "溝", // 10**32 kō
798 "穣", // 10**28 jō
799 "秭", // 10**24 shi
800 "垓", // 10**20 gai
801 "京", // 10**16 kei
802 "兆", // 10**12 chō
803 "億", // 10**8 oku
804 "万", // 10**4 man
805 ];
806
807 // JA's set_high_numwords: `max = 4 * len(high)` (= 48), then
808 // `zip(high, range(max, 0, -4))` -> 48, 44, ..., 4. The zip is exact
809 // (12 words, 12 exponents), so nothing is silently dropped.
810 let mut n: u32 = 4 * high.len() as u32;
811 for word in high.iter() {
812 cards.insert(BigInt::from(10u8).pow(n), *word);
813 n -= 4;
814 }
815
816 set_mid_numwords(&mut cards, &[(1000, "千"), (100, "百")]);
817
818 // set_low_numwords maps these to 10, 9, ... 1, 0 by position.
819 // "零" is select_text's resolution of (("零", "〇"), ("ゼロ", "れい"))
820 // under reading=False/prefer=None; "七" and "四" likewise collapse
821 // their (kana, kana) alternative halves away. See module docs.
822 set_low_numwords(
823 &mut cards,
824 &[
825 "十", // 10 jū
826 "九", // 9 kyū
827 "八", // 8 hachi
828 "七", // 7 nana / shichi
829 "六", // 6 roku
830 "五", // 5 go
831 "四", // 4 yon / shi
832 "三", // 3 san
833 "二", // 2 ni
834 "一", // 1 ichi
835 "零", // 0 ZERO / rei
836 ],
837 );
838
839 // MAXVAL = 1000 * first-inserted card key = 1000 * 10**48 = 10**51.
840 let maxval = cards.highest().cloned().unwrap() * BigInt::from(1000);
841
842 // The unresolved table for the kwargs engine: same keys, same
843 // descending order, but both halves of every Python tuple kept, with
844 // the multi-alternative entries (0 kanji, 0/4/7 kana) intact.
845 let mut kw_cards: Vec<KwCard> = Vec::new();
846 let kw_high: [(u32, &str, &str); 12] = [
847 (48, "極", "ごく"),
848 (44, "載", "さい"),
849 (40, "正", "せい"),
850 (36, "澗", "かん"),
851 (32, "溝", "こう"),
852 (28, "穣", "じょう"),
853 (24, "秭", "し"),
854 (20, "垓", "がい"),
855 (16, "京", "けい"),
856 (12, "兆", "ちょう"),
857 (8, "億", "おく"),
858 (4, "万", "まん"),
859 ];
860 for (exp, kanji, kana) in kw_high {
861 kw_cards.push(KwCard {
862 num: BigInt::from(10u8).pow(exp),
863 kanji: vec![kanji],
864 kana: vec![kana],
865 });
866 }
867 kw_cards.push(KwCard { num: BigInt::from(1000), kanji: vec!["千"], kana: vec!["せん"] });
868 kw_cards.push(KwCard { num: BigInt::from(100), kanji: vec!["百"], kana: vec!["ひゃく"] });
869 let kw_low: [(u32, Vec<&'static str>, Vec<&'static str>); 11] = [
870 (10, vec!["十"], vec!["じゅう"]),
871 (9, vec!["九"], vec!["きゅう"]),
872 (8, vec!["八"], vec!["はち"]),
873 (7, vec!["七"], vec!["なな", "しち"]),
874 (6, vec!["六"], vec!["ろく"]),
875 (5, vec!["五"], vec!["ご"]),
876 (4, vec!["四"], vec!["よん", "し"]),
877 (3, vec!["三"], vec!["さん"]),
878 (2, vec!["二"], vec!["に"]),
879 (1, vec!["一"], vec!["いち"]),
880 (0, vec!["零", "〇"], vec!["ゼロ", "れい"]),
881 ];
882 for (n, kanji, kana) in kw_low {
883 kw_cards.push(KwCard { num: BigInt::from(n), kanji, kana });
884 }
885
886 LangJa {
887 cards,
888 maxval,
889 kw_cards,
890 currency_forms: build_currency_forms(),
891 }
892 }
893
894 /// `Num2Word_Base.verify_ordinal`. The float check above it is vacuous for
895 /// integer input, so only the negative guard survives.
896 fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
897 if value.is_negative() {
898 return Err(N2WError::Type(format!(
899 "Cannot treat negative num {} as ordinal.",
900 value
901 )));
902 }
903 Ok(())
904 }
905
906 /// `_ordinal_suffix(reading=False, counter="番")` -> `counter + "目"`.
907 ///
908 /// The `reading=True` arm raises `NotImplementedError` for any counter
909 /// other than "番"; neither `reading` nor `counter` is reachable through
910 /// the trait, so only the kanji default is modelled.
911 fn ordinal_suffix(&self) -> &'static str {
912 "番目"
913 }
914
915 /// `to_currency`'s fractional-cents arm: `self.to_cardinal_float(right / 100.0)`.
916 ///
917 /// This looks like it needs the float path, and does not. `right` arrives
918 /// from `parse_currency_parts(..., keep_precision=False)` — JA never passes
919 /// `keep_precision`, so `right` is `int(fraction * 100)` and is **always a
920 /// whole 0..=99**, even on the branch Python calls "fractional cents".
921 /// Which is the bug: `right / 100.0` divides the *already-rounded* whole
922 /// cents by 100 again, so `2.567 EUR` renders "二ユーロ零点五七セント" —
923 /// "two euro zero point five seven cents". Faithfully reproduced.
924 ///
925 /// Over that closed 100-value domain `Num2Word_JA.to_cardinal_float`
926 /// reduces to a constant projection, so no `repr(float)`/`float2tuple`
927 /// machinery is needed:
928 ///
929 /// * `pre = int(right / 100.0)` is 0, so the head is always `to_cardinal(0)`.
930 /// * `precision = abs(Decimal(repr(right / 100.0)).as_tuple().exponent)`.
931 /// `repr` of a k/100 double is its shortest round-tripping form, i.e.
932 /// "0.0"/"0.1".."0.9" when `right` is a multiple of ten and
933 /// "0.01".."0.99" otherwise — so precision is 1 or 2, never 0, and the
934 /// `if self.precision:` guard on the pointword always passes.
935 /// * `post` is then `right / 10` or `right`, zero-padded to `precision`.
936 /// * The `if value < 0 and pre == 0` negword arm is unreachable: `right`
937 /// is non-negative, so `right / 100.0` never is.
938 ///
939 /// Checked against the interpreter for every `right` in 0..=99: exact match.
940 /// Note JA's `to_cardinal_float` joins with `""` (Base joins with `" "`),
941 /// which is why this builds the string by concatenation.
942 fn cents_as_hundredths(&self, right: &BigInt) -> Result<String> {
943 let r = match right.to_u32() {
944 Some(r) if r <= 99 => r,
945 // parse_currency_parts with divisor=100 and keep_precision=false
946 // cannot produce anything else; see the doc comment above.
947 _ => unreachable!("JA cents out of the 0..=99 domain: {}", right),
948 };
949 let (precision, post) = if r % 10 == 0 { (1usize, r / 10) } else { (2usize, r) };
950
951 let mut out = self.to_cardinal(&BigInt::zero())?;
952 // title() is the identity for JA; pointword() is ("点", "てん")[0].
953 out.push_str(self.pointword());
954 for ch in format!("{:0>width$}", post, width = precision).chars() {
955 let digit = BigInt::from(ch.to_digit(10).unwrap());
956 out.push_str(&self.to_cardinal(&digit)?);
957 }
958 Ok(out)
959 }
960
961 // ---- kwargs engine ----------------------------------------------------
962 //
963 // A verbatim port of Python's `Num2Word_JA.splitnum` +
964 // `Num2Word_Base.clean` over the unresolved card table, threading
965 // `reading`/`prefer` through every `select_text`. With
966 // `reading=False, prefer=[]` it produces exactly what the pre-resolved
967 // fast path (`to_cardinal` above) does; the plain trait methods keep the
968 // fast path, and only the `*_kw` hooks come through here.
969
970 /// `self.cards[1]`, unresolved.
971 fn kw_card_one(&self) -> &KwCard {
972 self.kw_cards.iter().find(|c| c.num.is_one()).unwrap()
973 }
974
975 /// `Num2Word_JA.splitnum(value, reading, prefer)`.
976 fn splitnum_kw(&self, value: &BigInt, reading: bool, prefer: &[String]) -> Vec<JaNode> {
977 for card in &self.kw_cards {
978 if card.num > *value {
979 continue;
980 }
981 // Card 0 is only selected when value == 0, which takes the
982 // special (1, 0) divmod — so the division below never sees a
983 // zero card.
984 let (div, mod_) = if value.is_zero() {
985 (BigInt::one(), BigInt::zero())
986 } else {
987 (value / &card.num, value % &card.num)
988 };
989
990 let mut out: Vec<JaNode> = Vec::new();
991 if div.is_one() {
992 out.push(JaNode::Pair(
993 select_text(self.kw_card_one(), reading, prefer).to_string(),
994 BigInt::one(),
995 ));
996 } else if &div == value {
997 // The tally branch ("eg Roman Numerals"). Unreachable for
998 // JA's dense card table (it needs elem == 1 with value > 1,
999 // but every value >= 2 matches a card >= 2 first); ported
1000 // for shape.
1001 let n = div.to_usize().expect("JA tally count");
1002 return vec![JaNode::Pair(
1003 select_text(card, reading, prefer).repeat(n),
1004 div * &card.num,
1005 )];
1006 } else {
1007 out.push(JaNode::List(self.splitnum_kw(&div, reading, prefer)));
1008 }
1009
1010 out.push(JaNode::Pair(
1011 select_text(card, reading, prefer).to_string(),
1012 card.num.clone(),
1013 ));
1014
1015 if !mod_.is_zero() {
1016 out.push(JaNode::List(self.splitnum_kw(&mod_, reading, prefer)));
1017 }
1018 return out;
1019 }
1020 unreachable!("JA splitnum_kw: no card <= {}", value)
1021 }
1022
1023 /// `Num2Word_JA.merge` with `rendaku_merge_pairs` live (kana pairs can
1024 /// satisfy its literals). Same three arms as the fast path's `merge`,
1025 /// same unreachable `lnum == rnum` fall-off.
1026 fn merge_kw(&self, l: (String, BigInt), r: (String, BigInt)) -> (String, BigInt) {
1027 let (lt, ln) = l;
1028 let (rt, rn) = r;
1029 if ln.is_one() && rn < BigInt::from(10000) {
1030 return (rt, rn);
1031 }
1032 match ln.cmp(&rn) {
1033 Ordering::Greater => (format!("{}{}", lt, rt), ln + rn),
1034 Ordering::Less => rendaku_merge_pairs((lt, ln), (rt, rn)),
1035 Ordering::Equal => unreachable!(
1036 "JA merge_kw: lnum == rnum ({}) is unreachable; Python returns None here",
1037 ln
1038 ),
1039 }
1040 }
1041
1042 /// `Num2Word_Base.clean`, over `JaNode`s and `merge_kw`.
1043 fn clean_kw(&self, mut val: Vec<JaNode>) -> (String, BigInt) {
1044 while val.len() != 1 {
1045 let first_two_pairs =
1046 matches!(&val[0], JaNode::Pair(..)) && matches!(&val[1], JaNode::Pair(..));
1047 let mut out: Vec<JaNode> = Vec::new();
1048 if first_two_pairs {
1049 let mut it = val.into_iter();
1050 let (a, b) = (it.next().unwrap(), it.next().unwrap());
1051 if let (JaNode::Pair(lt, ln), JaNode::Pair(rt, rn)) = (a, b) {
1052 let (t, n) = self.merge_kw((lt, ln), (rt, rn));
1053 out.push(JaNode::Pair(t, n));
1054 } else {
1055 unreachable!()
1056 }
1057 // `if val[2:]: out.append(val[2:])` — the rest, as ONE
1058 // nested list.
1059 let rest: Vec<JaNode> = it.collect();
1060 if !rest.is_empty() {
1061 out.push(JaNode::List(rest));
1062 }
1063 } else {
1064 for elem in val {
1065 match elem {
1066 JaNode::List(l) => {
1067 if l.len() == 1 {
1068 out.push(l.into_iter().next().unwrap());
1069 } else {
1070 let (t, n) = self.clean_kw(l);
1071 out.push(JaNode::Pair(t, n));
1072 }
1073 }
1074 pair => out.push(pair),
1075 }
1076 }
1077 }
1078 val = out;
1079 }
1080 match val.into_iter().next().unwrap() {
1081 JaNode::Pair(t, n) => (t, n),
1082 // splitnum never returns a 1-element list, so the single
1083 // survivor is always a pair; kept total for safety.
1084 JaNode::List(l) => self.clean_kw(l),
1085 }
1086 }
1087
1088 /// `Num2Word_JA.to_cardinal(value, reading, prefer)`, integer arm.
1089 fn cardinal_kw_int(&self, value: &BigInt, reading: bool, prefer: &[String]) -> Result<String> {
1090 let mut out = String::new();
1091 let mut v = value.clone();
1092 if v.is_negative() {
1093 v = v.abs();
1094 out = self.negword().to_string(); // verbatim, no space
1095 }
1096 if &v >= self.maxval() {
1097 return Err(N2WError::Overflow(format!(
1098 "abs({}) must be less than {}.",
1099 v,
1100 self.maxval()
1101 )));
1102 }
1103 let tree = self.splitnum_kw(&v, reading, prefer);
1104 let (words, _) = self.clean_kw(tree);
1105 // title() is the identity for JA.
1106 Ok(format!("{}{}", out, words))
1107 }
1108
1109 /// `Num2Word_JA.to_cardinal_float(value, reading, prefer)` — the same
1110 /// float-casting/precision semantics as the plain `to_cardinal_float`
1111 /// override below, plus the `prefer = prefer or ["れい"]` default, which
1112 /// is inert on the kanji side (れい matches no kanji alternative) but
1113 /// makes a kana fractional digit 0 read れい instead of ゼロ.
1114 fn cardinal_float_impl(
1115 &self,
1116 value: &FloatValue,
1117 reading: bool,
1118 prefer: &[String],
1119 ) -> Result<String> {
1120 let eff_prefer: Vec<String> = if prefer.is_empty() {
1121 vec!["れい".to_string()]
1122 } else {
1123 prefer.to_vec()
1124 };
1125 let (f, precision) = match value {
1126 FloatValue::Float { value, precision } => (*value, *precision),
1127 FloatValue::Decimal { value, .. } => {
1128 let f = value.to_f64().ok_or_else(|| {
1129 N2WError::Value(format!("cannot represent {} as f64", value))
1130 })?;
1131 (f, float_repr_precision(f))
1132 }
1133 };
1134 let (pre, post) = float2tuple(&FloatValue::Float { value: f, precision });
1135
1136 let post_str = post.to_string();
1137 let post_str = format!(
1138 "{}{}",
1139 "0".repeat((precision as usize).saturating_sub(post_str.len())),
1140 post_str
1141 );
1142
1143 let mut out = vec![self.cardinal_kw_int(&pre, reading, &eff_prefer)?];
1144 if value.is_negative() && pre.is_zero() {
1145 out.insert(0, self.negword().to_string());
1146 }
1147 if precision > 0 {
1148 // pointword = ("点", "てん")[1 if reading else 0].
1149 out.push((if reading { "てん" } else { "点" }).to_string());
1150 }
1151 for ch in post_str.chars().take(precision as usize) {
1152 let d = ch.to_digit(10).ok_or_else(|| {
1153 N2WError::Value(format!("non-digit {:?} in fractional part", ch))
1154 })?;
1155 out.push(self.cardinal_kw_int(&BigInt::from(d), reading, &eff_prefer)?);
1156 }
1157 Ok(out.join(""))
1158 }
1159
1160 /// [`Self::cents_as_hundredths`] with `reading`/`prefer` threaded — the
1161 /// kwargs twin, same closed 0..=99 domain, same re-divided-cents bug.
1162 fn cents_as_hundredths_kw(
1163 &self,
1164 right: &BigInt,
1165 reading: bool,
1166 prefer: &[String],
1167 ) -> Result<String> {
1168 let r = match right.to_u32() {
1169 Some(r) if r <= 99 => r,
1170 _ => unreachable!("JA cents out of the 0..=99 domain: {}", right),
1171 };
1172 let (precision, post) = if r % 10 == 0 { (1usize, r / 10) } else { (2usize, r) };
1173 let eff_prefer: Vec<String> = if prefer.is_empty() {
1174 vec!["れい".to_string()]
1175 } else {
1176 prefer.to_vec()
1177 };
1178 let mut out = self.cardinal_kw_int(&BigInt::zero(), reading, &eff_prefer)?;
1179 out.push_str(if reading { "てん" } else { "点" });
1180 for ch in format!("{:0>width$}", post, width = precision).chars() {
1181 let digit = BigInt::from(ch.to_digit(10).unwrap());
1182 out.push_str(&self.cardinal_kw_int(&digit, reading, &eff_prefer)?);
1183 }
1184 Ok(out)
1185 }
1186
1187 /// `_ordinal_suffix(reading, counter)`, kwargs-visible form.
1188 ///
1189 /// The genuine Python `NotImplementedError` (reading with a counter other
1190 /// than 番) is raised as a `Custom` builtins.NotImplementedError rather
1191 /// than `N2WError::NotImplemented`: the latter is this codebase's "hook
1192 /// not ported" marker and the two deserve distinct spellings even though
1193 /// they map to the same Python class at the boundary. Note the wrapper's
1194 /// `except NotImplementedError` cannot tell them apart, so it still falls
1195 /// back to Python for these rows — which re-raises the identical error,
1196 /// keeping the observable behaviour byte-for-byte.
1197 fn ordinal_suffix_kw(&self, reading: bool, counter: &str) -> Result<String> {
1198 if reading {
1199 if counter == "番" {
1200 Ok("ばんめ".to_string())
1201 } else {
1202 Err(N2WError::Custom {
1203 module: "builtins",
1204 class: "NotImplementedError",
1205 msg: format!("Reading not implemented for {}", counter),
1206 })
1207 }
1208 } else {
1209 Ok(format!("{}目", counter))
1210 }
1211 }
1212
1213 /// `to_year`'s era branch for an integer year, all reading modes.
1214 ///
1215 /// The kanji arm calls the fast-path `to_cardinal`: `prefer` cannot
1216 /// change a kanji cardinal for any nonzero value (the only kanji
1217 /// alternative is at 0, and splitnum emits the zero card only for the
1218 /// value 0 itself — era years are >= 1), so this is exactly Python's
1219 /// `to_cardinal(era_year, reading=False, prefer=prefer)`.
1220 fn year_era_int(&self, year: &BigInt, reading: &JaReading, prefer: &[String]) -> Result<String> {
1221 let min_year = ERA_START[0].0; // 645
1222 if *year < BigInt::from(min_year) {
1223 return Err(N2WError::Value(format!(
1224 "Can't convert years less than {} to era",
1225 min_year
1226 )));
1227 }
1228 let idx = era_index(&|s| year.cmp(&BigInt::from(s)), prefer);
1229 let era = ERA_START[idx];
1230 let era_year = year - BigInt::from(era.0) + BigInt::one();
1231 match reading {
1232 // `reading == "arabic"`: kanji era name, Arabic era year, no
1233 // 元/がん special case.
1234 JaReading::Arabic => Ok(format!("{}{}年", era.1, era_year)),
1235 JaReading::Kana => {
1236 let mut words = if era_year.is_one() {
1237 "がん".to_string()
1238 } else {
1239 self.cardinal_kw_int(&era_year, true, prefer)?
1240 };
1241 // …きゅう (3 chars) -> …く for years ending in 9.
1242 if &era_year % BigInt::from(10) == BigInt::from(9) {
1243 words = format!("{}く", chop_last3(&words));
1244 }
1245 Ok(format!("{}{}ねん", era.2, words))
1246 }
1247 JaReading::Off => {
1248 let words = if era_year.is_one() {
1249 "元".to_string()
1250 } else {
1251 self.to_cardinal(&era_year)?
1252 };
1253 Ok(format!("{}{}年", era.1, words))
1254 }
1255 }
1256 }
1257}
1258
1259impl Lang for LangJa {
1260 /// This language's own `to_currency(currency=...)` default,
1261 /// read from the live Python signature. Only 44 of 156 use EUR.
1262 fn default_currency(&self) -> &str {
1263 "JPY"
1264 }
1265
1266 /// This language's own `to_currency(separator=...)` default,
1267 /// read from the live Python signature. Base's is ",", but only
1268 /// 36 of 149 languages actually use it — most default to " " or a
1269 /// conjunction, so inheriting Base's comma silently corrupts them.
1270 fn default_separator(&self) -> &str {
1271 ""
1272 }
1273
1274 fn cards(&self) -> &Cards {
1275 &self.cards
1276 }
1277
1278 fn maxval(&self) -> &BigInt {
1279 &self.maxval
1280 }
1281
1282 fn negword(&self) -> &str {
1283 // NB: no trailing space, and to_cardinal below must not add one.
1284 "マイナス"
1285 }
1286
1287 fn pointword(&self) -> &str {
1288 // pointword = ("点", "てん"); reading=False selects index 0.
1289 // Float paths are out of scope; carried for completeness.
1290 "点"
1291 }
1292
1293 fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
1294 let (ltext, lnum) = l;
1295 let (rtext, rnum) = r;
1296 let myriad = BigInt::from(10000);
1297
1298 if lnum.is_one() && rnum < &myriad {
1299 // "ignore lpair if lnum is 1 and rnum is less than 10000"
1300 // -> 1000 renders "千", but 10000 renders "一万".
1301 (rtext.to_string(), rnum.clone())
1302 } else if lnum > rnum {
1303 // rnum is added to lnum
1304 (format!("{}{}", ltext, rtext), lnum + rnum)
1305 } else if lnum < rnum {
1306 // rnum is multiplied by lnum. Python delegates to
1307 // rendaku_merge_pairs, whose every rule is gated on hiragana
1308 // literals and so cannot fire on the kanji path; this is its
1309 // fall-through. See module docs.
1310 (format!("{}{}", ltext, rtext), lnum * rnum)
1311 } else {
1312 // lnum == rnum: Python falls off the end of `merge` and returns
1313 // None, which `clean` would then spin on forever. Unreachable:
1314 // splitnum only ever hands `clean` a (div, elem) pair with
1315 // div < elem (elem is the largest card <= value, and adjacent
1316 // cards differ by a factor of at least 10), or an
1317 // (accumulated, mod) pair with mod < elem <= accumulated.
1318 // Confirmed by instrumenting the Python original over 12,060
1319 // values: zero hits.
1320 unreachable!(
1321 "JA merge: lnum == rnum ({}) is unreachable; Python returns None here",
1322 lnum
1323 )
1324 }
1325 }
1326
1327 /// `Num2Word_JA.to_cardinal`. Identical to the base implementation except
1328 /// the negative prefix is `self.negword` verbatim (no strip + space).
1329 fn to_cardinal(&self, value: &BigInt) -> Result<String> {
1330 let mut out = String::new();
1331 let mut v = value.clone();
1332 if v.is_negative() {
1333 v = v.abs();
1334 out = self.negword().to_string();
1335 }
1336
1337 if &v >= self.maxval() {
1338 return Err(N2WError::Overflow(format!(
1339 "abs({}) must be less than {}.",
1340 v,
1341 self.maxval()
1342 )));
1343 }
1344
1345 // splitnum cannot return None here: v >= 0 and cards contains 0, so
1346 // the scan always finds a card <= v. Mapped to Overflow to match the
1347 // base engine's handling rather than panicking.
1348 let tree = splitnum(self, &v).ok_or_else(|| {
1349 N2WError::Overflow(format!("abs({}) must be less than {}.", v, self.maxval()))
1350 })?;
1351 let words = match clean(self, tree) {
1352 Node::Leaf(t, _) => t,
1353 Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
1354 };
1355 // is_title is False for JA, so title() is the identity; kept for parity.
1356 Ok(self.title(&format!("{}{}", out, words)))
1357 }
1358
1359 fn to_ordinal(&self, value: &BigInt) -> Result<String> {
1360 self.verify_ordinal(value)?;
1361 let base = self.to_cardinal(value)?;
1362 Ok(format!("{}{}", base, self.ordinal_suffix()))
1363 }
1364
1365 /// Note: no `verify_ordinal`, and no `to_cardinal`. Python interpolates the
1366 /// raw value with `"%s%s"`, so negatives survive as "-1番目" and there is
1367 /// no MAXVAL ceiling.
1368 fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
1369 Ok(format!("{}{}", value, self.ordinal_suffix()))
1370 }
1371
1372 /// `to_year(val, era=True, reading=False, prefer=None)` — the era
1373 /// calendar at its defaults. The Gregorian (`era=False`), kana and
1374 /// `"arabic"` branches, and the `prefer` backtrack in the search, are
1375 /// reachable only through [`Lang::to_year_kw`] below; the search itself
1376 /// lives in [`era_index`]. Termination and in-range `mid` were verified
1377 /// against the interpreter for every year in 645..=12000.
1378 fn to_year(&self, value: &BigInt) -> Result<String> {
1379 self.year_era_int(value, &JaReading::Off, &[])
1380 }
1381
1382 // ---- float / Decimal entries -----------------------------------------
1383
1384 /// `to_ordinal(float/Decimal)`: `Num2Word_Base.verify_ordinal` runs on
1385 /// the raw value — non-whole raises the float TypeError, whole-negative
1386 /// the negative one, and a whole non-negative value takes the integer
1387 /// cardinal + 番目. `-0.0` (float or Decimal) passes BOTH checks
1388 /// (`int(-0.0) == -0.0` and `abs(-0.0) == -0.0`), so it is 零番目, not
1389 /// an error — which is why this must not use the sign-bit-aware
1390 /// `FloatValue::is_negative`.
1391 fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
1392 match value.as_whole_int() {
1393 Option::None => Err(N2WError::Type(format!(
1394 "Cannot treat float {} as ordinal.",
1395 py_value_str(value)
1396 ))),
1397 Some(i) => {
1398 if i.is_negative() {
1399 return Err(N2WError::Type(format!(
1400 "Cannot treat negative num {} as ordinal.",
1401 py_value_str(value)
1402 )));
1403 }
1404 Ok(format!("{}{}", self.to_cardinal(&i)?, self.ordinal_suffix()))
1405 }
1406 }
1407 }
1408
1409 /// `to_ordinal_num(float/Decimal)`: `"%s%s" % (value, "番目")` — the raw
1410 /// `str(value)`, no verify_ordinal, no cardinal. `"-0.0番目"`,
1411 /// `"1e+16番目"` and `"5.00番目"` are all real.
1412 fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
1413 Ok(format!("{}{}", repr_str, self.ordinal_suffix()))
1414 }
1415
1416 /// `to_year(float/Decimal)`: the era search runs on the raw number, so
1417 /// everything below 645 — negatives, ±0.0, fractions — raises the same
1418 /// ValueError as an integer year would. `era_year = year - start + 1`
1419 /// stays in the value's own arithmetic: two chained f64 ops for a float
1420 /// (`to_year(1e16)` really is 令和…七千九百八十年, the double-rounded
1421 /// `1e16 - 2019 + 1`; `1e20 - 2018` rounds back to 1e20 → 令和一垓年),
1422 /// exact BigDecimal for a Decimal (`1E+20` → …七千九百八十二年). A whole
1423 /// era year takes the integer cardinal; a fractional one (year 1000.5)
1424 /// goes through JA's float grammar, matching `to_cardinal`'s own
1425 /// routing.
1426 fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
1427 let min_year = ERA_START[0].0; // 645
1428 let too_early = || {
1429 N2WError::Value(format!("Can't convert years less than {} to era", min_year))
1430 };
1431 match value {
1432 FloatValue::Float { value: f, .. } => {
1433 let f = *f;
1434 if f < min_year as f64 {
1435 return Err(too_early());
1436 }
1437 // NaN/±inf never reach the float entries (the dispatcher
1438 // keeps them on the Python side), so partial_cmp is total.
1439 let idx = era_index(
1440 &|s| f.partial_cmp(&(s as f64)).expect("finite year"),
1441 &[],
1442 );
1443 let era = ERA_START[idx];
1444 // Python evaluates `year - era[0] + 1` left to right in f64.
1445 let era_year = f - era.0 as f64 + 1.0;
1446 let words = if era_year == 1.0 {
1447 "元".to_string()
1448 } else {
1449 let fv = FloatValue::Float {
1450 value: era_year,
1451 precision: float_repr_precision(era_year),
1452 };
1453 match fv.as_whole_int() {
1454 Some(i) => self.to_cardinal(&i)?,
1455 Option::None => self.to_cardinal_float(&fv, None)?,
1456 }
1457 };
1458 Ok(format!("{}{}年", era.1, words))
1459 }
1460 FloatValue::Decimal { value: d, .. } => {
1461 if *d < BigDecimal::from(min_year) {
1462 return Err(too_early());
1463 }
1464 let idx = era_index(&|s| d.cmp(&BigDecimal::from(s)), &[]);
1465 let era = ERA_START[idx];
1466 let era_year = d - BigDecimal::from(era.0) + BigDecimal::from(1);
1467 let words = if era_year == BigDecimal::from(1) {
1468 "元".to_string()
1469 } else {
1470 let fv = FloatValue::Decimal { value: era_year, precision: 0 };
1471 match fv.as_whole_int() {
1472 Some(i) => self.to_cardinal(&i)?,
1473 // JA's to_cardinal_float float-casts the Decimal and
1474 // recomputes the precision from the cast double, so
1475 // the carried 0 is never read.
1476 Option::None => self.to_cardinal_float(&fv, None)?,
1477 }
1478 };
1479 Ok(format!("{}{}年", era.1, words))
1480 }
1481 }
1482 }
1483
1484 // ---- grammatical kwargs -----------------------------------------------
1485
1486 /// `to_cardinal(value, reading=False, prefer=None)`.
1487 fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1488 if kw.is_empty() {
1489 return self.to_cardinal(value);
1490 }
1491 if !kw.only(&["reading", "prefer"]) {
1492 return Err(N2WError::Fallback("kwargs".into()));
1493 }
1494 let reading = parse_reading(kw);
1495 let prefer = parse_prefer(kw)?;
1496 self.cardinal_kw_int(value, reading.is_on(), &prefer)
1497 }
1498
1499 /// `to_ordinal(value, reading=False, prefer=None, counter="番")`.
1500 /// verify_ordinal runs FIRST — a negative raises TypeError even when
1501 /// the counter would raise NotImplementedError.
1502 fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1503 if kw.is_empty() {
1504 return self.to_ordinal(value);
1505 }
1506 if !kw.only(&["reading", "prefer", "counter"]) {
1507 return Err(N2WError::Fallback("kwargs".into()));
1508 }
1509 let reading = parse_reading(kw);
1510 let prefer = parse_prefer(kw)?;
1511 let counter = match kw.get("counter") {
1512 Option::None => "番",
1513 Some(KwVal::Str(s)) => s.as_str(),
1514 // A non-str counter TypeErrors in Python only after the
1515 // verify/cardinal work; defer the whole call.
1516 Some(_) => return Err(N2WError::Fallback("kwargs".into())),
1517 };
1518 self.verify_ordinal(value)?;
1519 let base = self.cardinal_kw_int(value, reading.is_on(), &prefer)?;
1520 let suffix = self.ordinal_suffix_kw(reading.is_on(), counter)?;
1521 Ok(format!("{}{}", base, suffix))
1522 }
1523
1524 /// `to_ordinal_num(value, reading=False, counter="番")` — note: NO
1525 /// `prefer` in this signature, and still no verify_ordinal ("-5個目").
1526 fn to_ordinal_num_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1527 if kw.is_empty() {
1528 return self.to_ordinal_num(value);
1529 }
1530 if !kw.only(&["reading", "counter"]) {
1531 return Err(N2WError::Fallback("kwargs".into()));
1532 }
1533 let reading = parse_reading(kw);
1534 let counter = match kw.get("counter") {
1535 Option::None => "番",
1536 Some(KwVal::Str(s)) => s.as_str(),
1537 Some(_) => return Err(N2WError::Fallback("kwargs".into())),
1538 };
1539 let suffix = self.ordinal_suffix_kw(reading.is_on(), counter)?;
1540 Ok(format!("{}{}", value, suffix))
1541 }
1542
1543 /// `to_year(val, suffix=None, longval=True, reading=False, prefer=None,
1544 /// era=True)`. `suffix` and `longval` are accepted and never read (any
1545 /// value passes). An explicit `era=None` is falsy — `if not era:` — and
1546 /// takes the Gregorian branch, unlike most None-means-default kwargs.
1547 fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
1548 if kw.is_empty() {
1549 return self.to_year(value);
1550 }
1551 if !kw.only(&["suffix", "longval", "reading", "prefer", "era"]) {
1552 return Err(N2WError::Fallback("kwargs".into()));
1553 }
1554 let reading = parse_reading(kw);
1555 let prefer = parse_prefer(kw)?;
1556 let era = match kw.get("era") {
1557 Option::None => true,
1558 Some(v) => kw_truthy(v),
1559 };
1560
1561 if !era {
1562 // Gregorian calendar: 紀元前/きげんぜん for BCE, and on the
1563 // reading path a year ending in 9 swaps …きゅう for …く.
1564 // The kana cardinal is used for ANY truthy reading, "arabic"
1565 // included — only the era branch string-compares it.
1566 let mut year = value.clone();
1567 let mut prefix = "";
1568 if year.is_negative() {
1569 year = -year;
1570 prefix = if reading.is_on() { "きげんぜん" } else { "紀元前" };
1571 }
1572 let mut words = self.cardinal_kw_int(&year, reading.is_on(), &prefer)?;
1573 if reading.is_on() && &year % BigInt::from(10) == BigInt::from(9) {
1574 words = format!("{}く", chop_last3(&words));
1575 }
1576 return Ok(format!(
1577 "{}{}{}",
1578 prefix,
1579 words,
1580 if reading.is_on() { "ねん" } else { "年" }
1581 ));
1582 }
1583
1584 self.year_era_int(value, &reading, &prefer)
1585 }
1586
1587 /// `to_cardinal(float/Decimal, reading=..., prefer=...)` — the full
1588 /// entry: `assert int(value) == value` sends a whole value down the
1589 /// integer path with the same kwargs.
1590 fn to_cardinal_float_kw(
1591 &self,
1592 value: &FloatValue,
1593 precision_override: Option<u32>,
1594 kw: &Kwargs,
1595 ) -> Result<String> {
1596 if kw.is_empty() {
1597 return self.cardinal_float_entry(value, precision_override);
1598 }
1599 if !kw.only(&["reading", "prefer"]) {
1600 return Err(N2WError::Fallback("kwargs".into()));
1601 }
1602 let reading = parse_reading(kw);
1603 let prefer = parse_prefer(kw)?;
1604 if let Some(i) = value.as_whole_int() {
1605 return self.cardinal_kw_int(&i, reading.is_on(), &prefer);
1606 }
1607 // precision_override dropped: JA's signature has no `precision=`.
1608 self.cardinal_float_impl(value, reading.is_on(), &prefer)
1609 }
1610
1611 /// `to_currency(..., reading=False, prefer=None)`. A structural copy of
1612 /// [`Lang::to_currency`] below with the two kwargs threaded — kept
1613 /// separate so the frozen non-kwargs path stays byte-identical. Slot 1
1614 /// of CURRENCY_FORMS (the hiragana reading) replaces slot 0, the
1615 /// cardinals go kana, but `minus_str` stays `self.negword` verbatim:
1616 /// `-5 JPY, reading=True` → "マイナスごえん".
1617 fn to_currency_kw(
1618 &self,
1619 val: &CurrencyValue,
1620 currency: &str,
1621 cents: bool,
1622 separator: Option<&str>,
1623 adjective: bool,
1624 kw: &Kwargs,
1625 ) -> Result<String> {
1626 if kw.is_empty() {
1627 return self.to_currency(val, currency, cents, separator, adjective);
1628 }
1629 if !kw.only(&["reading", "prefer"]) {
1630 return Err(N2WError::Fallback("kwargs".into()));
1631 }
1632 let reading = parse_reading(kw).is_on();
1633 let prefer = parse_prefer(kw)?;
1634 let ri = if reading { 1 } else { 0 };
1635
1636 // From here on this mirrors to_currency; see its comments.
1637 let decimal_val = match val {
1638 CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
1639 CurrencyValue::Decimal { value, .. } => value.clone(),
1640 };
1641 let scaled = &decimal_val * BigDecimal::from(100);
1642 let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
1643 let as_float = CurrencyValue::Decimal {
1644 value: decimal_val,
1645 has_decimal: false,
1646 is_float: true,
1647 };
1648 let (left, right, is_negative) = parse_currency_parts(&as_float, cents, false, 100);
1649 let right = right.as_bigint_and_exponent().0;
1650
1651 let forms = self.currency_forms.get(currency).ok_or_else(|| {
1652 N2WError::NotImplemented(format!(
1653 "Currency code \"{}\" not implemented for \"{}\"",
1654 currency,
1655 self.lang_name()
1656 ))
1657 })?;
1658 let mut cr1 = forms.unit.clone();
1659 let cr2 = &forms.subunit;
1660
1661 if adjective {
1662 if let Some(adj) = self.currency_adjective(currency) {
1663 cr1 = prefix_currency(adj, &cr1);
1664 }
1665 }
1666
1667 let minus_str = if is_negative { self.negword() } else { "" };
1668
1669 let (right_str, cr2_str) = if has_fractional_cents && !cr2.is_empty() {
1670 (
1671 self.cents_as_hundredths_kw(&right, reading, &prefer)?,
1672 cr2[ri].as_str(),
1673 )
1674 } else if !cr2.is_empty() && !right.is_zero() {
1675 (
1676 self.cardinal_kw_int(&right, reading, &prefer)?,
1677 cr2[ri].as_str(),
1678 )
1679 } else {
1680 (String::new(), "")
1681 };
1682
1683 Ok(format!(
1684 "{}{}{}{}{}",
1685 minus_str,
1686 self.cardinal_kw_int(&left, reading, &prefer)?,
1687 cr1[ri],
1688 right_str,
1689 cr2_str,
1690 ))
1691 }
1692
1693 /// `Num2Word_JA.to_cardinal_float`. Two deliberate divergences from
1694 /// `Num2Word_Base.to_cardinal_float` (i.e. from `default_to_cardinal_float`):
1695 ///
1696 /// 1. **It float-casts everything.** Python does
1697 /// `pre, post = self.float2tuple(float(value))` unconditionally — it
1698 /// never takes Base's Decimal-preserving arm. So a `Decimal` goes
1699 /// through the lossy binary path too, and the precision is recomputed
1700 /// from `repr(float(value))` rather than the Decimal's own exponent.
1701 /// This is load-bearing: `Decimal("1.10") -> "一点一"` (precision 1 from
1702 /// `repr(1.1)`, **not** "一点一零"), and
1703 /// `Decimal("98746251323029.99") -> "…点九八"` — the issue-#603 silent
1704 /// trillion-scale rounding, which is the *correct* JA output, not a bug
1705 /// to repair.
1706 /// 2. **It joins with `""`**, where Base joins with `" "`.
1707 ///
1708 /// The `negword.strip()` / `pointword[0]` / `value < 0 and pre == 0`
1709 /// details are identical to Base once resolved: JA's `negword` "マイナス"
1710 /// has no spaces to strip, its `pointword` slot 0 is "点", and `title()` is
1711 /// the identity (JA never sets `is_title`).
1712 ///
1713 /// JA's signature is `(value, reading=False, prefer=None)` — no `precision=`
1714 /// kwarg — so `precision_override` is dropped, matching the interpreter:
1715 /// `num2words(1.5, lang='ja', precision=3) == "一点五"`. The `prefer` default
1716 /// `["れい"]` is inert on the `reading=False` path (it is a hiragana reading,
1717 /// absent from every kanji card tuple), so digit 0 still resolves to "零".
1718 fn to_cardinal_float(
1719 &self,
1720 value: &FloatValue,
1721 _precision_override: Option<u32>,
1722 ) -> Result<String> {
1723 // `float(value)`: identity for a float, lossy binary cast for a
1724 // Decimal. `precision = abs(Decimal(str(f)).as_tuple().exponent)`.
1725 // For the `Float` arm the precision was already derived from
1726 // `repr(value)` on the Python side (the binding computed it the same
1727 // way JA would), so it is used as-is; only the `Decimal` arm needs the
1728 // recompute from the cast double.
1729 let (f, precision) = match value {
1730 FloatValue::Float { value, precision } => (*value, *precision),
1731 FloatValue::Decimal { value, .. } => {
1732 let f = value.to_f64().ok_or_else(|| {
1733 N2WError::Value(format!("cannot represent {} as f64", value))
1734 })?;
1735 (f, float_repr_precision(f))
1736 }
1737 };
1738
1739 let (pre, post) = float2tuple(&FloatValue::Float { value: f, precision });
1740
1741 // post = str(post); post = "0" * (precision - len(post)) + post
1742 let post_str = post.to_string();
1743 let post_str = format!(
1744 "{}{}",
1745 "0".repeat((precision as usize).saturating_sub(post_str.len())),
1746 post_str
1747 );
1748
1749 let mut out = vec![self.to_cardinal(&pre)?];
1750 // `if value < 0 and pre == 0`: the sign is otherwise lost because
1751 // int(-0.5) == 0. `self.negword.strip()` == "マイナス" (nothing to strip).
1752 if value.is_negative() && pre.is_zero() {
1753 out.insert(0, self.negword().to_string());
1754 }
1755 // `if self.precision:` — pointword slot 0 is "点"; title() is identity.
1756 if precision > 0 {
1757 out.push(self.title(self.pointword()));
1758 }
1759 for ch in post_str.chars().take(precision as usize) {
1760 let d = ch.to_digit(10).ok_or_else(|| {
1761 N2WError::Value(format!("non-digit {:?} in fractional part", ch))
1762 })?;
1763 out.push(self.to_cardinal(&BigInt::from(d))?);
1764 }
1765 // JA joins with "" (Base joins with " ").
1766 Ok(out.join(""))
1767 }
1768
1769 // ---- currency -------------------------------------------------------
1770 //
1771 // JA overrides `to_currency` outright; see the module docs for the full
1772 // list of ways it diverges from `Num2Word_Base.to_currency`. Everything
1773 // else on the currency surface is left at the trait default *because the
1774 // default is already right*:
1775 //
1776 // * `currency_precision` — JA's CURRENCY_PRECISION is `{}`, so
1777 // `.get(code, 100)` is a constant 100, exactly the trait default.
1778 // Overriding it would be a no-op at best and a JPY regression at worst.
1779 // * `currency_adjective` — CURRENCY_ADJECTIVES is `{}` -> always None.
1780 // * `pluralize` — unreachable; JA indexes `cr1[0]` itself.
1781 // * `money_verbose` / `cents_verbose` / `cents_terse` — JA's
1782 // `to_currency` calls none of them. `default_to_cheque` does call
1783 // `money_verbose`, whose default (`self.to_cardinal`) dispatches back
1784 // to JA's override, which is what Python's `_money_verbose` does too.
1785 // * `to_cheque` — inherited unchanged from Num2Word_Base.
1786 // * `cardinal_from_decimal` — JA's fractional-cents arm goes through its
1787 // own `to_cardinal_float`, not through `_cents_verbose`, so this hook
1788 // is never reached. Left alone.
1789
1790 fn lang_name(&self) -> &str {
1791 "Num2Word_JA"
1792 }
1793
1794 fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
1795 self.currency_forms.get(code)
1796 }
1797
1798 /// `Num2Word_JA.to_currency`.
1799 ///
1800 /// `separator` is accepted and never read — that is the Python signature,
1801 /// not an oversight here (`separator="XX"` changes nothing upstream). The
1802 /// output is a bare `"%s%s%s%s%s"` concatenation with no spaces at all.
1803 fn to_currency(
1804 &self,
1805 val: &CurrencyValue,
1806 currency: &str,
1807 cents: bool,
1808 _separator: Option<&str>,
1809 adjective: bool,
1810 ) -> Result<String> {
1811 // `decimal_val = Decimal(str(val))` — the *original* value, captured
1812 // before the int -> float coercion below.
1813 let decimal_val = match val {
1814 CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
1815 CurrencyValue::Decimal { value, .. } => value.clone(),
1816 };
1817
1818 // `has_fractional_cents = (decimal_val * 100) % 1 != 0`. The 100 is
1819 // JA's own literal, not currency_precision(currency) — JA hardcodes it
1820 // and ignores CURRENCY_PRECISION entirely. Testing "is the scaled
1821 // value an integer" is sign-agnostic, so it does not matter whether
1822 // with_scale(0) truncates or rounds.
1823 let scaled = &decimal_val * BigDecimal::from(100);
1824 let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
1825
1826 // `if isinstance(val, int): val = float(val)`. Load-bearing: it forces
1827 // parse_currency_parts down its non-int branch, the only one that
1828 // never reads is_int_with_cents. Passing the Int through instead would
1829 // let `cents=true` split it into units+cents and render 1000000 EUR as
1830 // "一万ユーロ" rather than "百万ユーロ".
1831 //
1832 // has_decimal is irrelevant here — JA has no `has_decimal` guard, which
1833 // is why the float 1.0 prints no cents, unlike Base.
1834 let as_float = CurrencyValue::Decimal {
1835 value: decimal_val,
1836 has_decimal: false,
1837 is_float: true,
1838 };
1839
1840 // is_int_with_cents=cents mirrors the Python call but is inert: the
1841 // value is a float by now. divisor=100 / keep_precision=false are
1842 // parse_currency_parts' own defaults, which JA never overrides — so
1843 // `right` is whole cents, 0..=99, on every path.
1844 let (left, right, is_negative) = parse_currency_parts(&as_float, cents, false, 100);
1845 let right = right.as_bigint_and_exponent().0;
1846
1847 // Python looks the currency up *after* parsing, inside a try/except
1848 // KeyError. Order is only observable when parsing itself raises, which
1849 // it cannot here; kept anyway.
1850 let forms = self.currency_forms.get(currency).ok_or_else(|| {
1851 N2WError::NotImplemented(format!(
1852 "Currency code \"{}\" not implemented for \"{}\"",
1853 currency,
1854 self.lang_name()
1855 ))
1856 })?;
1857 let mut cr1 = forms.unit.clone();
1858 let cr2 = &forms.subunit;
1859 // `if (cents or abs(val) != left) and not cr2: raise ValueError(...)`
1860 // is unreachable — every cr2 in JA's table is a non-empty tuple — so
1861 // no ValueError arm is modelled. Note it sits *inside* the try that
1862 // only catches KeyError, so it would escape as a real ValueError.
1863
1864 // Dead for JA (CURRENCY_ADJECTIVES is empty, so currency_adjective is
1865 // always None), but ported because it is in the source.
1866 if adjective {
1867 if let Some(adj) = self.currency_adjective(currency) {
1868 cr1 = prefix_currency(adj, &cr1);
1869 }
1870 }
1871
1872 // `self.negword` verbatim — NOT Base's `"%s " % negword.strip()`.
1873 let minus_str = if is_negative { self.negword() } else { "" };
1874
1875 // cr1/cr2 slot 0 is the kanji: `cr1[1] if reading else cr1[0]`, and
1876 // reading is unreachable through the trait.
1877 let (right_str, cr2_str) = if has_fractional_cents && !cr2.is_empty() {
1878 // NB: cr2_str is unconditional on this arm — Python gates it on
1879 // `if cr2 else ""`, not on `right` — so 0.001 EUR still emits the
1880 // subunit: "零ユーロ零点零セント".
1881 (self.cents_as_hundredths(&right)?, cr2[0].as_str())
1882 } else if !cr2.is_empty() && !right.is_zero() {
1883 (self.to_cardinal(&right)?, cr2[0].as_str())
1884 } else {
1885 (String::new(), "")
1886 };
1887
1888 Ok(format!(
1889 "{}{}{}{}{}",
1890 minus_str,
1891 self.to_cardinal(&left)?,
1892 cr1[0],
1893 right_str,
1894 cr2_str,
1895 ))
1896 }
1897}
1898
1899#[cfg(test)]
1900mod tests {
1901 use super::*;
1902 use std::str::FromStr;
1903
1904 /// Drive a corpus row exactly as `num2words2-py`'s binding does, so the
1905 /// argument literal decides int-vs-float the way `repr(value)` did on the
1906 /// Python side: `100` is an int, `1.0` a float, and the two take different
1907 /// paths through `parse_currency_parts`. `cents=true` / `separator=None` /
1908 /// `adjective=false` mirror `bench/diff_test.py`; for JA all three are
1909 /// inert (see module docs and `inert_kwargs`).
1910 fn run_currency(arg: &str, code: &str) -> Result<String> {
1911 let is_int = !arg.contains('.') && !arg.to_lowercase().contains('e');
1912 let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
1913 LangJa::new().to_currency(&v, code, true, None, false)
1914 }
1915
1916 /// The corpus records failures by Python exception name only.
1917 fn err_name(e: &N2WError) -> &'static str {
1918 match e {
1919 N2WError::NotImplemented(_) => "NotImplementedError",
1920 N2WError::Overflow(_) => "OverflowError",
1921 N2WError::Type(_) => "TypeError",
1922 N2WError::Value(_) => "ValueError",
1923 N2WError::Index(_) => "IndexError",
1924 N2WError::Key(_) => "KeyError",
1925 _ => "OtherError",
1926 }
1927 }
1928
1929 /// Every `ja` currency row in the frozen corpus, verbatim.
1930 #[test]
1931 fn corpus_currency() {
1932 let cases: &[(&str, &str, std::result::Result<&str, &str>)] = &[
1933 ("0", "EUR", Ok("零ユーロ")),
1934 ("1", "EUR", Ok("一ユーロ")),
1935 ("2", "EUR", Ok("二ユーロ")),
1936 ("100", "EUR", Ok("百ユーロ")),
1937 ("12.34", "EUR", Ok("十二ユーロ三十四セント")),
1938 ("0.01", "EUR", Ok("零ユーロ一セント")),
1939 ("1.0", "EUR", Ok("一ユーロ")),
1940 ("99.99", "EUR", Ok("九十九ユーロ九十九セント")),
1941 ("1234.56", "EUR", Ok("千二百三十四ユーロ五十六セント")),
1942 ("-12.34", "EUR", Ok("マイナス十二ユーロ三十四セント")),
1943 ("1000000", "EUR", Ok("百万ユーロ")),
1944 ("0.5", "EUR", Ok("零ユーロ五十セント")),
1945 ("0", "USD", Ok("零ドル")),
1946 ("1", "USD", Ok("一ドル")),
1947 ("2", "USD", Ok("二ドル")),
1948 ("100", "USD", Ok("百ドル")),
1949 ("12.34", "USD", Ok("十二ドル三十四セント")),
1950 ("0.01", "USD", Ok("零ドル一セント")),
1951 ("1.0", "USD", Ok("一ドル")),
1952 ("99.99", "USD", Ok("九十九ドル九十九セント")),
1953 ("1234.56", "USD", Ok("千二百三十四ドル五十六セント")),
1954 ("-12.34", "USD", Ok("マイナス十二ドル三十四セント")),
1955 ("1000000", "USD", Ok("百万ドル")),
1956 ("0.5", "USD", Ok("零ドル五十セント")),
1957 ("0", "GBP", Ok("零ポンド")),
1958 ("1", "GBP", Ok("一ポンド")),
1959 ("2", "GBP", Ok("二ポンド")),
1960 ("100", "GBP", Ok("百ポンド")),
1961 ("12.34", "GBP", Ok("十二ポンド三十四ペンス")),
1962 ("0.01", "GBP", Ok("零ポンド一ペンス")),
1963 ("1.0", "GBP", Ok("一ポンド")),
1964 ("99.99", "GBP", Ok("九十九ポンド九十九ペンス")),
1965 ("1234.56", "GBP", Ok("千二百三十四ポンド五十六ペンス")),
1966 ("-12.34", "GBP", Ok("マイナス十二ポンド三十四ペンス")),
1967 ("1000000", "GBP", Ok("百万ポンド")),
1968 ("0.5", "GBP", Ok("零ポンド五十ペンス")),
1969 ("0", "JPY", Ok("零円")),
1970 ("1", "JPY", Ok("一円")),
1971 ("2", "JPY", Ok("二円")),
1972 ("100", "JPY", Ok("百円")),
1973 ("12.34", "JPY", Ok("十二円三十四銭")),
1974 ("0.01", "JPY", Ok("零円一銭")),
1975 ("1.0", "JPY", Ok("一円")),
1976 ("99.99", "JPY", Ok("九十九円九十九銭")),
1977 ("1234.56", "JPY", Ok("千二百三十四円五十六銭")),
1978 ("-12.34", "JPY", Ok("マイナス十二円三十四銭")),
1979 ("1000000", "JPY", Ok("百万円")),
1980 ("0.5", "JPY", Ok("零円五十銭")),
1981 ("0", "KWD", Err("NotImplementedError")),
1982 ("1", "KWD", Err("NotImplementedError")),
1983 ("2", "KWD", Err("NotImplementedError")),
1984 ("100", "KWD", Err("NotImplementedError")),
1985 ("12.34", "KWD", Err("NotImplementedError")),
1986 ("0.01", "KWD", Err("NotImplementedError")),
1987 ("1.0", "KWD", Err("NotImplementedError")),
1988 ("99.99", "KWD", Err("NotImplementedError")),
1989 ("1234.56", "KWD", Err("NotImplementedError")),
1990 ("-12.34", "KWD", Err("NotImplementedError")),
1991 ("1000000", "KWD", Err("NotImplementedError")),
1992 ("0.5", "KWD", Err("NotImplementedError")),
1993 ("0", "BHD", Err("NotImplementedError")),
1994 ("1", "BHD", Err("NotImplementedError")),
1995 ("2", "BHD", Err("NotImplementedError")),
1996 ("100", "BHD", Err("NotImplementedError")),
1997 ("12.34", "BHD", Err("NotImplementedError")),
1998 ("0.01", "BHD", Err("NotImplementedError")),
1999 ("1.0", "BHD", Err("NotImplementedError")),
2000 ("99.99", "BHD", Err("NotImplementedError")),
2001 ("1234.56", "BHD", Err("NotImplementedError")),
2002 ("-12.34", "BHD", Err("NotImplementedError")),
2003 ("1000000", "BHD", Err("NotImplementedError")),
2004 ("0.5", "BHD", Err("NotImplementedError")),
2005 ("0", "INR", Err("NotImplementedError")),
2006 ("1", "INR", Err("NotImplementedError")),
2007 ("2", "INR", Err("NotImplementedError")),
2008 ("100", "INR", Err("NotImplementedError")),
2009 ("12.34", "INR", Err("NotImplementedError")),
2010 ("0.01", "INR", Err("NotImplementedError")),
2011 ("1.0", "INR", Err("NotImplementedError")),
2012 ("99.99", "INR", Err("NotImplementedError")),
2013 ("1234.56", "INR", Err("NotImplementedError")),
2014 ("-12.34", "INR", Err("NotImplementedError")),
2015 ("1000000", "INR", Err("NotImplementedError")),
2016 ("0.5", "INR", Err("NotImplementedError")),
2017 ("0", "CNY", Ok("零元")),
2018 ("1", "CNY", Ok("一元")),
2019 ("2", "CNY", Ok("二元")),
2020 ("100", "CNY", Ok("百元")),
2021 ("12.34", "CNY", Ok("十二元三十四分")),
2022 ("0.01", "CNY", Ok("零元一分")),
2023 ("1.0", "CNY", Ok("一元")),
2024 ("99.99", "CNY", Ok("九十九元九十九分")),
2025 ("1234.56", "CNY", Ok("千二百三十四元五十六分")),
2026 ("-12.34", "CNY", Ok("マイナス十二元三十四分")),
2027 ("1000000", "CNY", Ok("百万元")),
2028 ("0.5", "CNY", Ok("零元五十分")),
2029 ("0", "CHF", Err("NotImplementedError")),
2030 ("1", "CHF", Err("NotImplementedError")),
2031 ("2", "CHF", Err("NotImplementedError")),
2032 ("100", "CHF", Err("NotImplementedError")),
2033 ("12.34", "CHF", Err("NotImplementedError")),
2034 ("0.01", "CHF", Err("NotImplementedError")),
2035 ("1.0", "CHF", Err("NotImplementedError")),
2036 ("99.99", "CHF", Err("NotImplementedError")),
2037 ("1234.56", "CHF", Err("NotImplementedError")),
2038 ("-12.34", "CHF", Err("NotImplementedError")),
2039 ("1000000", "CHF", Err("NotImplementedError")),
2040 ("0.5", "CHF", Err("NotImplementedError")),
2041 ];
2042 for (arg, code, want) in cases {
2043 let got = run_currency(arg, code);
2044 match (want, &got) {
2045 (Ok(w), Ok(g)) => assert_eq!(g, w, "currency:{} arg={}", code, arg),
2046 (Err(w), Err(g)) => assert_eq!(&err_name(g), w, "currency:{} arg={}", code, arg),
2047 _ => panic!("currency:{} arg={}: want {:?}, got {:?}", code, arg, want, got),
2048 }
2049 }
2050 }
2051
2052 /// Every `ja` cheque row in the frozen corpus, verbatim. JPY's expected
2053 /// output ends in hiragana "えん" — Base's `cr1[-1]` "plural" reaching
2054 /// into JA's reading slot. See the module docs.
2055 #[test]
2056 fn corpus_cheque() {
2057 let cases: &[(&str, &str, std::result::Result<&str, &str>)] = &[
2058 ("1234.56", "EUR", Ok("千二百三十四 AND 56/100 ユーロ")),
2059 ("1234.56", "USD", Ok("千二百三十四 AND 56/100 ドル")),
2060 ("1234.56", "GBP", Ok("千二百三十四 AND 56/100 ポンド")),
2061 ("1234.56", "JPY", Ok("千二百三十四 AND 56/100 えん")),
2062 ("1234.56", "KWD", Err("NotImplementedError")),
2063 ("1234.56", "BHD", Err("NotImplementedError")),
2064 ("1234.56", "INR", Err("NotImplementedError")),
2065 ("1234.56", "CNY", Ok("千二百三十四 AND 56/100 元")),
2066 ("1234.56", "CHF", Err("NotImplementedError")),
2067 ];
2068 for (arg, code, want) in cases {
2069 let d = BigDecimal::from_str(arg).unwrap();
2070 let got = LangJa::new().to_cheque(&d, code);
2071 match (want, &got) {
2072 (Ok(w), Ok(g)) => assert_eq!(g, w, "cheque:{} arg={}", code, arg),
2073 (Err(w), Err(g)) => assert_eq!(&err_name(g), w, "cheque:{} arg={}", code, arg),
2074 _ => panic!("cheque:{} arg={}: want {:?}, got {:?}", code, arg, want, got),
2075 }
2076 }
2077 }
2078
2079 /// The exact NotImplementedError text, which the corpus only records by
2080 /// type. Python: 'Currency code "%s" not implemented for "%s"'.
2081 #[test]
2082 fn missing_currency_message() {
2083 match run_currency("1.0", "KWD") {
2084 Err(N2WError::NotImplemented(m)) => {
2085 assert_eq!(m, "Currency code \"KWD\" not implemented for \"Num2Word_JA\"")
2086 }
2087 other => panic!("expected NotImplemented, got {:?}", other),
2088 }
2089 }
2090
2091 /// `cents`, `separator` and `adjective` are all dead in JA's to_currency.
2092 /// Load-bearing: the corpus rows were generated with JA's own
2093 /// `cents=False` default, but the diff harness passes `cents=true`. If
2094 /// `cents` ever went live, `1000000` would split into units+cents and
2095 /// render "一万ユーロ" instead of "百万ユーロ".
2096 #[test]
2097 fn inert_kwargs() {
2098 let l = LangJa::new();
2099 for arg in ["0", "1", "2", "100", "1000000", "12.34", "0.01", "1.0", "99.99",
2100 "1234.56", "-12.34", "0.5"] {
2101 let is_int = !arg.contains('.');
2102 let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
2103 for code in ["JPY", "EUR", "USD", "GBP", "CNY"] {
2104 let base = l.to_currency(&v, code, false, None, false).unwrap();
2105 assert_eq!(l.to_currency(&v, code, true, None, false).unwrap(), base,
2106 "cents= changed {} {}", code, arg);
2107 assert_eq!(l.to_currency(&v, code, false, Some("XX"), false).unwrap(), base,
2108 "separator= changed {} {}", code, arg);
2109 assert_eq!(l.to_currency(&v, code, false, None, true).unwrap(), base,
2110 "adjective= changed {} {}", code, arg);
2111 }
2112 }
2113 }
2114
2115 /// Build the `FloatValue` a genuine float takes, deriving precision the
2116 /// same way the py binding does (`abs(Decimal(repr(f)).as_tuple().exponent)`).
2117 fn float_val(f: f64) -> FloatValue {
2118 FloatValue::Float { value: f, precision: float_repr_precision(f) }
2119 }
2120
2121 /// Every non-integer `ja` float `cardinal` row in the frozen corpus. The
2122 /// integer-valued floats (`0.0`, `1.0`) route to the integer path in JA's
2123 /// `to_cardinal` (`int(value) == value`), never reaching `to_cardinal_float`.
2124 #[test]
2125 fn corpus_cardinal_float() {
2126 let cases: &[(f64, &str)] = &[
2127 (0.5, "零点五"),
2128 (1.5, "一点五"),
2129 (2.25, "二点二五"),
2130 (3.14, "三点一四"),
2131 (0.01, "零点零一"),
2132 (0.1, "零点一"),
2133 (0.99, "零点九九"),
2134 (1.01, "一点零一"),
2135 (12.34, "十二点三四"),
2136 (99.99, "九十九点九九"),
2137 (100.5, "百点五"),
2138 (1234.56, "千二百三十四点五六"),
2139 (-0.5, "マイナス零点五"),
2140 (-1.5, "マイナス一点五"),
2141 (-12.34, "マイナス十二点三四"),
2142 (1.005, "一点零零五"), // f64 artefact: 0.005*1000 -> 5.000..4
2143 (2.675, "二点六七五"), // f64 artefact: 0.675*1000 -> 674.9999..8 rescued to 675
2144 ];
2145 for (f, want) in cases {
2146 assert_eq!(
2147 LangJa::new().to_cardinal_float(&float_val(*f), None).unwrap(),
2148 *want,
2149 "cardinal float arg={}",
2150 f
2151 );
2152 }
2153 }
2154
2155 /// Every `ja` `cardinal_dec` (Decimal input) row in the frozen corpus. JA
2156 /// float-casts the Decimal, so precision comes from `repr(float)` — hence
2157 /// `1.10 -> "一点一"` (not "一点一零") and `98746251323029.99 -> "…点九八"`
2158 /// (the #603 rounding, correct for JA). The carried `precision` field is
2159 /// ignored by the override, so it is set to the Decimal's own exponent
2160 /// here only for realism.
2161 #[test]
2162 fn corpus_cardinal_dec() {
2163 let cases: &[(&str, u32, &str)] = &[
2164 ("0.01", 2, "零点零一"),
2165 ("1.10", 2, "一点一"),
2166 ("12.345", 3, "十二点三四五"),
2167 ("98746251323029.99", 2, "九十八兆七千四百六十二億五千百三十二万三千二十九点九八"),
2168 ("0.001", 3, "零点零零一"),
2169 ];
2170 for (s, prec, want) in cases {
2171 let v = FloatValue::Decimal {
2172 value: BigDecimal::from_str(s).unwrap(),
2173 precision: *prec,
2174 };
2175 assert_eq!(
2176 LangJa::new().to_cardinal_float(&v, None).unwrap(),
2177 *want,
2178 "cardinal_dec arg={}",
2179 s
2180 );
2181 }
2182 }
2183
2184 /// Extra float/Decimal cases beyond the frozen corpus, values taken from
2185 /// the live interpreter. Guards the f64-artefact heuristic (`1.239999999`
2186 /// must survive as nine 9s, not collapse to `1.24`), the large-value
2187 /// integer head, and the Decimal float-cast dropping trailing zeros
2188 /// (`Decimal("1.230000")` -> precision 2, not 6).
2189 #[test]
2190 fn interpreter_cross_check() {
2191 let l = LangJa::new();
2192 let floats: &[(f64, &str)] = &[
2193 (0.001, "零点零零一"),
2194 (1000000.5, "百万点五"),
2195 (123456.789, "十二万三千四百五十六点七八九"),
2196 (0.7071, "零点七零七一"),
2197 (1.239999999, "一点二三九九九九九九九"),
2198 (-0.01, "マイナス零点零一"),
2199 (-0.999, "マイナス零点九九九"),
2200 (0.0009, "零点零零零九"),
2201 (1000000000.5, "十億点五"),
2202 (0.1234567, "零点一二三四五六七"),
2203 ];
2204 for (f, want) in floats {
2205 assert_eq!(l.to_cardinal_float(&float_val(*f), None).unwrap(), *want, "float {}", f);
2206 }
2207 let decs: &[(&str, &str)] = &[
2208 ("1.230000", "一点二三"),
2209 ("0.50", "零点五"),
2210 ("1000.00001", "千点零零零零一"),
2211 ("1.999999999999", "一点九九九九九九九九九九九九"),
2212 ];
2213 for (s, want) in decs {
2214 let v = FloatValue::Decimal { value: BigDecimal::from_str(s).unwrap(), precision: 0 };
2215 assert_eq!(l.to_cardinal_float(&v, None).unwrap(), *want, "dec {}", s);
2216 }
2217 }
2218
2219 /// The fractional-cents arm, which no corpus row reaches. Values taken
2220 /// from the Python interpreter. Note `2.567` -> "零点五七セント": JA
2221 /// re-divides the already-rounded whole cents by 100. That is the bug,
2222 /// reproduced deliberately.
2223 #[test]
2224 fn fractional_cents_arm() {
2225 for (arg, want) in [
2226 ("1.005", "一ユーロ零点零一セント"),
2227 ("0.001", "零ユーロ零点零セント"),
2228 ("1.011", "一ユーロ零点零一セント"),
2229 ("0.005", "零ユーロ零点零一セント"),
2230 ("2.567", "二ユーロ零点五七セント"),
2231 ("0.999", "一ユーロ零点零セント"),
2232 ("12.345", "十二ユーロ零点三五セント"),
2233 ("-1.005", "マイナス一ユーロ零点零一セント"),
2234 ("0.7071", "零ユーロ零点七一セント"),
2235 ] {
2236 assert_eq!(run_currency(arg, "EUR").unwrap(), want, "arg={}", arg);
2237 }
2238 }
2239}