Extract PluralUtils from IString.js and fix plural rule evaluation bugs (Phase 1) - #509
Extract PluralUtils from IString.js and fix plural rule evaluation bugs (Phase 1)#509gouniLee wants to merge 8 commits into
Conversation
Move plural rule engine (plurals_default, loadPlurals, _fncs) out of IString.js into a standalone PluralUtils.js module. IString retains backward-compatible aliases for all three static members so no callers need to change. Also add docs/IString-Refactoring.md with the full split plan.
Correctness: - Fix w/t CLDR operands in calculateNumberDigits: trailing zeros were not stripped, making w identical to v and t identical to f Cleanup: - Introduce local `fn` alias for PluralUtils._fncs, eliminating 27 repetitions of the full qualified name throughout _fncs methods - Simplify loadPlurals locale resolution to a single nested ternary - Remove firstProp: defined but never called (dead code) - Inline matchRange into its two callers (inrange, notin) and remove the one-line wrapper - matchRangeContinuous: combine duplicate return true branches; use continue to skip undefined entries instead of wrapping in if - calculateNumberDigits: remove intermediate numberDigits object; return object literals directly from each branch - getValue: collapse string branch to single ternary return - eq/neq: remove (x ? true : false) / (x === y ? true : false) idiom - or/and: remove unnecessary intermediate result variable - Replace Object.prototype.toString.call checks with Array.isArray - Remove ilib.bind from loadPlurals callback (this was unused) Net result: 422 → 291 lines (-131)
Add unit tests for PluralUtils.js and fix several bugs surfaced while writing them. All bugs predate the extraction from IString.js and only affect the fallback path used when Intl.PluralRules is unavailable. calculateNumberDigits: - use the absolute value so operands match CLDR for negative numbers - read the full exponent, not just its first digit (e.g. 1e-21) - keep large integers intact instead of mangling them via parseInt matchRangeContinuous / inrange: - treat a 3+ value list (e.g. [11,71,91]) as a set, not a range, so values between the endpoints no longer match - handle nested/mixed range lists (e.g. [[3,10],[13,19]], [[3,4],9]) neq: - guard typeof(n) === 'object' for symmetry with eq Document the remaining two-value-list ambiguity ([2,4] as range vs set) in PluralUtils.js and its root cause in tools/cldr/genplurals.js. Verified against Intl.PluralRules for 214/215 locales (Scottish Gaelic is the known data-format limitation).
…perands
The CLDR operand fix in PluralUtils (using the absolute value of the
number) changes the plural category of negative temperatures when iLib's
own plural rules are used instead of Intl.PluralRules. For -16.6°C:
- be-BY: n mod 10 = 6 (in 5..9) -> "many" (градусаў)
- mt-MT: n mod 100 = 16 (in 11..19) -> "many" ("-il grad")
This path is only taken where Intl.PluralRules is unavailable (e.g. Node
10 with small-icu), so the tests only failed there. Update the expected
values for the non-Intl branches and add the standard nodejs/browser
CLDR-version split to the mt-MT test, which previously asserted a single
value. Intl-path expectations are unchanged.
| test.equal(str, "-16,666666666666668 градуса Цэльсія"); | ||
| } else { | ||
| test.equal(str, "-16,666666666666668 градусы Цэльсія"); | ||
| test.equal(str, "-16,666666666666668 градусаў Цэльсія"); |
There was a problem hiding this comment.
Why did this change? All you did is move the code to a new file, so the results should not be any different.
There was a problem hiding this comment.
The changes in this branch include not only splitting the PluralUtil.js file, but also improving the existing logic. In the process, a bug was also identified and fixed. The same conditional branch also exists in the browser case of the same test.
To explain this part in more detail:
PluralUtils.calculateNumberDigits was fixed to use Math.abs(number) before computing CLDR operands, as the spec defines operand n on the absolute value. With -16.666, the old code computed n mod 10 = -6 (no rule matched → fell through to few); the fixed code computes n mod 10 = 6, which falls in the 5..9 range → many → градусаў.
This only affects the non-Intl.PluralRules path (Node 10 / small-icu). Modern Node uses Intl.PluralRules directly and is unaffected.
| }); | ||
| var str = uf.format(m1); | ||
| test.equal(str, "-16.666666666666668 grad Celsius"); | ||
| var platform = ilib._getPlatform(); |
There was a problem hiding this comment.
This is for the same reason I mentioned in the comment above.
|
@ehoogerbeets Could you check this PR? I have left a response to your question. |
Checklist
ReleaseNoteshas been updated or is not needed.Node.jsandChrome Browser.Summary
Extracts the plural rule engine (
plurals_default,loadPlurals,_fncs) out ofIString.jsinto a standalonePluralUtils.jsmodule (Phase 1 of the IStringrefactoring plan), then refactors and fixes several correctness bugs surfaced while
adding unit tests.
IString.jsshrinks by ~383 lines; the extracted module is 378 lines. The publicAPI is unchanged —
IString.loadPlurals,IString._fncs, andIString.plurals_defaultare retained as backward-compatible aliases, so none of the38
require('./IString')callers need to change.What changed
Refactor
js/lib/PluralUtils.js;IStringkeeps aliases forall three static members.
fnalias (removes 27 repetitions of thequalified name), inline
matchRangeinto its callers, remove dead code(
firstProp),Array.isArrayinstead ofObject.prototype.toString.call, dropunused
ilib.bind. Net −131 lines in the engine itself.Bug fixes (all predate the extraction; all only affect the fallback path used when
Intl.PluralRulesis unavailable, e.g. Node 10 with small-icu)calculateNumberDigits: use the absolute value so CLDR operands are correct fornegative numbers; read the full exponent rather than just its first digit (e.g.
1e-21); keep large integers intact instead of mangling them throughparseInt;strip trailing zeros so the
w/toperands are no longer identical tov/f.matchRangeContinuous/inrange: treat a 3+ value list (e.g.[11,71,91]) as aset, not a range; handle nested/mixed range lists (e.g.
[[3,10],[13,19]],[[3,4],9]).neq: guardtypeof(n) === 'object'for symmetry witheq.Known limitation (documented, not fixed here)
2..4from a two-valueset
2,4— both collapse to[2,4]. The runtime assumes "range" (correct for e.g.Russian, wrong for Scottish Gaelic). Documented in
PluralUtils.jswith the rootcause in
tools/cldr/genplurals.js. A proper fix requires an unambiguous data formatand regenerating every locale's
plurals.json.Tests
js/test/root/testpluralutils.js— ~63 tests / 138 assertions coveringcalculateNumberDigits(negative, exponent, large-int), set-vs-range matching,nested/mixed lists, and several real locales (Breton, Russian, English). Passes in
all three build modes (dynamic, dynamicdata, assembled).
Intl.PluralRulesfor 214/215 locales over 0..200 (onlyScottish Gaelic differs — the documented data-format limitation).
testunitfmt_be_BY/testunitfmt_mt_MTtemperature-2 expectations for thenon-Intl branch, since the absolute-value fix changes the plural category of negative
temperatures on that path only (be-BY → "many"
градусаў; mt-MT → "many"-il grad).Intl-path expectations unchanged.
Notes for reviewers
IStringFmt.js) is planned separately andtracked in the refactoring doc.
modern Node/browsers (Intl path) is unaffected.