Skip to content

Commit 50bd498

Browse files
committed
feat: use Decimal in polyfills to fix floating point issues
feat(@formatjs/intl-numberformat): Support ES2025 fix #4678
1 parent b6de002 commit 50bd498

File tree

71 files changed

+12368
-11828
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+12368
-11828
lines changed

MODULE.bazel.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@types/babel__helper-plugin-utils": "^7.10.3",
5454
"@types/babel__traverse": "^7.20.6",
5555
"@types/benchmark": "^2.1.5",
56+
"@types/big.js": "^6.2.2",
5657
"@types/eslint": "9.6.1",
5758
"@types/estree": "^1.0.6",
5859
"@types/fs-extra": "^11.0.4",
@@ -92,6 +93,7 @@
9293
"commander": "^12.1.0",
9394
"content-tag": "^3.0.0",
9495
"core-js": "^3.38.1",
96+
"decimal.js": "^10.4.3",
9597
"ember-template-recast": "^6.1.5",
9698
"emoji-regex": "^10.4.0",
9799
"eslint": "9.16.0",

packages/ecma402-abstract/262.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {Decimal} from 'decimal.js'
2+
import {ZERO} from './constants'
13
/**
24
* https://tc39.es/ecma262/#sec-tostring
35
*/
@@ -14,43 +16,37 @@ export function ToString(o: unknown): string {
1416
* https://tc39.es/ecma262/#sec-tonumber
1517
* @param val
1618
*/
17-
export function ToNumber(val: any): number {
19+
export function ToNumber(val: any): Decimal {
1820
if (val === undefined) {
19-
return NaN
21+
return new Decimal(NaN)
2022
}
2123
if (val === null) {
22-
return +0
24+
return ZERO
2325
}
2426
if (typeof val === 'boolean') {
25-
return val ? 1 : +0
26-
}
27-
if (typeof val === 'number') {
28-
return val
27+
return new Decimal(val ? 1 : 0)
2928
}
3029
if (typeof val === 'symbol' || typeof val === 'bigint') {
3130
throw new TypeError('Cannot convert symbol/bigint to number')
3231
}
33-
return Number(val)
32+
return new Decimal(Number(val))
3433
}
3534

3635
/**
3736
* https://tc39.es/ecma262/#sec-tointeger
3837
* @param n
3938
*/
40-
function ToInteger(n: any) {
39+
function ToInteger(n: any): Decimal {
4140
const number = ToNumber(n)
42-
if (isNaN(number) || SameValue(number, -0)) {
43-
return 0
41+
if (number.isNaN() || number.isZero()) {
42+
return ZERO
4443
}
45-
if (isFinite(number)) {
44+
if (number.isFinite()) {
4645
return number
4746
}
48-
let integer = Math.floor(Math.abs(number))
49-
if (number < 0) {
50-
integer = -integer
51-
}
52-
if (SameValue(integer, -0)) {
53-
return 0
47+
let integer = number.abs().floor()
48+
if (number.isNegative()) {
49+
integer = integer.negated()
5450
}
5551
return integer
5652
}
@@ -66,7 +62,7 @@ export function TimeClip(time: number): number {
6662
if (Math.abs(time) > 8.64 * 1e15) {
6763
return NaN
6864
}
69-
return ToInteger(time)
65+
return ToInteger(time).toNumber()
7066
}
7167

7268
/**

packages/ecma402-abstract/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ SRCS = glob(
3535
SRC_DEPS = [
3636
":node_modules/@formatjs/intl-localematcher",
3737
":node_modules/@formatjs/fast-memoize",
38+
"//:node_modules/decimal.js",
3839
]
3940

4041
ts_compile(

packages/ecma402-abstract/NumberFormat/ApplyUnsignedRoundingMode.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import Decimal from 'decimal.js'
12
import {UnsignedRoundingModeType} from '../types/number'
3+
import {invariant} from '../utils'
24

35
export function ApplyUnsignedRoundingMode(
4-
x: number,
5-
r1: number,
6-
r2: number,
6+
x: Decimal,
7+
r1: Decimal,
8+
r2: Decimal,
79
unsignedRoundingMode: UnsignedRoundingModeType
8-
): number {
9-
if (x === r1) return r1
10-
if (unsignedRoundingMode === undefined) {
11-
throw new Error('unsignedRoundingMode is mandatory')
12-
}
10+
): Decimal {
11+
if (x.eq(r1)) return r1
12+
invariant(
13+
r1.lessThan(x) && x.lessThan(r2),
14+
`x should be between r1 and r2 but x=${x}, r1=${r1}, r2=${r2}`
15+
)
1316

1417
if (unsignedRoundingMode === 'zero') {
1518
return r1
@@ -18,19 +21,17 @@ export function ApplyUnsignedRoundingMode(
1821
return r2
1922
}
2023

21-
const d1 = x - r1
22-
const d2 = r2 - x
24+
const d1 = x.minus(r1)
25+
const d2 = r2.minus(x)
2326

24-
if (d1 < d2) {
27+
if (d1.lessThan(d2)) {
2528
return r1
2629
}
27-
if (d2 < d1) {
30+
if (d2.lessThan(d1)) {
2831
return r2
2932
}
3033

31-
if (d1 !== d2) {
32-
throw new Error('Unexpected error')
33-
}
34+
invariant(d1.eq(d2), 'd1 should be equal to d2')
3435

3536
if (unsignedRoundingMode === 'half-zero') {
3637
return r1
@@ -39,15 +40,14 @@ export function ApplyUnsignedRoundingMode(
3940
return r2
4041
}
4142

42-
if (unsignedRoundingMode !== 'half-even') {
43-
throw new Error(
44-
`Unexpected value for unsignedRoundingMode: ${unsignedRoundingMode}`
45-
)
46-
}
43+
invariant(
44+
unsignedRoundingMode === 'half-even',
45+
'unsignedRoundingMode should be half-even'
46+
)
4747

48-
const cardinality = (r1 / (r2 - r1)) % 2
48+
const cardinality = r1.div(r2.minus(r1)).mod(2)
4949

50-
if (cardinality === 0) {
50+
if (cardinality.isZero()) {
5151
return r1
5252
}
5353
return r2
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {getMagnitude} from '../utils'
1+
import Decimal from 'decimal.js'
2+
import {TEN} from '../constants'
3+
import {NumberFormatInternal} from '../types/number'
24
import {ComputeExponentForMagnitude} from './ComputeExponentForMagnitude'
35
import {FormatNumericToString} from './FormatNumericToString'
4-
import {NumberFormatInternal} from '../types/number'
5-
66
/**
77
* The abstract operation ComputeExponent computes an exponent (power of ten) by which to scale x
88
* according to the number formatting settings. It handles cases such as 999 rounding up to 1000,
@@ -12,38 +12,38 @@ import {NumberFormatInternal} from '../types/number'
1212
*/
1313
export function ComputeExponent(
1414
numberFormat: Intl.NumberFormat,
15-
x: number,
15+
x: Decimal,
1616
{
1717
getInternalSlots,
1818
}: {getInternalSlots(nf: Intl.NumberFormat): NumberFormatInternal}
1919
): [number, number] {
20-
if (x === 0) {
20+
if (x.isZero()) {
2121
return [0, 0]
2222
}
23-
if (x < 0) {
24-
x = -x
23+
if (x.isNegative()) {
24+
x = x.negated()
2525
}
26-
const magnitude = getMagnitude(x)
26+
const magnitude = x.log(10).floor()
2727
const exponent = ComputeExponentForMagnitude(numberFormat, magnitude, {
2828
getInternalSlots,
2929
})
3030
// Preserve more precision by doing multiplication when exponent is negative.
31-
x = exponent < 0 ? x * 10 ** -exponent : x / 10 ** exponent
31+
x = x.times(TEN.pow(-exponent))
3232
const formatNumberResult = FormatNumericToString(
3333
getInternalSlots(numberFormat),
3434
x
3535
)
36-
if (formatNumberResult.roundedNumber === 0) {
37-
return [exponent, magnitude]
36+
if (formatNumberResult.roundedNumber.isZero()) {
37+
return [exponent, magnitude.toNumber()]
3838
}
39-
const newMagnitude = getMagnitude(formatNumberResult.roundedNumber)
40-
if (newMagnitude === magnitude - exponent) {
41-
return [exponent, magnitude]
39+
const newMagnitude = formatNumberResult.roundedNumber.log(10).floor()
40+
if (newMagnitude.eq(magnitude.minus(exponent))) {
41+
return [exponent, magnitude.toNumber()]
4242
}
4343
return [
44-
ComputeExponentForMagnitude(numberFormat, magnitude + 1, {
44+
ComputeExponentForMagnitude(numberFormat, magnitude.plus(1), {
4545
getInternalSlots,
4646
}),
47-
magnitude + 1,
47+
magnitude.plus(1).toNumber(),
4848
]
4949
}

packages/ecma402-abstract/NumberFormat/ComputeExponentForMagnitude.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import {NumberFormatInternal, DecimalFormatNum} from '../types/number'
2-
1+
import Decimal from 'decimal.js'
2+
import {TEN} from '../constants'
3+
import {DecimalFormatNum, NumberFormatInternal} from '../types/number'
4+
import {invariant} from '../utils'
5+
Decimal.set({
6+
toExpPos: 100,
7+
})
38
/**
49
* The abstract operation ComputeExponentForMagnitude computes an exponent by which to scale a
510
* number of the given magnitude (power of ten of the most significant digit) according to the
611
* locale and the desired notation (scientific, engineering, or compact).
712
*/
813
export function ComputeExponentForMagnitude(
914
numberFormat: Intl.NumberFormat,
10-
magnitude: number,
15+
magnitude: Decimal,
1116
{
1217
getInternalSlots,
1318
}: {getInternalSlots(nf: Intl.NumberFormat): NumberFormatInternal}
@@ -19,10 +24,12 @@ export function ComputeExponentForMagnitude(
1924
case 'standard':
2025
return 0
2126
case 'scientific':
22-
return magnitude
27+
return magnitude.toNumber()
2328
case 'engineering':
24-
return Math.floor(magnitude / 3) * 3
29+
const thousands = magnitude.div(3).floor()
30+
return thousands.times(3).toNumber()
2531
default: {
32+
invariant(notation === 'compact', 'Invalid notation')
2633
// Let exponent be an implementation- and locale-dependent (ILD) integer by which to scale a
2734
// number of the given magnitude in compact notation for the current locale.
2835
const {compactDisplay, style, currencyDisplay} = internalSlots
@@ -41,7 +48,7 @@ export function ComputeExponentForMagnitude(
4148
if (!thresholdMap) {
4249
return 0
4350
}
44-
const num = String(10 ** magnitude) as DecimalFormatNum
51+
const num = TEN.pow(magnitude).toString() as DecimalFormatNum
4552
const thresholds = Object.keys(thresholdMap) as DecimalFormatNum[] // TODO: this can be pre-processed
4653
if (num < thresholds[0]) {
4754
return 0

packages/ecma402-abstract/NumberFormat/FormatNumericRange.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Decimal from 'decimal.js'
12
import {NumberFormatInternal} from '../types/number'
23
import {PartitionNumberRangePattern} from './PartitionNumberRangePattern'
34

@@ -6,8 +7,8 @@ import {PartitionNumberRangePattern} from './PartitionNumberRangePattern'
67
*/
78
export function FormatNumericRange(
89
numberFormat: Intl.NumberFormat,
9-
x: number,
10-
y: number,
10+
x: Decimal,
11+
y: Decimal,
1112
{
1213
getInternalSlots,
1314
}: {

packages/ecma402-abstract/NumberFormat/FormatNumericRangeToParts.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Decimal from 'decimal.js'
12
import {NumberFormatInternal, NumberRangeToParts} from '../types/number'
23
import {PartitionNumberRangePattern} from './PartitionNumberRangePattern'
34

@@ -6,8 +7,8 @@ import {PartitionNumberRangePattern} from './PartitionNumberRangePattern'
67
*/
78
export function FormatNumericRangeToParts(
89
numberFormat: Intl.NumberFormat,
9-
x: number,
10-
y: number,
10+
x: Decimal,
11+
y: Decimal,
1112
{
1213
getInternalSlots,
1314
}: {

packages/ecma402-abstract/NumberFormat/FormatNumericToParts.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import {PartitionNumberPattern} from './PartitionNumberPattern'
1+
import Decimal from 'decimal.js'
22
import {ArrayCreate} from '../262'
33
import {NumberFormatInternal, NumberFormatPart} from '../types/number'
4+
import {PartitionNumberPattern} from './PartitionNumberPattern'
45

56
export function FormatNumericToParts(
67
nf: Intl.NumberFormat,
7-
x: number,
8+
x: Decimal,
89
implDetails: {
910
getInternalSlots(nf: Intl.NumberFormat): NumberFormatInternal
1011
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy