fix(intl): #5581 — add Intl.NumberFormat formatRange/formatRangeToParts#5771
Conversation
📝 WalkthroughWalkthroughAdds ChangesIntl.NumberFormat formatRange / formatRangeToParts
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
`Intl.NumberFormat.prototype.formatRange` and `formatRangeToParts` (the
Intl.NumberFormat-v3 methods) were missing, so every test262 case touching
them threw "formatRange is not a function".
Add both as non-bound prototype methods plus own this-based instance
properties (native Intl method dispatch resolves from own props, not the
static prototype). Using a this-based — rather than bound — closure means a
detached `nf.formatRange` reference loses `this` and the receiver guard
throws a TypeError, matching the spec's plain prototype method
(formatRange/invoked-as-func.js).
The range engine is a best-effort PartitionNumberRangePattern: undefined
endpoints are a TypeError and NaN endpoints a RangeError; mathematically
equal endpoints collapse to a single value; distinct endpoints that round
to the same string render approximate ("~"); otherwise the two renderings
join with an en dash, with parts tagged startRange/endRange/shared. The
exact ICU field-collapsing / locale range pattern is not reproduced, so
the three exact-output cases (formatRange/{en-US,pt-PT}, formatRangeToParts
/en-US) remain out of scope.
Split the generic `install_constructor` helper into a new `intl/install.rs`
child module so `intl.rs` stays under the 2000-line file-size gate.
Validated against the pinned test262 corpus: intl402/NumberFormat goes
from 83 -> 71 failing (12 fixed: invoked-as-func/length/name/prop-desc/
nan-arguments-throws/x-greater-than-y-not-throws for both methods), the new
failing set is a strict subset of the prior one (no regressions), and 0
compile failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5c5f7a9 to
d12d00d
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/perry-runtime/src/intl.rs (1)
1772-1775: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate this comment to match the instance shadowing behavior.
Line 1775 says these methods are installed “on the prototype only,” but lines 1075-1090 also install own non-bound instance properties for Perry dispatch. Keep the “not bound” warning, but remove the “prototype only” claim.
Proposed comment fix
- // and the `this_intl_object` guard throws, so they are installed on - // the prototype only — never as own bound instance functions. + // and the `this_intl_object` guard throws. Instances also shadow + // these with own non-bound closures for Perry's native dispatch, + // but never as own bound instance functions.🤖 Prompt for 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. In `@crates/perry-runtime/src/intl.rs` around lines 1772 - 1775, The comment near the Intl range-format methods is outdated because it claims they are installed “on the prototype only,” but the implementation in Intl.NumberFormat also adds own non-bound instance properties for Perry dispatch. Update the comment around the formatRange/formatRangeToParts setup to keep the detached-reference/this_intl_object warning while removing the “prototype only” wording and reflecting the instance-shadowing behavior.
🤖 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 `@crates/perry-runtime/src/intl/number_format.rs`:
- Around line 1723-1728: The early equality collapse in range_endpoints_equal
and the related fast path in the range formatting flow are incorrectly bypassing
the approximate form for equal endpoints. Update the range handling in
number_format.rs so formatRange and formatRangeToParts still go through the
approximate rendering path when the two endpoints are equal, ensuring
formatRangeToParts includes approximatelySign instead of returning a single
collapsed value.
---
Nitpick comments:
In `@crates/perry-runtime/src/intl.rs`:
- Around line 1772-1775: The comment near the Intl range-format methods is
outdated because it claims they are installed “on the prototype only,” but the
implementation in Intl.NumberFormat also adds own non-bound instance properties
for Perry dispatch. Update the comment around the formatRange/formatRangeToParts
setup to keep the detached-reference/this_intl_object warning while removing the
“prototype only” wording and reflecting the instance-shadowing behavior.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d516a2cc-33b6-409d-bd69-c5e35c6df3fd
📒 Files selected for processing (3)
crates/perry-runtime/src/intl.rscrates/perry-runtime/src/intl/install.rscrates/perry-runtime/src/intl/number_format.rs
| /// Two endpoints are "mathematically equal" for range-collapse purposes when | ||
| /// they are the same Number — including `+0`/`-0`, which compare equal — so the | ||
| /// range renders as a single value rather than the approximate (`~`) form. | ||
| fn range_endpoints_equal(x: f64, y: f64) -> bool { | ||
| x == y | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant file and inspect the surrounding range-formatting logic.
ast-grep outline crates/perry-runtime/src/intl/number_format.rs --view expanded || true
echo
echo '--- lines around 1723-1785 ---'
sed -n '1700,1805p' crates/perry-runtime/src/intl/number_format.rs | cat -n
echo
echo '--- search for range_endpoints_equal and approximate handling ---'
rg -n "range_endpoints_equal|approximatelySign|FormatApproximately|formatRange|formatRangeToParts|range_parts_to_js_array" crates/perry-runtime/src/intl/number_format.rsRepository: PerryTS/perry
Length of output: 10970
🌐 Web query:
ECMA-402 NumberFormat formatRange identical endpoints approximate FormatApproximately identical endpoints spec
💡 Result:
In the ECMA-402 specification, the Intl.NumberFormat.prototype.formatRange method handles cases where the start and end values of a range result in the same formatted string (either because the inputs are identical or because they round to the same value under the formatter's settings) by collapsing the range into a single value [1][2][3]. When this collapse occurs, the output may be prefixed with an "approximately equals" symbol (e.g., "~$3") [1][2][3]. The insertion of this symbol is locale-dependent and occurs even when startRange === endRange [1][4]. Technically, the specification defines this behavior through the following mechanisms: - Collapsing: If the start and end values resolve to the same formatted string, the range is condensed into a single representation [1][2]. - Approximately Sign: The approximately equals symbol (often ~) is added to signify that the value represents a range that has collapsed into a single point due to formatting constraints [5]. - formatRangeToParts: The corresponding formatRangeToParts method exposes this state explicitly, returning an array of parts where the first token may be an "approximatelySign" (type: "approximatelySign") and all other tokens are marked with source: "shared" [4]. This behavior was introduced as part of the Intl.NumberFormat V3 proposal to provide parity with Intl.DateTimeFormat.prototype.formatRange [5][6].
Citations:
- 1: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRange
- 2: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRange
- 3: https://github.com/mdn/content/blob/main/files/en-us/web/javascript/reference/global_objects/intl/numberformat/formatrange/index.md
- 4: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRangeToParts
- 5: https://tc39.es/proposal-intl-numberformat-v3/
- 6: Add Intl.NumberFormat formatRange tc39/ecma402#393
Keep equal endpoints on the approximate path. formatRange(3, 3) should still emit the approximate form, and formatRangeToParts(3, 3) should include approximatelySign; the early returns here skip both.
🤖 Prompt for 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.
In `@crates/perry-runtime/src/intl/number_format.rs` around lines 1723 - 1728, The
early equality collapse in range_endpoints_equal and the related fast path in
the range formatting flow are incorrectly bypassing the approximate form for
equal endpoints. Update the range handling in number_format.rs so formatRange
and formatRangeToParts still go through the approximate rendering path when the
two endpoints are equal, ensuring formatRangeToParts includes approximatelySign
instead of returning a single collapsed value.
…5773) Follow-up to #5771 (CodeRabbit). ECMA-402 PartitionNumberRangePattern renders the approximate ("~") form whenever the two endpoints format to the same string — including mathematically equal endpoints. The merged code special-cased equal endpoints to a plain single value, so `formatRange(3, 3)` returned "3" and `formatRangeToParts(3, 3)` omitted the `approximatelySign`, diverging from the spec and from Node: new Intl.NumberFormat("en-US").formatRange(3, 3) // "~3" new Intl.NumberFormat("en-US").formatRangeToParts(3, 3) // [{approximatelySign,~,shared},{integer,3,shared}] Drop the `range_endpoints_equal` fast path so equal endpoints flow through the existing `sx == sy` approximate branch. Verified byte-for-byte against node v26 and the pinned test262 corpus (intl402/NumberFormat formatRange /formatRangeToParts: 16 pass / 3 fail unchanged, 0 compile failures). Co-authored-by: Ralph <ralph@skelpo.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Intl.NumberFormat.prototype.formatRangeandformatRangeToParts(theIntl.NumberFormat-v3methods) were not implemented, so every test262 case touching them threwformatRange is not a function. This adds both.Approach
prop-desc/length/name) plus own this-based instance properties. Native Intl method dispatch in Perry resolves instance methods from own props, not the static prototype, so a prototype-only install isn't callable asnf.formatRange(...).nf.formatRangereference losesthisand the receiver guard throws aTypeError, matching the spec's plain (non-bound) prototype method —formatRange/invoked-as-func.js.PartitionNumberRangePattern:undefinedendpoints →TypeError,NaNendpoints →RangeError; mathematically-equal endpoints collapse to a single value; distinct endpoints that round to the same string render approximate (~); otherwise the two renderings join with an en dash, parts taggedstartRange/endRange/shared.Out of scope
Full ICU field-collapsing and locale range patterns are not reproduced, so the three exact-output cases (
formatRange/{en-US,pt-PT},formatRangeToParts/en-US) remain failing. These need a real ICU range formatter.Validation
Against the pinned test262 corpus (
scripts/test262_subset.py, sha4249661):intl402/NumberFormat: 83 → 71 failing (12 fixed:invoked-as-func/length/name/prop-desc/nan-arguments-throws/x-greater-than-y-not-throwsfor both methods).Refs #5581.
🤖 Generated with Claude Code
Summary by CodeRabbit
Intl.NumberFormatrange formatting, includingformatRangeandformatRangeToParts.