Skip to content

Intl.NumberFormat: fix percent formatRange scaling twice on approximately path - #36362

Open
robobun wants to merge 5 commits into
mainfrom
farm/7cfed714/intl-numberformat-percent-formatrange
Open

Intl.NumberFormat: fix percent formatRange scaling twice on approximately path#36362
robobun wants to merge 5 commits into
mainfrom
farm/7cfed714/intl-numberformat-percent-formatrange

Conversation

@robobun

@robobun robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

With style: "percent", Intl.NumberFormat.prototype.formatRange (and formatRangeToParts) applies the ×100 scale twice whenever the range collapses to a single approximate value (equal endpoints, or endpoints that round to the same formatted number):

for (const loc of ["en", "de", "ja"]) {
  const nf = new Intl.NumberFormat(loc, { style: "percent" });
  console.log(loc, nf.format(0.5), nf.formatRange(0.5, 0.5), nf.formatRange(0.5, 0.500001), nf.formatRange(0.5, 0.6));
}
// bun : en 50% ~5,000% ~5,000% 50% – 60%   |  de ≈5.000 %  |  ja 約5,000%
// node: en 50% ~50%    ~50%    50% – 60%   |  de ≈50 %     |  ja 約50%

Reproduces in every locale and through every input path (double, BigInt, string); compact notation shows the same ×100 (formatRange(15, 15)~150K%, Node ~1.5K%). Distinct ranges, style: "unit" / "currency" / "decimal" collapses, and macOS are all correct.

Cause

