fix(ru-RU,uk-UA,pl-PL,hr-HR): guard scale ceilings in the Slavic group#360
Conversation
Four Slavic languages, each with its own ceilings (verified individually): - ru-RU: cardinal/currency 10^33, ordinal 10^33 (SCALE_FORMS + ORDINAL_SCALES both 10 entries). TypeError crasher. - uk-UA: cardinal/currency 10^30, ordinal 10^15. TypeError crasher. - pl-PL: cardinal/currency 10^33 (PLURAL_FORMS object, keys 1-10), ordinal 10^24. Its decimal looked "clean" to the probe but is silently-WRONG above the ceiling (spelled via integerToWords past its scale words) — guarded by code analysis, like the az ordinal earlier. - hr-HR: cardinal/currency 10^30, ordinal 10^15. TypeError crasher. Entry-point pattern; ceilings derived from each table ((length + 1) * 3, units separate). Decimal guarded in all four (integer-spelled fractions). Verified per locale (well-formed or RangeError; integer/ordinal/decimal boundaries); full suite green (269). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds explicit magnitude ceiling guards for four Slavic locales to prevent crashes and silent mis-spellings when scale tables are exceeded.
Changes:
- Introduced per-locale MAX_* exponent/limit constants derived from scale tables.
- Added entry-point range checks for cardinal/currency/ordinal (and decimal significant digits) that throw a consistent “too large” error.
- Covered a Polish-specific “silently wrong” decimal path by guarding fractional integer spelling past the cardinal ceiling.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/uk-UA.js | Adds MAX_* ceilings and guards at toCardinal/toOrdinal/toCurrency entry points. |
| src/ru-RU.js | Adds MAX_* ceilings and guards at toCardinal/toOrdinal/toCurrency entry points. |
| src/pl-PL.js | Adds MAX_* ceilings computed from PLURAL_FORMS/ORDINAL_SCALES and guards, including decimal ceiling to prevent silent mis-spellings. |
| src/hr-HR.js | Adds MAX_* ceilings and guards at toCardinal/toOrdinal/toCurrency entry points. |
| const MAX_CARDINAL_EXPONENT = (Object.keys(PLURAL_FORMS).length + 1) * 3 | ||
| const MAX_CARDINAL = 10n ** BigInt(MAX_CARDINAL_EXPONENT) |
There was a problem hiding this comment.
Good call — fixed in bc8fc36. Now derives MAX_CARDINAL_EXPONENT from the max numeric key (Math.max(...Object.keys(PLURAL_FORMS).map(Number))) rather than .length, so it's robust to gaps or non-scale keys. Same 10^33 ceiling, boundary unchanged.
| // Supported magnitude ceilings (checked at the public entry points). Both | ||
| // tables are indexed [scaleIndex - 1] (units separate), so the ceiling is | ||
| // 10^((length + 1) * 3): cardinal/currency 10^30, ordinal 10^15. | ||
| const MAX_CARDINAL_EXPONENT = (SCALE_FORMS.length + 1) * 3 | ||
| const MAX_CARDINAL = 10n ** BigInt(MAX_CARDINAL_EXPONENT) | ||
| const MAX_ORDINAL_EXPONENT = (ORDINAL_SCALES.length + 1) * 3 | ||
| const MAX_ORDINAL = 10n ** BigInt(MAX_ORDINAL_EXPONENT) |
There was a problem hiding this comment.
Intentional, and a deliberate architecture decision earlier in this series. The contract is enforced by (1) composition — a shared helper each language calls (tooLargeError, already used here) plus the shared parse-* utils — and (2) the forthcoming fast-check gate, which verifies in-range → well-formed; out-of-range → RangeError across all 72 languages uniformly. We considered a wrapper/factory that owns the parse→guard→assemble flow and rejected it: it's template-method inheritance that takes the joining logic away from each self-contained language file (the deliberate design here). The one duplicated line — if (x >= MAX) throw tooLargeError(exp) — already composes the shared helper. A computeExponentFromScaleTable can't be shared cleanly either: the derivation genuinely differs per language (length*3, (length+1)*3, (2*length+2)*3 for long-scale -ard families, max-key for pl's object), because each indexes its scale table differently. So the per-language MAX_* block is the right home; the gate keeps them consistent by outcome. (Context: .github/instructions/scale-ceiling.instructions.md.)
Address Copilot review on #360: Object.keys(PLURAL_FORMS).length assumes contiguous numeric keys. Derive MAX_CARDINAL_EXPONENT from the max numeric key instead — robust to gaps or non-scale keys if PLURAL_FORMS is ever refactored. Same ceiling (10^33), boundary unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Twelfth in the fix-then-gate series. Four Slavic languages, each verified individually (their ceilings genuinely differ).
Notable: pl-PL decimal (another probe false-negative)
pl's decimal looked clean to the well-formedness probe even at 40 digits — but above 10³³ it's silently wrong (spelled via
integerToWordspast its scale words, dropping magnitude rather than emittingundefined), the same class as the az ordinal. Guarded by code analysis, not the probe's verdict.Fix
Entry-point pattern; ceilings derived from each table (
(length + 1) * 3, units separate; pl usesObject.keys(PLURAL_FORMS).length). Decimal guarded in all four (integer-spelled fractions). The 3 SCALE_FORMS/ORDINAL_SCALES tables were already module-scope.Verification
Per locale: well-formed string or
RangeErroracross±10⁴⁰; integer/ordinal/decimal boundaries confirmceil−1ok /ceilthrows. Existing fixtures green, lint clean, 269 tests.🤖 Generated with Claude Code