Skip to content

Commit 17c55a9

Browse files
longlhoclaude
andcommitted
fix(@formatjs/intl-durationformat): pass BigDecimal directly to NumberFormat
Addresses review feedback on #6466: the previous version round-tripped the BigDecimal through `.toString() as ${number}` before formatToParts, which is conceptually lossy and required a hand-waving cast. NumberFormat (V3) coerces non-primitive inputs through ToPrimitive → toString and parses the result as a StringNumericLiteral, so passing the BigDecimal straight through gives the same exact-decimal semantics with no cast. - Widen `createMemoizedNumberFormat` to a `NumberFormatLike` that types `format`/`formatToParts` as accepting BigDecimal (and string). - Drop the `${number}` cast at the formatToParts call. - Revert the `ES2023.Intl` lib addition — no longer needed now that the call site doesn't depend on the V3 string overload signature. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 1429e8f commit 17c55a9

4 files changed

Lines changed: 21 additions & 8 deletions

File tree

packages/ecma402-abstract/utils.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {BigDecimal} from '@formatjs/bigdecimal'
12
import {memoize, strategies} from '@formatjs/fast-memoize'
23

34
export function repeat(s: string, times: number): string {
@@ -142,11 +143,23 @@ export function invariant(
142143
}
143144
}
144145

146+
// Native NumberFormat coerces non-primitive inputs via ToPrimitive (calling
147+
// `toString()`) before feeding them to ToIntlMathematicalValue, so a
148+
// `BigDecimal` passed to `format`/`formatToParts` is parsed as an exact
149+
// StringNumericLiteral. The widened return type lets call sites carry
150+
// BigDecimal end-to-end without lossy intermediate `.toString()` casts.
151+
type NumberFormatLike = Omit<Intl.NumberFormat, 'format' | 'formatToParts'> & {
152+
format(value: number | bigint | string | BigDecimal | undefined): string
153+
formatToParts(
154+
value: number | bigint | string | BigDecimal | undefined
155+
): Intl.NumberFormatPart[]
156+
}
157+
145158
export const createMemoizedNumberFormat: (
146159
...args: ConstructorParameters<typeof Intl.NumberFormat>
147-
) => Intl.NumberFormat = memoize(
160+
) => NumberFormatLike = memoize(
148161
(...args: ConstructorParameters<typeof Intl.NumberFormat>) =>
149-
new Intl.NumberFormat(...args),
162+
new Intl.NumberFormat(...args) as NumberFormatLike,
150163
{
151164
strategy: strategies.variadic,
152165
}

packages/intl-durationformat/abstract/PartitionDurationFormatPattern.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ export function PartitionDurationFormatPattern(
3434
// Carry the value as BigDecimal end-to-end so sub-second rollups stay
3535
// exact. Float arithmetic like `1 + 473/1e3` lands on
3636
// `1.4729999999999998650`, which `roundingMode: 'trunc'` truncates to
37-
// `1.472999999` instead of `1.473` (#6462). NumberFormat (V3) accepts
38-
// a decimal string and parses it as a Mathematical Value, sidestepping
39-
// the IEEE 754 round-trip entirely.
37+
// `1.472999999` instead of `1.473` (#6462).
4038
let value = new BigDecimal(duration[row.valueField])
4139
const style = internalSlots[row.styleSlot]
4240
const display = internalSlots[row.displaySlot]
@@ -104,7 +102,11 @@ export function PartitionDurationFormatPattern(
104102
value: separator,
105103
})
106104
}
107-
let parts = nf.formatToParts(value.toString() as `${number}`)
105+
// Pass the BigDecimal straight through. NumberFormat (V3) parses it
106+
// as an exact Mathematical Value via ToPrimitive → BigDecimal.toString,
107+
// which sidesteps the IEEE 754 round-trip that breaks
108+
// `roundingMode: 'trunc'` on values like `1 + 473/1e3` (#6462).
109+
let parts = nf.formatToParts(value)
108110
parts.forEach(({type, value}) => {
109111
list.push({
110112
type,

tools/tsconfig.bzl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ BASE_TSCONFIG = {
1919
"ES2021.intl",
2020
"ES2021.String",
2121
"ES2022.Intl",
22-
"ES2023.Intl",
2322
"ESNext.Intl",
2423
],
2524
"declaration": True,

tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"ES2021.intl",
1717
"ES2021.String",
1818
"ES2022.Intl",
19-
"ES2023.Intl",
2019
"ESNext.Intl"
2120
],
2221
"module": "esnext",

0 commit comments

Comments
 (0)