ICU-23110. NumberRangeFormatterImpl::format calls formatterImpl1.preProcess(data.quantity1, ...) (which applies the skeleton's scale/100), then on the UNUM_IDENTITY_FALLBACK_APPROXIMATELY path hands the already-scaled data.quantity1 to formatApproximately, which calls fApproximatelyFormatter.preProcess(data.quantity1, ...) again. The intervening resetExponent() only clears compact/scientific exponent state, not the multiplier, so 0.5 → 50 → 5000.

JSC builds the range formatter from the same skeleton as format() (" percent scale/100") and passes raw inputs to unumrf_formatDoubleRange / unumrf_formatDecimalRange, which is also what V8 does. Node is correct only because it ships ICU ≥ 78. Bun bundles ICU 75.1 on Linux/musl/Android/FreeBSD and 73.2 on Windows; macOS links Apple's libicucore, which already renders correctly.

Fix

oven-sh/WebKit#374 backports unicode-org/icu@757be359 (first released in ICU 78.1) as a build-time patch applied in every Dockerfile that builds ICU. The patch takes a copy of data.quantity1 before the first preProcess and feeds that copy to formatApproximately, so the approximately formatter starts from the original quantity.

This PR:

  • bumps WEBKIT_VERSION to that PR's preview build;
  • adds coverage in test/js/web/intl/intl.test.ts: for en/de/ja/ar, the numeric parts of formatRangeToParts(a, a) must equal formatToParts(a), and the collapsed range string must contain format(a). Covers double, BigInt, string, and compact-notation inputs, plus a distinct-range sanity check.

Verification

Built ICU 75.1 with and without the backport and drove unumrf_formatDoubleRange directly:

                        unpatched       patched
formatRange(0.5, 0.5)   ~5,000%         ~50%
formatRange(0.5, 0.6)   50% – 60%       50% – 60%
formatRange(15, 15)     ~150,000%       ~1,500%

The new test block:

  • fails on the current WebKit prebuilt (Expected to contain: "50%" / Received: "~5,000%", all four locales, BigInt/string/compact);
  • passes on Node (ICU 78.3) and on macOS (Apple ICU 74.2).

Note: the automated fail-before/fail-after gate does not apply here because the code change is a prebuilt-dependency bump (scripts/build/deps/webkit.ts), not src/**; the WebKit preview build is what makes the new tests pass. This PR's CI build step will fail until the oven-sh/WebKit#374 preview release (autobuild-preview-pr-374-9cfb1f57) is published; once it merges the WEBKIT_VERSION here will be updated to the resulting autobuild sha.


no test proof · iteration 3 · Platform-specific test-only change; deferring to CI.

…tely path

With style:"percent", formatRange(a, a) (and endpoints that round equal)
rendered "~5,000%" for 0.5 instead of "~50%": ICU's
NumberRangeFormatterImpl::formatApproximately re-runs preProcess on an
already-scaled quantity, applying scale/100 a second time (ICU-23110,
fixed upstream in ICU 78.1).

The fix is a build-time ICU patch applied in oven-sh/WebKit#374. This
commit bumps WEBKIT_VERSION to that PR's preview build and adds
coverage: for en/de/ja/ar, the numeric parts of
formatRangeToParts(a, a) must equal formatToParts(a), and the collapsed
range string must contain format(a). Covers double, BigInt, string, and
compact-notation inputs, plus a distinct-range sanity check.

macOS is unaffected (Apple's system ICU already renders correctly).
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

WebKit preview dependency

Layer / File(s) Summary
WebKit prebuilt version update
scripts/build/deps/webkit.ts
Updates WEBKIT_VERSION to an autobuild preview tag used for WebKit prebuilt downloads and cache identity.

Intl.NumberFormat regression coverage

Layer / File(s) Summary
NumberFormat range regression tests
test/js/web/intl/intl.test.ts
Adds percent-style formatRange and formatRangeToParts tests for equal, near-equal, and distinct values, including BigInt, string, and compact-notation inputs.

Possibly related PRs

Suggested reviewers: dylan-conway

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main fix to percent formatRange scaling in Intl.NumberFormat.
Description check ✅ Passed The PR description covers the change and includes a verification section, satisfying the template’s required info.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Status: waiting on oven-sh/WebKit#374 CI to publish autobuild-preview-pr-374-46c558e6. Until then this PR's build step fails at the WebKit prebuilt download (HTTP 404); that is expected and not a problem with the diff.

The initial WebKit run failed on Android/FreeBSD because ubuntu:24.04 does not ship patch and those images had no patch step before; oven-sh/WebKit@46c558e6 adds it.

Repro:

bun -e 'console.log(new Intl.NumberFormat("en",{style:"percent"}).formatRange(0.5,0.5))'
# bun: ~5,000%   node: ~50%

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:26 AM PT - Jul 29th, 2026

@autofix-ci[bot], your commit 8687fc2 has 2 failures in Build #85209 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36362

That installs a local version of the PR into your bun-36362 executable, so you can run:

bun-36362 --bun

The previous preview sha was superseded after installing `patch` in the
Android/FreeBSD Dockerfiles (ubuntu:24.04 does not ship it and those
images had no patch step before).
Comment thread test/js/web/intl/intl.test.ts Outdated
robobun and others added 2 commits July 29, 2026 12:16
expect({a, b, range}.range) reads the property before expect() sees the
wrapper, so the loop context never appears in the failure message.
Use toEqual with expect.stringContaining so the diff shows which (a, b)
pair produced the mismatch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build/deps/webkit.ts`:
- Line 6: Update the WEBKIT_VERSION constant to reference a currently published
merged WebKit SHA or release tag, replacing the unavailable
autobuild-preview-pr-374-46c558e6 value so the default prebuilt download
succeeds.

In `@test/js/web/intl/intl.test.ts`:
- Around line 107-108: Update the distinct-endpoint range assertion near
nf.formatRange(0.5, 0.6) to validate the structured output rather than substring
containment. Use formatRangeToParts() and compare its range-related parts with
formatToParts() results, or explicitly assert the expected startRange and
endRange parts; apply the same structural validation to the compact-notation
check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 26a9ebbe-8679-4ae1-8630-70f1554359c8

📥 Commits

Reviewing files that changed from the base of the PR and between a96934f and 8687fc2.

📒 Files selected for processing (2)
  • scripts/build/deps/webkit.ts
  • test/js/web/intl/intl.test.ts

Comment thread scripts/build/deps/webkit.ts
Comment thread test/js/web/intl/intl.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants