Add fixed-point string and number conversions#1567
Conversation
|
BundleMonFiles updated (4)
Unchanged files (143)
Total files change +1.96KB +0.38% Final result: ✅ View report in BundleMon website ➡️ |
|
Documentation Preview: https://kit-docs-fnq3cvfoh-anza-tech.vercel.app |
dec9bf6 to
8e377fb
Compare
888f715 to
5612ac9
Compare
5612ac9 to
d29dae8
Compare
8e377fb to
89cd89f
Compare
| return `${sign}${integerPart}`; | ||
| } | ||
| return `${sign}${integerPart}.${fractionalPart}`; | ||
| } |
There was a problem hiding this comment.
I think we could use Intl.NumberFormat here
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: padTrailingZeros ? decimals : 0,
maximumFractionDigits: decimals,
useGrouping: false
}).format(`${raw}E-${decimals}`)should be equivalent to this I think.
Edit: Confirmed this passes all tests
I also wonder if we should expose a function to get from a fixed point to {raw, decimals} (ie everything before the call to this function) so that apps can add their own Intl.NumberFormat call with custom locale and formatting. Could be added later though.
There was a problem hiding this comment.
That's not a bad idea but we would need a cast because NumberFormat technically only works on number types.
interface NumberFormat {
format(value: number): string;
resolvedOptions(): ResolvedNumberFormatOptions;
}I went down a small rabbit hole on this. I was initially worried that providing a string as argument would internally transform it to a number and therefore lose precision when reaching the boundaries of JavaScript numbers. Turns out since ES2023, the spec actually does say string arguments are parsed as a StringNumericLiteral and stored as an exact "mathematical value", so precision is preserved in fully-compliant environments. That said, I still think an explicit homemade solution might be worth keeping mainly due to inconsistencies between environments. For instance, (according to Claude) Hermes / React Native added string-arg support much later and therefore we may run into some issues when running in older environments. I also think this custom solution should be faster (not that it matters a lot) since it doesn't need to account for all the formatting logic that NumberFormat provides.
This is why I thought of offering lossy toNumber helpers so apps can explicitly rely on NumberFormat when they want locale-aware output and have accepted the precision tradeoff. And as you suggested, they can even use strings now if they are in the correct environments (although they will need a type-cast because TypeScript hasn't caught up with the new API yet).
Regarding the function that outputs { raw, decimals }, am I right to say this would be for binary fixed-point only (since decimal fixed-points are already described as such) and it would wrap the following conversion?
// Convert the base-2 representation to an exact base-10 representation:
// raw / 2 ** F === (raw * 5 ** F) / 10 ** F, which terminates cleanly.
// The transformed raw carries exactly F decimal digits of precision.
const base10Decimals = value.fractionalBits;
const base10Raw = base10Decimals === 0 ? value.raw : value.raw * 5n ** BigInt(base10Decimals);If so, I'm happy to add a binaryFixedPointToBase10 function (name TBD) that returns { raw, decimals }. I initially wanted to suggest a decimalFixedPointToBinaryFixedPoint/binaryFixedPointToDecimalFixedPoint duo but turns out this is a bit more complicated than that because you will likely need to adjust the totalBits/fractionalBits to keep the range of values intact (or throw arithmetic overflows).
Let me know what you think and I can push a new PR on top of that stack.
There was a problem hiding this comment.
Yea it's weirdly missing from Typescript still, but I learnt about it from Steve's issue about adding math way back: #29. I've used it a bunch since and find it really nice.
I definitely think Intl.NumberFormat with string scientific notation is the most flexible API for display rather than going through number there, but having toNumber conversions is still worth it for other use cases where you need a number IMO.
Agree the custom solution should be faster, and if Intl.NumberFormat doesn't have the support yet then I'm good to keep it as-is.
On the function for {raw, decimals}, the only other thing the base10 one would give us is the rounding - but NumberFormat has a lot of rounding modes and I think it's a superset of ours. So probably not worth adding anything there. I think just adding binary -> base10 like you said is the way to go there. Then anyone who wants to use Intl.NumberFormat with either decimal or binary fixed points can easily do so, with whatever locale and formatting suit them.
There was a problem hiding this comment.
That's a really good shout, I had forgotten about this part of the original issue. How about on top of the toNumber and toString helpers, I provide the following helpers in a subsequent PR in the stack:
formatDecimalFixedPoint(formatter: Intl.NumberFormat, value: DecimalFixedPoint): stringformatBinaryFixedPoint(formatter: Intl.NumberFormat, value: BinaryFixedPoint): stringbinaryFixedPointToBase10(value: BinaryFixedPoint): { raw: bigint, decimals: number }(used internally by the binary fixed-point helpers)
That way, people can mostly rely on the format helpers when they need to display values but the toString helpers stay lossless and environment-safe. Wdyt?
89cd89f to
f97f875
Compare
d29dae8 to
89b1755
Compare
f97f875 to
2031a98
Compare
09f7d3d to
d77d17f
Compare
2031a98 to
ec8d8d1
Compare
Merge activity
|
This PR adds three new public helpers to `@solana/fixed-points`: `formatDecimalFixedPoint` and `formatBinaryFixedPoint` accept any `Intl.NumberFormat` instance and route the underlying raw bigint through ES2023 string scientific notation (`"<raw>E-<decimals>"`), which preserves full precision regardless of the value's magnitude. They give consumers locale-aware output, currency formatting, grouping separators, and any rounding mode supported by `Intl.NumberFormat`, while the existing lossless `*ToString` helpers remain the recommended choice when portability across older runtimes (older Hermes/React Native, etc.) is a concern. The PR also exposes `binaryFixedPointToBase10`, which returns a binary fixed-point's exact `(raw, decimals)` base-10 representation. It is used internally by `formatBinaryFixedPoint` and by `binaryFixedPointToString` (deduplicating an inline conversion), and it lets consumers plug their own formatter on top of binary fixed-points just as they already can with decimal ones. Decimal fixed-points already expose `(raw, decimals)` directly on the value object so no equivalent helper is needed for them. Continuation of the discussion on #1567.
d77d17f to
a3fa2e7
Compare
ec8d8d1 to
947f588
Compare
This PR adds three new public helpers to `@solana/fixed-points`: `formatDecimalFixedPoint` and `formatBinaryFixedPoint` accept any `Intl.NumberFormat` instance and route the underlying raw bigint through ES2023 string scientific notation (`"<raw>E-<decimals>"`), which preserves full precision regardless of the value's magnitude. They give consumers locale-aware output, currency formatting, grouping separators, and any rounding mode supported by `Intl.NumberFormat`, while the existing lossless `*ToString` helpers remain the recommended choice when portability across older runtimes (older Hermes/React Native, etc.) is a concern. The PR also exposes `binaryFixedPointToBase10`, which returns a binary fixed-point's exact `(raw, decimals)` base-10 representation. It is used internally by `formatBinaryFixedPoint` and by `binaryFixedPointToString` (deduplicating an inline conversion), and it lets consumers plug their own formatter on top of binary fixed-points just as they already can with decimal ones. Decimal fixed-points already expose `(raw, decimals)` directly on the value object so no equivalent helper is needed for them. Continuation of the discussion on #1567.
947f588 to
9d7fc99
Compare
a3fa2e7 to
236f83c
Compare
This PR adds `binaryFixedPointToString`, `decimalFixedPointToString`, `binaryFixedPointToNumber`, and `decimalFixedPointToNumber` to `@solana/fixed-points`. The string variants accept an optional `FixedPointToStringOptions` bag (`decimals`, `padTrailingZeros`, `rounding`) and throw `SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS` (with `operation: 'toString'`) when capping requires a lossy rescale under the default `'strict'` mode. The number variants are lossy escape hatches bounded by IEEE 754's ~53-bit mantissa. `binaryFixedPointToNumber` splits the raw value into integer and fractional parts before coercing, preserving exactness whenever the result magnitude fits `Number.MAX_SAFE_INTEGER` even if the raw bigint does not.
9d7fc99 to
da94b9b
Compare
|
🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs' |
This PR adds three new public helpers to `@solana/fixed-points`: `formatDecimalFixedPoint` and `formatBinaryFixedPoint` accept any `Intl.NumberFormat` instance and route the underlying raw bigint through ES2023 string scientific notation (`"<raw>E-<decimals>"`), which preserves full precision regardless of the value's magnitude. They give consumers locale-aware output, currency formatting, grouping separators, and any rounding mode supported by `Intl.NumberFormat`, while the existing lossless `*ToString` helpers remain the recommended choice when portability across older runtimes (older Hermes/React Native, etc.) is a concern. The PR also exposes `binaryFixedPointToBase10`, which returns a binary fixed-point's exact `(raw, decimals)` base-10 representation. It is used internally by `formatBinaryFixedPoint` and by `binaryFixedPointToString` (deduplicating an inline conversion), and it lets consumers plug their own formatter on top of binary fixed-points just as they already can with decimal ones. Decimal fixed-points already expose `(raw, decimals)` directly on the value object so no equivalent helper is needed for them. Continuation of the discussion on #1567.
This PR adds three new public helpers to `@solana/fixed-points`: `formatDecimalFixedPoint` and `formatBinaryFixedPoint` accept any `Intl.NumberFormat` instance and route the underlying raw bigint through ES2023 string scientific notation (`"<raw>E-<decimals>"`), which preserves full precision regardless of the value's magnitude. They give consumers locale-aware output, currency formatting, grouping separators, and any rounding mode supported by `Intl.NumberFormat`, while the existing lossless `*ToString` helpers remain the recommended choice when portability across older runtimes (older Hermes/React Native, etc.) is a concern. The PR also exposes `binaryFixedPointToBase10`, which returns a binary fixed-point's exact `(raw, decimals)` base-10 representation. It is used internally by `formatBinaryFixedPoint` and by `binaryFixedPointToString` (deduplicating an inline conversion), and it lets consumers plug their own formatter on top of binary fixed-points just as they already can with decimal ones. Decimal fixed-points already expose `(raw, decimals)` directly on the value object so no equivalent helper is needed for them. Continuation of the discussion on #1567.

This PR adds
binaryFixedPointToString,decimalFixedPointToString,binaryFixedPointToNumber, anddecimalFixedPointToNumberto@solana/fixed-points. The string variants accept an optionalFixedPointToStringOptionsbag (decimals,padTrailingZeros,rounding) and throwSOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS(withoperation: 'toString') when capping requires a lossy rescale under the default'strict'mode.The number variants are lossy escape hatches bounded by IEEE 754's ~53-bit mantissa.
binaryFixedPointToNumbersplits the raw value into integer and fractional parts before coercing, preserving exactness whenever the result magnitude fitsNumber.MAX_SAFE_INTEGEReven if the raw bigint does not.