From cf7a07df7afe1f4d98a98476aa0df3804efe0f18 Mon Sep 17 00:00:00 2001 From: jessealama Date: Thu, 4 Jun 2026 14:39:13 +0000 Subject: [PATCH 01/17] fixup: [spec] `npm run build` --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 6ae02a83..8fa887f0 100644 --- a/index.html +++ b/index.html @@ -3231,7 +3231,7 @@

Stage 1 Draft / February 12, 2026

Decimal

+

Stage 1 Draft / June 9, 2026

Decimal

Introduction

From a179adc8ea4b7c80c3f43b595d222d0091e85812 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Thu, 4 Jun 2026 17:15:38 +0200 Subject: [PATCH 02/17] Relate Decimal and Amount Now that Amount has reached stage 2, we can more confidently align (and distinguish) the two proposals. --- README.md | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b3d2ca9b..eb69c51d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,31 @@ The champions welcome your participation in discussing the design space in the i **Stage**: Stage 1 of [the TC39 process](https://tc39.github.io/process-document/). A [draft specification](http://tc39.es/proposal-decimal/) is available. +## Decimal and Amount + +Decimal is designed to work hand-in-hand with the TC39 [Amount proposal](https://github.com/tc39/proposal-amount) (Stage 2). The two proposals split the problem of working with decimal quantities this way: + +**Decimal computes the value** + +and + +**Amount carries its precision and unit and formats it for humans.** + +Amount is a lightweight wrapper that pairs a numeric value with its measurement context (significant or fraction digits), optionally with a unit (including a currency), and locale-aware formatting. Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision, and definitely has no concept of units. Decimal canonicalizes trailing zeroes away (so `1.20` and `1.2` are the same Decimal), which is exactly the information Amount preserves. + +| Concern | Owner | +|---|---| +| Exact `+`, `−`, `×`, `÷`, remainder, and rounding | **Decimal** | +| Comparisons | **Decimal** | +| Extracting mantissa/exponent/significand | **Decimal** | +| Handling significant/fraction digits (including trailing zeroes) | **Amount** | +| Units and currency | **Amount** | +| Locale-aware formatting (`toLocaleString`) | **Amount** | +| Plural selection | **Amount** | +| Unit conversion | **Amount** | + +In practice, compute with Decimal and present with Amount. Run your calculation, then wrap the result in an Amount to round it for display, attach a currency or unit, and localize it. See the [currency conversion example](#currency-conversion) below. + ## Use cases and goals Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript. Currently, developers sometimes represent these using libraries for this purpose, or sometimes use Strings. Sadly, JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding @@ -60,17 +85,25 @@ console.log(total.toFixed(2)); ##### Currency conversion -Let's convert USD to EUR, given the exchange rate EUR --> USD. +Let's convert USD to EUR, given the exchange rate EUR --> USD. Decimal does the exact arithmetic; we then hand the result to [Amount](https://github.com/tc39/proposal-amount) to round it for display, attach the currency, and localize it. ```js +// Exact arithmetic — Decimal's job: let exchangeRateEurToUsd = new Decimal("1.09"); let amountInUsd = new Decimal("450.27"); let exchangeRateUsdToEur = new Decimal("1").divide(exchangeRateEurToUsd); +let amountInEur = amountInUsd.multiply(exchangeRateUsdToEur); -let amountInEur = exchangeRateUsdToEur.multiply(amountInUsd); -console.log(amountInEur.round(2).toString()); +// Presentation — Amount's job (precision + currency + locale): +let display = new Amount(amountInEur.toString(), { + unit: "EUR", + fractionDigits: 2, +}); +console.log(display.toLocaleString("de-DE")); // "413,09 €" ``` +Today the two types are bridged with `toString()`; a natural point of coordination between the proposals is for `Amount` to accept a `Decimal` value directly, so the handoff becomes `new Amount(amountInEur, { unit: "EUR", fractionDigits: 2 })`. + #### Why use JavaScript for this case? Historically, JavaScript may not have been considered a language where exact decimal numbers are even exactly representable, with the understanding that doing calculations is bound to propagate any initial rounding errors when numbers were created. In some application architectures, JS only deals with a string representing a human-readable decimal quantity (e.g, `"1.25"`), and never does calculations or conversions. However, several trends push towards JS’s deeper involvement in with decimal quantities: From 9cbeb17393456fe41fb2af130150cbd8e710363b Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Thu, 4 Jun 2026 17:36:36 +0200 Subject: [PATCH 03/17] Refer to (to-be-defined) AOs for String rendering --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eb69c51d..334ac42c 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ Amount is a lightweight wrapper that pairs a numeric value with its measurement | Extracting mantissa/exponent/significand | **Decimal** | | Handling significant/fraction digits (including trailing zeroes) | **Amount** | | Units and currency | **Amount** | -| Locale-aware formatting (`toLocaleString`) | **Amount** | -| Plural selection | **Amount** | +| Plain string rendering (`toString`, `toFixed`, `toPrecision`, `toExponential`) | **Decimal** (via shared rendering AOs that Amount can reuse) | +| Locale-aware formatting | **Amount** (Decimal's `toLocaleString` delegates here) | +| Plural selection | **Amount** (Decimal may lose relevant data) | | Unit conversion | **Amount** | In practice, compute with Decimal and present with Amount. Run your calculation, then wrap the result in an Amount to round it for display, attach a currency or unit, and localize it. See the [currency conversion example](#currency-conversion) below. @@ -331,14 +332,16 @@ Decimal objects can be constructed from Numbers, Strings, and BigInts. Similarly ### String formatting -`Decimal` objects can be converted to Strings in a number of ways, similar to Numbers: +`Decimal` objects can be converted to plain, locale-independent Strings in a number of ways, similar to Numbers: - `toString()` is similar to the behavior on Number, e.g., `new Decimal("123.456").toString()` is `"123.456"`. ([#12](https://github.com/tc39/proposal-decimal/issues/12)) - `toFixed()` is similar to Number's `toFixed()` -- `toPrecison()` is similar to Number's `toPrecision()` +- `toPrecision()` is similar to Number's `toPrecision()` - `toExponential()` is similar to Number's `toExponential()` -- `Intl.NumberFormat.prototype.format` will support Decimal values directly ([#15](https://github.com/tc39/proposal-decimal/issues/15)) -- `Intl.PluralRules.prototype.select` will similarly support Decimal values + +We intend to specify these methods in terms of abstract operations that render a mathematical value as a digit string (in fixed, precision-limited, or exponential form). These abstract operations are written so that the [Amount proposal](https://github.com/tc39/proposal-amount) can reuse them, giving Decimal and Amount a single, consistent notion of how a decimal value becomes a string. + +For locale-aware formatting, Decimal leans on Amount. `Decimal.prototype.toLocaleString` is defined to wrap the Decimal value in an Amount, which configures an `Intl.NumberFormat` and calls its `format` method ([#15](https://github.com/tc39/proposal-decimal/issues/15)). This keeps a single source of truth for localization, units, currency, and plural selection in Amount, rather than duplicating that machinery on Decimal. ## Past discussions in TC39 plenaries From 26e2424969398a2e747f964fc7fa228cebef5f8e Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Thu, 4 Jun 2026 17:49:05 +0200 Subject: [PATCH 04/17] Mention related proposals --- RELATED.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELATED.md b/RELATED.md index 20d1a5ea..095459f5 100644 --- a/RELATED.md +++ b/RELATED.md @@ -2,6 +2,14 @@ The need for accurately representing decimal quantities is not new or unique to our current circumstances. That's why there are a number of popular JS ecosystem libraries for decimal, why many other programming languages, databases and standards have built-in data types for this purpose, and why TC39 has been considering adding Decimal for at least 12 years. +### Allied TC39 proposals + +Decimal is part of a small family of TC39 proposals that, together, cover working with decimal quantities. Decimal supplies the exact-arithmetic core; the others handle measurement context and formatting: + +- [**Amount**](https://github.com/tc39/proposal-amount) (Stage 2): a wrapper around a numeric value and an optional unit/currency, with support for precision (significant/fraction digits). Amount deliberately does no arithmetic, and Decimal deliberately tracks neither display precision nor units. See the [Decimal and Amount](./README.md#decimal-and-amount) section of the README for the division of labor. +- [**Keep trailing zeroes**](https://github.com/tc39/proposal-intl-keep-trailing-zeros) (Stage 3): ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings (e.g., unintentionally normalizing `"1.20"` to `"1.2"`). +- [**Smart Units**](https://github.com/tc39/proposal-smart-unit-preferences): a follow-on to Amount for locale- and usage-aware unit preferences and conversion. + ### Related JS ecosystem libraries JavaScript programmers are using decimal data types today with various ecosystem libraries. The most popular three on npm are each by [MikeMcl](https://github.com/mikemcl): From 9cc0cb48767ec9d2705b8ebb2eb52dc3d9ace15f Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Thu, 4 Jun 2026 17:53:05 +0200 Subject: [PATCH 05/17] Focus more on exact arithmetic --- README.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 334ac42c..e7fb02a3 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,9 @@ In other words, with JS increasingly being used in contexts and scenarios where This use case implies the following goals: -- Avoid unintentional rounding that causes user-visible errors -- Basic mathematical functions such as addition, subtraction, multiplication, and division -- Sufficient precision for typical money and other human-readable quantities, including cryptocurrency (where many decimal digits are routinely needed) +- Exact arithmetic: Addition, subtraction, multiplication, and division compute their exact decimal result, with no unintended rounding to cause user-visible errors +- Explicit, opt-in rounding when a calculation requires it, with a choice of rounding mode +- Sufficient precision for typical human-readable quantities, preserved across arithmetic - Sufficient ergonomics to enable correct usage - Be implementable with adequate performance/memory usage for applications - (Please file an issue to mention more requirements) @@ -297,20 +297,20 @@ Decimal is based on IEEE 754-2019 Decimal128, which is a standard for base-10 de - a single NaN value--distinct from the built-in `NaN` of JS. The difference between quiet and singaling NaNs will be collapsed into a single (quiet) Decimal NaN. - positive and negative infinity will be available, though, as with `NaN`, they are distinct from JS's built-in `Infinity` and `-Infinity`. -Decimal canonicalizes when converting to strings and after performing arithmetic operations. This means that Decimals do not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2. This is an important omission from the capabilities defined by IEEE 754 Decimal128. +Decimal canonicalizes when converting to strings and after performing arithmetic operations. This means that Decimals do not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2. This is a deliberate omission from the capabilities defined by IEEE 754 Decimal128: trailing zeroes are display precision, not arithmetic, and tracking them is precisely the job of [Amount](https://github.com/tc39/proposal-amount). (Relatedly, the [keep trailing zeroes proposal](https://github.com/tc39/proposal-intl-keep-trailing-zeros), now at Stage 3, ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings.) ### Operator semantics - Arithmetic - Unary operations - Absolute value - - Negation + - Negation - Binary operations - Addition - - Multiplication - - Subtraction - - Division - - Remainder + - Multiplication + - Subtraction + - Division + - Remainder - Rounding: All five rounding modes of IEEE 754—floor, ceiling, truncate, round-ties-to-even, and round-ties-away-from-zero—will be supported. - (This implies that a couple of the rounding modes in `Intl.NumberFormat` and `Temporal` won't be supported.) - Comparisons @@ -387,10 +387,6 @@ In our discussions we have consistently emphasized the need for basic arithmetic These can be more straightforwardly added in a v2 of Decimal. Based on developer feedback we have already received, we sense that there is relatively little need for these functions. But it is not unreasonable to expect that such feedback will arrive once a v1 of Decimal is widely used. -### Precision metadata for formatting - -A future addition could be a type that pairs a Decimal value with precision metadata (e.g., number of fractional digits). This would be useful for formatting scenarios where trailing zeroes matter, such as displaying currency amounts. Such functionality may be addressed by the TC39 [Amount proposal](https://github.com/tc39/proposal-amount); see [proposal-amount#75](https://github.com/tc39/proposal-amount/issues/75) for discussion. - ## FAQ ### What about rational numbers? From fa12ec48351eba5b087597e54d503a69df5fc26d Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Thu, 4 Jun 2026 18:06:38 +0200 Subject: [PATCH 06/17] Remove discussion of the data exchange case The parts of the data exchange story that are still valid can be discussed as part of the main use case. --- README.md | 57 +++++++++++++++++++------------------------------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index e7fb02a3..28ba3944 100644 --- a/README.md +++ b/README.md @@ -121,46 +121,13 @@ In all of these environments, the lack of decimal number support means that vari In other words, with JS increasingly being used in contexts and scenarios where it traditionally did not appear, the need for being able to natively handle basic data, such as decimal numbers, that other systems already natively handle is increasing. -#### Goals implied by the main use cases - -This use case implies the following goals: - -- Exact arithmetic: Addition, subtraction, multiplication, and division compute their exact decimal result, with no unintended rounding to cause user-visible errors -- Explicit, opt-in rounding when a calculation requires it, with a choice of rounding mode -- Sufficient precision for typical human-readable quantities, preserved across arithmetic -- Sufficient ergonomics to enable correct usage -- Be implementable with adequate performance/memory usage for applications -- (Please file an issue to mention more requirements) - -### Secondary use case: Data exchange - -In both frontend and backend settings, JavaScript is used to communicate with external systems, such as databases and foreign function interfaces to other programming languages. Many external systems already natively support decimal numbers. In such a setting, JavaScript is then the lower common denominator. With decimals in JavaScript, one has the confident that the numeric data one consumes and produces is handled exactly. - -#### Why use JavaScript for this case? - -JavaScript is frequently used as a language to glue other systems together, whether in client, server or embedded applications. Its ease of programming and embedding, and ubiquity, lend itself to this sort of use case. Programmers often don’t have the option to choose another language. When decimals appear in these contexts, it adds more burden on the embedder to develop an application-specific way to handle things; such specificity makes things less composable. - -#### Goals implied by the use case - -This use case implies the following goals: +#### Working with decimal data from other systems -- Basic mathematical functions such as `+`, `-`, `*` should be available -- Sufficient precision for these applications (unclear how high--would require more analysis of applications) -- Be implementable with adequate performance/memory usage for applications -- -0 (minus zero), NaN, and (positive and negative) infinity may be useful here and exposed as such, rather than throwing exceptions, to continue work in exceptional conditions -- (Please file an issue to mention more requirements) - -Interaction with other systems brings the following requirements: +JavaScript is often the glue between systems—databases, foreign-function interfaces, other services—that natively support decimal numbers. Decimal lets you take that data and compute on it exactly, preserving its mathematical value. -- Ability to round-trip decimal quantities from other systems -- Serialization and deserialization in standard decimal formats, e.g., IEEE 754's multiple formats -- Precision sufficient for the applications on the other side - -#### Sample code +Note the scope. Decimal preserves *value*, not representation: it canonicalizes trailing zeroes away (`1.20` becomes `1.2`). So if you only need to transport a value untouched, a String already does that losslessly; and if you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate*. -##### Configure a database adapter to use JS-native decimals - -The following is fictional, but illustrates the idea. Notice the `sql_decimal` configuration option and how the values returned from the DB are handled in JS as Decimal values, rather than as strings or as JS `Number`s: +The following is fictional, but illustrates the idea. Note the `sql_decimal` configuration option, and how values returned from the DB arrive as Decimal values—ready for exact arithmetic—rather than as strings or JS `Number`s: ```js const { Client } = require("pg"); @@ -180,6 +147,17 @@ client.query("SELECT prices FROM data_with_numbers", (err, res) => { }); ``` +#### Goals implied by the main use cases + +This use case implies the following goals: + +- Exact arithmetic: Addition, subtraction, multiplication, and division compute their exact decimal result, with no unintended rounding to cause user-visible errors +- Explicit, opt-in rounding when a calculation requires it, with a choice of rounding mode +- Sufficient precision for typical human-readable quantities, preserved across arithmetic +- Sufficient ergonomics to enable correct usage +- Be implementable with adequate performance/memory usage for applications +- (Please file an issue to mention more requirements) + ### Tertiary use case: Numerical calculations on more precise floats If it works out reasonably to provide for it within the same proposal, it would also be nice to provide support for higher-precision applications of floating point numbers. @@ -296,6 +274,9 @@ Decimal is based on IEEE 754-2019 Decimal128, which is a standard for base-10 de - a single NaN value--distinct from the built-in `NaN` of JS. The difference between quiet and singaling NaNs will be collapsed into a single (quiet) Decimal NaN. - positive and negative infinity will be available, though, as with `NaN`, they are distinct from JS's built-in `Infinity` and `-Infinity`. +- negative zero (`-0`) is a distinct value from positive zero, mirroring IEEE 754 and JavaScript's `Number`; the two compare as equal. + +These special values are exposed as ordinary Decimal values rather than causing exceptions, so that computation can continue in exceptional conditions. Decimal canonicalizes when converting to strings and after performing arithmetic operations. This means that Decimals do not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2. This is a deliberate omission from the capabilities defined by IEEE 754 Decimal128: trailing zeroes are display precision, not arithmetic, and tracking them is precisely the job of [Amount](https://github.com/tc39/proposal-amount). (Relatedly, the [keep trailing zeroes proposal](https://github.com/tc39/proposal-intl-keep-trailing-zeros), now at Stage 3, ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings.) @@ -319,7 +300,7 @@ Decimal canonicalizes when converting to strings and after performing arithmetic - greater-than, greater-than-or-equal - Mantissa, exponent, significand -The library of numerical functions here is kept deliberately minimal. It is based around targeting the primary use case, in which fairly straightforward calculations are envisioned. The secondary use case (data exchange) will involve probably little or no calculation at all. For the tertiary use case of scientific/numerical computations, developers may experiment in JavaScript, developing such libraries, and we may decide to standardize these functions in a follow-on proposal; a minimal toolkit of mantissa, exponent, and significand will be available. We currently do not have good insight into the developer needs for this use case, except generically: square roots, exponentiation & logarithms, and trigonometric functions might be needed, but we are not sure if this is a complete list, and which are more important to have than others. In the meantime, one can use the various functions in JavaScript’s `Math` standard library. +The library of numerical functions here is kept deliberately minimal. It is based around targeting the primary use case, in which fairly straightforward calculations are envisioned. For scientific or numerical computations, developers may experiment in JavaScript, developing such libraries, and we may decide to standardize these functions in a follow-on proposal; a minimal toolkit of mantissa, exponent, and significand will be available. We currently do not have good insight into the developer needs for this use case, except generically: square roots, exponentiation & logarithms, and trigonometric functions might be needed, but we are not sure if this is a complete list, and which are more important to have than others. In the meantime, one can use the various functions in JavaScript’s `Math` standard library. ### Unsupported operations From 8160c962d94e2e2b8ebd7aefd38244a185d5b993 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Fri, 5 Jun 2026 17:02:34 +0200 Subject: [PATCH 07/17] Sharpen opening pitch --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 28ba3944..434874e4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ The TC39 Decimal proposal aims to add exact decimal arithmetic to JavaScript, eliminating the rounding errors that occur with binary floating-point numbers. +There are two ways to see why this matters. The practical one is that end-user-visible bugs come from using binary floating-point Numbers for calculations that are intended to be in decimal arithmetic. The other is more foundational: we want a numeric type that behaves the way we were taught arithmetic works — `0.1 + 0.2` is `0.3`. Decimal is, in essence, a *do-what-I-mean* (DWIM) approach to numbers: the value and the everyday mental model line up, with no hidden binary representation oddities. + The champions welcome your participation in discussing the design space in the issues linked above. **We are seeking input for your needs around JavaScript decimal in [this survey](https://forms.gle/A2YaTr3Tn1o3D7hdA).** **Champions**: From 5ef33e3e32c0d0c3b869dfac2c132165d03ddfb1 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Fri, 5 Jun 2026 17:03:15 +0200 Subject: [PATCH 08/17] More work on the relationship with Amount --- README.md | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 434874e4..7f4a7992 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Decimal is designed to work hand-in-hand with the TC39 [Amount proposal](https:/ and -**Amount carries its precision and unit and formats it for humans.** +**Amount carries a value's precision and unit.** -Amount is a lightweight wrapper that pairs a numeric value with its measurement context (significant or fraction digits), optionally with a unit (including a currency), and locale-aware formatting. Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision, and definitely has no concept of units. Decimal canonicalizes trailing zeroes away (so `1.20` and `1.2` are the same Decimal), which is exactly the information Amount preserves. +Amount wraps a numeric value with its measurement context (significant or fraction digits), as well as, optionally, a unit (including a currency). Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision and has no concept of units. Decimal canonicalizes trailing zeroes away (so `1.20` and `1.2` are equal), which is exactly the information Amount preserves. Decimal values can be rendered in a locale-sensitive way with `toLocaleString`. Amount is what adds the precision and unit/currency data, and it can take a Decimal to do so. | Concern | Owner | |---|---| @@ -36,13 +36,11 @@ Amount is a lightweight wrapper that pairs a numeric value with its measurement | Handling significant/fraction digits (including trailing zeroes) | **Amount** | | Units and currency | **Amount** | | Plain string rendering (`toString`, `toFixed`, `toPrecision`, `toExponential`) | **Decimal** (via shared rendering AOs that Amount can reuse) | -| Locale-aware formatting | **Amount** (Decimal's `toLocaleString` delegates here) | +| Locale-aware formatting (`toLocaleString`) | **Decimal** for a bare number; **Amount** when units, currency, or display precision are involved | | Plural selection | **Amount** (Decimal may lose relevant data) | | Unit conversion | **Amount** | -In practice, compute with Decimal and present with Amount. Run your calculation, then wrap the result in an Amount to round it for display, attach a currency or unit, and localize it. See the [currency conversion example](#currency-conversion) below. - -## Use cases and goals +## Use case and goals Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript. Currently, developers sometimes represent these using libraries for this purpose, or sometimes use Strings. Sadly, JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding errors. @@ -53,7 +51,7 @@ As currently defined in JavaScript, Numbers are 64-bit binary floating-point num The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. Being built-in to JavaScript means that we will get optimizable, well-maintained implementations that don’t require transmitting, storing, or parsing and jit-optimizing every additional JavaScript code. -### Primary use case: Exact decimal arithmetic for financial calculations +### Exact decimal arithmetic for financial calculations Many currencies tend to be expressed with decimal quantities. Although it’s possible to represent money as integer “cents” (multiply all quantities by 100), this approach runs into a couple of issues: @@ -149,7 +147,7 @@ client.query("SELECT prices FROM data_with_numbers", (err, res) => { }); ``` -#### Goals implied by the main use cases +#### Goals implied by this use case This use case implies the following goals: @@ -160,15 +158,9 @@ This use case implies the following goals: - Be implementable with adequate performance/memory usage for applications - (Please file an issue to mention more requirements) -### Tertiary use case: Numerical calculations on more precise floats - -If it works out reasonably to provide for it within the same proposal, it would also be nice to provide support for higher-precision applications of floating point numbers. - -If Decimal is arbitrary-precision or supports greater precision than Number, it may also be used for applications which need very large floating point numbers, such as astronomical calculations, physics, or even certain games. In some sense, larger or arbitrary-precision binary floats (as supported by [QuickJS](https://bellard.org/quickjs/), or IEEE 754 128-bit/256-bit binary floats) may be more efficient, but Decimal may also be suitable if the need is ultimately for human-consumable, and reproducible, calculations. - ### Language design goals -In addition to the goals which come directly from use cases mentioned above: +In addition to the goals which come directly from the use case mentioned above: - Well-defined semantics, with the same result regardless of which implementation and context a piece of code is run in - Build a consistent story for numerics in JavaScript together with Numbers, BigInt, operator overloading, and @@ -233,7 +225,7 @@ We will use the **Decimal128** data model for JavaScript decimals. Decimal128 is The "BigDecimal" data model is based on unlimited-size decimals (no fixed bith-width), understood exactly as mathematical values. -From the champion group’s perspective, both BigDecimal and Decimal128 are both coherent, valid proposals that would meet the needs of the primary use case. Just looking at the diversity of semantics in other programming languages, and the lack of practical issues that programmers run into, shows us that there are many workable answers here. +From the champion group’s perspective, both BigDecimal and Decimal128 are both coherent, valid proposals that would meet the needs of the main use case. Just looking at the diversity of semantics in other programming languages, and the lack of practical issues that programmers run into, shows us that there are many workable answers here. Operators always calculate their exact answer. In particular, if two BigDecimals are multiplied, the precision of the result may be up to the _sum_ of the operands. For this reason, `BigDecimal.pow` takes a mandatory options object, to ensure that the result does not go out of control in precision. @@ -302,7 +294,7 @@ Decimal canonicalizes when converting to strings and after performing arithmetic - greater-than, greater-than-or-equal - Mantissa, exponent, significand -The library of numerical functions here is kept deliberately minimal. It is based around targeting the primary use case, in which fairly straightforward calculations are envisioned. For scientific or numerical computations, developers may experiment in JavaScript, developing such libraries, and we may decide to standardize these functions in a follow-on proposal; a minimal toolkit of mantissa, exponent, and significand will be available. We currently do not have good insight into the developer needs for this use case, except generically: square roots, exponentiation & logarithms, and trigonometric functions might be needed, but we are not sure if this is a complete list, and which are more important to have than others. In the meantime, one can use the various functions in JavaScript’s `Math` standard library. +The library of numerical functions here is kept deliberately minimal. It is based around targeting the main use case, in which fairly straightforward calculations are envisioned. For scientific or numerical computations, developers may experiment in JavaScript, developing such libraries, and we may decide to standardize these functions in a follow-on proposal; a minimal toolkit of mantissa, exponent, and significand will be available. We currently do not have good insight into the developer needs for this use case, except generically: square roots, exponentiation & logarithms, and trigonometric functions might be needed, but we are not sure if this is a complete list, and which are more important to have than others. In the meantime, one can use the various functions in JavaScript’s `Math` standard library. ### Unsupported operations @@ -324,7 +316,9 @@ Decimal objects can be constructed from Numbers, Strings, and BigInts. Similarly We intend to specify these methods in terms of abstract operations that render a mathematical value as a digit string (in fixed, precision-limited, or exponential form). These abstract operations are written so that the [Amount proposal](https://github.com/tc39/proposal-amount) can reuse them, giving Decimal and Amount a single, consistent notion of how a decimal value becomes a string. -For locale-aware formatting, Decimal leans on Amount. `Decimal.prototype.toLocaleString` is defined to wrap the Decimal value in an Amount, which configures an `Intl.NumberFormat` and calls its `format` method ([#15](https://github.com/tc39/proposal-decimal/issues/15)). This keeps a single source of truth for localization, units, currency, and plural selection in Amount, rather than duplicating that machinery on Decimal. +Decimal also supports locale-aware formatting: `Decimal.prototype.toLocaleString` produces a localized string for a Decimal value, analogous to `Number.prototype.toLocaleString` ([#15](https://github.com/tc39/proposal-decimal/issues/15)). This is a basic need on the web, so Decimal offers it directly rather than requiring another type. + +Decimal and Amount are designed to share the underlying formatting machinery rather than duplicate it; whether `toLocaleString` is specified by building an Amount internally or via common abstract operations is an implementation detail to be settled in the spec. The division of labor is about *what each type carries into the output*: a bare Decimal localizes just a number, whereas Amount localizes a number together with its precision and an optional unit or currency. So when the output needs units, currency, or a preserved display precision, build an Amount — it can take a Decimal, adjust its precision, attach a unit or currency, and format the result with its own `Intl` support; for a plain localized number, Decimal's `toLocaleString` is enough. ## Past discussions in TC39 plenaries From 69892e9231989e028579541c19b6ad377b9f5d44 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Mon, 8 Jun 2026 15:30:24 +0200 Subject: [PATCH 09/17] Continue polishing --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f4a7992..545d8d4f 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ Amount wraps a numeric value with its measurement context (significant or fracti Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript. Currently, developers sometimes represent these using libraries for this purpose, or sometimes use Strings. Sadly, JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding errors. -What’s the issue? Why aren’t JS Numbers good enough? In what sense are they not “exact”? How is it possible that JavaScript's Numbers get something wrong, and have been getting it wrong for so long? +What’s the issue? Why doesn't `0.1 + 0.2` work out to `0.3`? In what sense are they not “exact”? How is it possible that JavaScript's Numbers get something wrong, and have been getting it wrong for so long? As currently defined in JavaScript, Numbers are 64-bit binary floating-point numbers. The conversion from most decimal values to binary floats rarely is an exact match. For instance: the decimal number 0.5 can be exactly represented in binary, but not 0.1; in fact, the the 64-bit floating point number corresponding to 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. Same for 0.2, 0.3, … Statistically, most human-authored decimal numbers cannot be exactly represented as a binary floating-point number (AKA float). -The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. Being built-in to JavaScript means that we will get optimizable, well-maintained implementations that don’t require transmitting, storing, or parsing and jit-optimizing every additional JavaScript code. +The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. Much of that comfort comes from a simple property: decimal numbers should behave the way one was taught arithmetic works, matching the everyday mental model rather than potentially surprising us with binary floating-point gotchas. ### Exact decimal arithmetic for financial calculations From 22fd10c11a288d68d2e29444f7b77e9bc7e37db8 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Mon, 8 Jun 2026 15:40:04 +0200 Subject: [PATCH 10/17] Fix some grammar errors and voice issues --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 545d8d4f..3bcda9c5 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ Amount wraps a numeric value with its measurement context (significant or fracti Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript. Currently, developers sometimes represent these using libraries for this purpose, or sometimes use Strings. Sadly, JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding errors. -What’s the issue? Why doesn't `0.1 + 0.2` work out to `0.3`? In what sense are they not “exact”? How is it possible that JavaScript's Numbers get something wrong, and have been getting it wrong for so long? +What’s the issue? Why doesn't `0.1 + 0.2` work out to `0.3`? In what sense are JS Numbers not “exact”? How is it possible that JavaScript's Numbers get something wrong, and have been getting it wrong for so long? -As currently defined in JavaScript, Numbers are 64-bit binary floating-point numbers. The conversion from most decimal values to binary floats rarely is an exact match. For instance: the decimal number 0.5 can be exactly represented in binary, but not 0.1; in fact, the the 64-bit floating point number corresponding to 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. Same for 0.2, 0.3, … Statistically, most human-authored decimal numbers cannot be exactly represented as a binary floating-point number (AKA float). +As currently defined in JavaScript, Numbers are 64-bit binary floating-point numbers. The conversion from most decimal values to binary floats rarely is an exact match. For instance: the decimal number 0.5 can be exactly represented in binary, but not 0.1; in fact, the 64-bit floating point number corresponding to 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. Same for 0.2, 0.3, … Statistically, most human-authored decimal numbers cannot be exactly represented as a binary floating-point number (AKA float). -The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. Much of that comfort comes from a simple property: decimal numbers should behave the way one was taught arithmetic works, matching the everyday mental model rather than potentially surprising us with binary floating-point gotchas. +The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. Much of that comfort comes from a simple property: decimal numbers should behave the way people already expect them to, matching everyday arithmetic rather than potentially surprising us with binary floating-point gotchas. ### Exact decimal arithmetic for financial calculations From 05b4a35dc8626179f88d7cda04a5e49d2869780a Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 9 Jun 2026 10:14:27 +0200 Subject: [PATCH 11/17] Talk about "normalize on the way out" --- README.md | 60 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3bcda9c5..6626e25d 100644 --- a/README.md +++ b/README.md @@ -16,30 +16,6 @@ The champions welcome your participation in discussing the design space in the i **Stage**: Stage 1 of [the TC39 process](https://tc39.github.io/process-document/). A [draft specification](http://tc39.es/proposal-decimal/) is available. -## Decimal and Amount - -Decimal is designed to work hand-in-hand with the TC39 [Amount proposal](https://github.com/tc39/proposal-amount) (Stage 2). The two proposals split the problem of working with decimal quantities this way: - -**Decimal computes the value** - -and - -**Amount carries a value's precision and unit.** - -Amount wraps a numeric value with its measurement context (significant or fraction digits), as well as, optionally, a unit (including a currency). Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision and has no concept of units. Decimal canonicalizes trailing zeroes away (so `1.20` and `1.2` are equal), which is exactly the information Amount preserves. Decimal values can be rendered in a locale-sensitive way with `toLocaleString`. Amount is what adds the precision and unit/currency data, and it can take a Decimal to do so. - -| Concern | Owner | -|---|---| -| Exact `+`, `−`, `×`, `÷`, remainder, and rounding | **Decimal** | -| Comparisons | **Decimal** | -| Extracting mantissa/exponent/significand | **Decimal** | -| Handling significant/fraction digits (including trailing zeroes) | **Amount** | -| Units and currency | **Amount** | -| Plain string rendering (`toString`, `toFixed`, `toPrecision`, `toExponential`) | **Decimal** (via shared rendering AOs that Amount can reuse) | -| Locale-aware formatting (`toLocaleString`) | **Decimal** for a bare number; **Amount** when units, currency, or display precision are involved | -| Plural selection | **Amount** (Decimal may lose relevant data) | -| Unit conversion | **Amount** | - ## Use case and goals Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript. Currently, developers sometimes represent these using libraries for this purpose, or sometimes use Strings. Sadly, JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding @@ -125,9 +101,9 @@ In other words, with JS increasingly being used in contexts and scenarios where JavaScript is often the glue between systems—databases, foreign-function interfaces, other services—that natively support decimal numbers. Decimal lets you take that data and compute on it exactly, preserving its mathematical value. -Note the scope. Decimal preserves *value*, not representation: it canonicalizes trailing zeroes away (`1.20` becomes `1.2`). So if you only need to transport a value untouched, a String already does that losslessly; and if you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate*. +Note that Decimal preserves *value*, not representation: Trailing zeroes of a decimal String, converted to a Decimal, aren't exposed, so `1.20` and `1.2` are indistinguishable. Implementations of Decimal may indeed preserve that information, but the API here does not expose any properties or functions that would allow one to detect this difference. So if you only need to transport a value untouched, a String already does that losslessly. If you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate* and preserve mathematical value. -The following is fictional, but illustrates the idea. Note the `sql_decimal` configuration option, and how values returned from the DB arrive as Decimal values—ready for exact arithmetic—rather than as strings or JS `Number`s: +The following example illustrates the idea. Note the `sql_decimal` configuration option, and how values returned from the DB arrive as Decimal values—ready for exact arithmetic—rather than as strings or JS `Number`s: ```js const { Client } = require("pg"); @@ -272,7 +248,9 @@ Decimal is based on IEEE 754-2019 Decimal128, which is a standard for base-10 de These special values are exposed as ordinary Decimal values rather than causing exceptions, so that computation can continue in exceptional conditions. -Decimal canonicalizes when converting to strings and after performing arithmetic operations. This means that Decimals do not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2. This is a deliberate omission from the capabilities defined by IEEE 754 Decimal128: trailing zeroes are display precision, not arithmetic, and tracking them is precisely the job of [Amount](https://github.com/tc39/proposal-amount). (Relatedly, the [keep trailing zeroes proposal](https://github.com/tc39/proposal-intl-keep-trailing-zeros), now at Stage 3, ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings.) +Decimal does not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2: equality compares mathematical values (so `1.20` and `1.2` are equal), and `toString` always renders a normalized String. Crucially, this is a guarantee about what Decimal *exposes*, not a requirement on how a value is *stored*. An implementation is free to keep values unnormalized internally — preserving trailing zeroes — and to normalize only on the way out, when rendering a String or comparing values. A motto that captures this is "normalize on the way out". + +This hybrid stance is deliberate. Mike Cowlishaw, an author of IEEE 754's decimal arithmetic, explains in the [Decimal Arithmetic FAQ](https://speleotrove.com/decimal/decifaq4.html) why decimal arithmetic is typically performed *unnormalized*: addition and subtraction often need no alignment shifts, the coefficient and exponent computations stay independent, and results match what users expect from manual calculation. Because Decimal never exposes whether a value is normalized, an implementation can reap essentially all of those benefits (mainly performance) without forcing callers to reason about trailing zeroes. Not exposing trailing zeroes is, in turn, a deliberate omission from the capabilities defined by IEEE 754: trailing zeroes are display precision, not arithmetic, and tracking them is precisely the job of [Amount](https://github.com/tc39/proposal-amount). (Relatedly, the [keep trailing zeroes proposal](https://github.com/tc39/proposal-intl-keep-trailing-zeros), now at Stage 3, ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings.) ### Operator semantics @@ -404,12 +382,38 @@ Therefore, this proposal does not contain any options to set the precision from Mike Cowlishaw’s excellent [Decimal FAQ](http://speleotrove.com/decimal/decifaq.html) explains many of the core design principles for decimal data types, which this proposal attempts to follow. -One notable exception is supporting trailing zeroes: Although Mike presents some interesting use cases, the Decimal champion group does not see these as being worth the complexity both for JS developers and implementors. Instead, Decimal values could be lossly represented as rationals, and are “canonicalized”. +One notable point concerns *exposing* trailing zeroes: although Mike presents some interesting use cases, the Decimal champion group does not see these as being worth the complexity both for JS developers and implementors. Decimal therefore never exposes whether a value carries trailing zeroes. `1.20` and `1.2` are indistinguishable here. As Mike explains in [FAQ part 4: “Why is decimal arithmetic unnormalized?”](https://speleotrove.com/decimal/decifaq4.html), the underlying arithmetic is most naturally performed on *unnormalized* values. This proposal takes a hybrid approach: implementations may store and compute on unnormalized values, thereby keeping the performance and simplicity benefits Mike describes, and need only “normalize on the way out” when rendering a String or comparing values. ## Relationship of Decimal to other TC39 proposals +### BigInt + This proposal can be seen as a follow-on to [BigInt](https://github.com/tc39/proposal-bigint/), which brought arbitrary-sized integers to JavaScript, and will be fully standardized in ES2020. However, unlike BigInt, Decimal (i) does not propose to intrduce a new primitive data type, (ii) does not propose operator overloading (which BigInt does support), and (iii) does not offer new syntax (numeric literla), which BigInt does add (e.g., `2345n`). +### Amount + +Decimal is designed to work hand-in-hand with the TC39 [Amount proposal](https://github.com/tc39/proposal-amount) (Stage 2). The two proposals split the problem of working with decimal quantities this way: + +**Decimal computes the value** + +and + +**Amount carries a value's precision and unit.** + +Amount wraps a numeric value with its measurement context (significant or fraction digits), as well as, optionally, a unit (including a currency). Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision and has no concept of units. Decimal does not expose trailing zeroes (so `1.20` and `1.2` are equal), which is exactly the information Amount preserves. Decimal values can be rendered in a locale-sensitive way with `toLocaleString`. Amount is what adds the precision and unit/currency data, and it can take a Decimal to do so. + +| Concern | Owner | +|---|---| +| Exact `+`, `−`, `×`, `÷`, remainder, and rounding | **Decimal** | +| Comparisons | **Decimal** | +| Extracting mantissa/exponent/significand | **Decimal** | +| Handling significant/fraction digits (including trailing zeroes) | **Amount** | +| Units and currency | **Amount** | +| Plain string rendering (`toString`, `toFixed`, `toPrecision`, `toExponential`) | **Decimal** (via shared rendering AOs that Amount can reuse) | +| Locale-aware formatting (`toLocaleString`) | **Decimal** for a bare number; **Amount** when units, currency, or display precision are involved | +| Plural selection | **Amount** (Decimal may lose relevant data) | +| Unit conversion | **Amount** | + ## Implementations - Experimental implementation in [QuickJS](https://bellard.org/quickjs/), from release 2020-01-05 (use the `--bignum` flag) From 3f093938d7d787f4ff073d217ccfe7c28bf486f3 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 9 Jun 2026 10:15:57 +0200 Subject: [PATCH 12/17] Talk about normalization on the way out --- docs/data-model.md | 14 +++++++------- docs/decimal.md | 2 +- docs/index.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/data-model.md b/docs/data-model.md index d8c2fc46..dd9746a3 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -60,19 +60,19 @@ posZero.equals(negZero); // => true Object.is(posZero, negZero); // => false ``` -### Canonicalization +### Normalization on the way out -Decimal values are canonicalized, meaning different representations of the same mathematical value are normalized: +Decimal never exposes trailing zeroes: different representations of the same mathematical value are indistinguishable. Equality compares mathematical values, and `toString` always renders a normalized String: ```javascript -// These all become the same Decimal value +// These all expose the same Decimal value new Decimal("1.20").toString(); // => "1.2" new Decimal("1.200").toString(); // => "1.2" new Decimal("01.2").toString(); // => "1.2" new Decimal("1.2e0").toString(); // => "1.2" ``` -This is a deliberate departure from the full IEEE 754 standard, which preserves trailing zeros. +This is a guarantee about what Decimal *exposes*, not a requirement on how a value is *stored*. An implementation may keep values unnormalized internally — preserving trailing zeroes, as the full IEEE 754 standard does — and normalize only on the way out, when rendering a String or comparing values ("normalize on the way out"). This hybrid approach captures the performance benefits of [unnormalized decimal arithmetic](https://speleotrove.com/decimal/decifaq4.html) without exposing display precision to callers. ## Arithmetic Operations @@ -171,10 +171,10 @@ const decimal = new Decimal("10.5"); This simplifies the implementation and avoids syntax complexity. -### Canonicalization vs Cohort Preservation +### Cohorts -IEEE 754 supports the concept of "cohorts", which are different representations of the same value (e.g., 1.20 vs 1.2). The Decimal proposal canonicalizes values, not preserving cohorts: +IEEE 754 supports the concept of "cohorts", which are different representations of the same value (e.g., 1.20 vs 1.2). An implementation of Decimal may preserve cohort members internally, but it never *exposes* which member a value belongs to. Trailing zeroes are thus not observable: ```javascript -new Decimal("1.20").toString(); // => "1.2" (trailing zero lost) +new Decimal("1.20").toString(); // => "1.2" ``` diff --git a/docs/decimal.md b/docs/decimal.md index 55223025..c689c271 100644 --- a/docs/decimal.md +++ b/docs/decimal.md @@ -12,7 +12,7 @@ total.toString(); // => "21.64" ``` -`Decimal` values are canonicalized, meaning that different representations of the same mathematical value are normalized. For example, "1.20" and "1.2" both result in the same `Decimal` value. +`Decimal` never exposes trailing zeroes: different representations of the same mathematical value are indistinguishable. For example, "1.20" and "1.2" compare as equal and render identically with `toString`. (An implementation may store values unnormalized internally and normalize only on the way out, when rendering or comparing.) ## Constructor diff --git a/docs/index.md b/docs/index.md index ec74d31d..5ef3998e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ Decimal uses a subset of the IEEE 754-2019 Decimal128 specification: - **128-bit representation** allowing up to 34 significant digits - **Base-10 exponent** range of -6143 to +6144 - **Special values**: NaN, positive and negative infinity for compatibility with IEEE 754 and JS's `Number` -- **Canonicalization**: values are normalized (e.g., "1.20" becomes "1.2") +- **Normalization on the way out**: trailing zeroes are never exposed, so "1.20" and "1.2" are indistinguishable (implementations may store values unnormalized internally) For a detailed explanation of the data model and design decisions, see [Data Model Documentation](./data-model.md). From 13ea6cb73aaeabf1d307f1f74c360c85a6915960 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 9 Jun 2026 11:11:51 +0200 Subject: [PATCH 13/17] Merge RELATED.md into README.md --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++- RELATED.md | 78 ------------------------------------------------------ 2 files changed, 59 insertions(+), 79 deletions(-) delete mode 100644 RELATED.md diff --git a/README.md b/README.md index 6626e25d..d6078b84 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,47 @@ Many languages added decimal support 10+ years ago. The IEEE 754-2008 standard f Moreover, having multiple numeric types in a language is not unusual. Languages ship with integers, floats, *and* decimals. (Some even have more numbers than that, such as built-in support for rationals.) Python even includes both decimals and rationals. The existence of JS's `Number` and `BigInt` doesn't preclude `Decimal`. Moreover, the need for decimal types across many languages reflects a genuine, universal need rather than a niche requirement. +### A fuller survey of decimal and related types + +The table above is deliberately short. Decimal, rationals, and similar features are in fact pervasive across standards, programming languages, databases, libraries, and even hardware. Below is a partial catalog, with a brief summary of the semantics in each. + +- Standards + - **IEEE 754**-2008 and later: 32-bit, 64-bit and 128-bit decimal; see [explanation](https://en.wikipedia.org/wiki/Decimal_floating_point#IEEE_754-2008_encoding) (recommends against using 32-bit decimal) + - (plus many of the below are standardized) +- Programming languages + - Fixed-size decimals: + - **[C](http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1312.pdf)**: 32, 64 and 128-bit IEE 754 decimal types, with a global settings object. Still a proposal, but has a GCC implementation. + - **[C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3871.html)**: Early proposal work in progress, to be based on IEEE 64 and 128-bit decimal. Still a proposal, but has a GCC implementation. + - **[C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/decimal)**/**[.NET]()**: Custom 128-bit decimal semantics with slightly different sizes for the mantissa vs exponent compared to IEEE. + - **[Swift](https://developer.apple.com/documentation/foundation/decimal)**/**[Obj-C](https://developer.apple.com/documentation/foundation/nsdecimal?language=objc)**: Yet another custom semantics for fixed-bit-size floating point decimal. + - Global settings for setting decimal precision + - **[Python](https://docs.python.org/2/library/decimal.html)**: Decimal with global settings to set precision. + - Rationals + - **[Perl6](https://docs.perl6.org/type/Rat)**: Literals like `1.5` are Rat instances! + - **[Common Lisp](http://www.lispworks.com/documentation/lw50/CLHS/Body/t_ratio.htm#ratio)**: Ratios live alongside floats; no decimal data type + - **[Scheme](http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.2)**: Analogous to Common Lisp, with different names for types (Racket is similar) + - **[Ruby](https://ruby-doc.org/core-2.6.5/Rational.html)**: Rational class alongside BigDecimal. + - Arbitrary-precision decimals (this proposal) + - **[Ruby](https://ruby-doc.org/stdlib-2.4.0/libdoc/bigdecimal/rdoc/BigDecimal.html)**: Arbitrary-precision Decimal, alongside Rational. + - **[PHP](http://php.net/manual/en/book.bc.php)**: A set of functions to bind to bc for mathematical calculations. An alternative community-driven [Decimal library](https://php-decimal.io/) is also available. + - **[Java](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/math/BigDecimal.html)**: Arbitrary-precision decimal based on objects and methods. Requires rounding modes and precision parameters for operations like division +- Databases + - Decimal with precision configurable in the schema + - [Microsoft SQL Server](https://docs.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql) + - [PostgreSQL](https://www.postgresql.org/docs/9.1/static/datatype-numeric.html) + - [MySQL](https://dev.mysql.com/doc/refman/5.7/en/precision-math-decimal-characteristics.html) + - IEEE 754-2008 decimal + - Bloomberg's [comdb2](https://bloomberg.github.io/comdb2/decimals.html) + - [MongoDB](https://docs.mongodb.com/manual/core/shell-types/#shell-type-decimal) +- Libraries + - Intel C [inteldfp](https://software.intel.com/en-us/articles/intel-decimal-floating-point-math-library): IEEE decimal + - Bloomberg C++ [bdldfp](https://github.com/bloomberg/bde/blob/master/groups/bdl/bdldfp/bdldfp_decimal.h): IEEE decimal + - IBM C [decnumber](http://speleotrove.com/decimal/decnumber.html): Configurable context with precision, rounding mode + - Rust crates [[1]](https://crates.io/crates/decimal) [[2]](https://crates.io/crates/bigdecimal) +- Hardware (all implementing IEEE decimal) + - [POWER6]() + - [RISC-V](https://en.wikichip.org/wiki/risc-v/standard_extensions) (planned) + ### Why JavaScript Lacks Decimal Support JavaScript's origins as a lightweight browser scripting language meant it initially shipped with minimal numeric support (just IEEE 754 binary floats). However, JavaScript's role has fundamentally changed. Back in the 1990s, JS was focused on form validation and DOM manipulation. Now we have full-stack applications, including financial systems, e-commerce platforms, serverless backends, and data processing pipelines. The language has evolved to meet modern needs but decimal arithmetic remains an important gap. @@ -189,6 +230,8 @@ Because JavaScript lacks native decimal support, developers have created numerou There are also money-specific libraries such as [dinero.js](https://www.npmjs.com/package/dinero.js) with about 180K weekly NPM downloads. Each implementation has slightly different semantics, requires coordination across libraries, adds to total bundle size, presumably performs worse than native implementations could, and creates interoperability challenges. We believe Decimal will standardize existing practice. Developers are already using decimal libraries, converting numbers to "cents" (error-prone, confusing), using `Number` incorrectly/unsoundly (causing bugs). Decimal doesn't ask developers to adopt something new; it offers a better way to do what they're already struggling to do. +The three most popular libraries above are each by [MikeMcl](https://github.com/mikemcl). They have some [interesting differences](https://github.com/MikeMcl/big.js/wiki), but also share a common shape: an object-and-method API, with rounding modes and precision limits configured on the constructor, and support for inherently rounding operations like square root, exponentiation, and division. We plan to learn from developers' experiences with these and other ecosystem libraries as we refine Decimal's design; the discussion continues in [#22](https://github.com/littledan/proposal-bigdecimal/issues/22). + ## Specification and standards Based on feedback from JS developers, engine implementors, and the members of the TC39 committee, we have a concrete proposal. Please see the [spec text](https://github.com/tc39/proposal-decimal/blob/main/spec.emu) ([HTML version](https://github.com/tc39/proposal-decimal/blob/main/spec.emu)). are provided below. You’re encouraged to join the discussion by commenting on the issues linked below or [filing your own](https://github.com/tc39/proposal-decimal/issues/new). @@ -300,6 +343,14 @@ Decimal and Amount are designed to share the underlying formatting machinery rat ## Past discussions in TC39 plenaries +Decimal has been under discussion in TC39 for a very long time, with proposals and feedback from many people including Sam Ruby, Mike Cowlishaw, Brendan Eich, Waldemar Horwat, Maciej Stachowiak, Dave Herman and Mark Miller. The earliest threads predate the modern proposal process: + +- A new `decimal` type was long planned for ES4, see [Proposed ECMAScript 4th Edition – Language Overview](https://www.ecma-international.org/activities/Languages/Language%20overview.pdf) +- In the following ES3.1/ES5 effort, discussions about a decimal type continued on es-discuss, e.g., [[1]](https://mail.mozilla.org/pipermail/es-discuss/2008-August/007244.html) [[2]](https://mail.mozilla.org/pipermail/es-discuss/2008-September/007466.html) +- Decimal was discussed at length in the development of ES6. It was eventually rolled into the broader typed objects/value types effort, which didn't make it into ES6, but is being incrementally developed now. + +More recently, Decimal has been on the TC39 plenary agenda repeatedly: + - [Decimal for stage 0](https://github.com/tc39/notes/blob/main/meetings/2017-11/nov-29.md#9ivb-decimal-for-stage-0) (November, 2017) - [BigDecimal for Stage 1](https://github.com/tc39/notes/blob/main/meetings/2020-02/february-4.md) (February, 2020) - [Decimal update](https://github.com/tc39/notes/blob/main/meetings/2020-03/march-31.md) (March, 2020) @@ -366,7 +417,7 @@ One option that’s raised is allowing for greater precision in more capable env ### How does this proposal relate to other TC39 proposals like operator overloading? -See [RELATED.md](./RELATED.md) for details. +Operator overloading is covered under [Future work](#arithmetic-operator-and-comparison-overloading): the first version of Decimal deliberately does *not* overload `+`, `<`, and friends, though the door is not permanently closed. The relationship to BigInt, Amount, and the other allied proposals is covered in [Relationship of Decimal to other TC39 proposals](#relationship-of-decimal-to-other-tc39-proposals). ### Why not have the maximum precision or default rounding mode set by the environment? @@ -414,6 +465,13 @@ Amount wraps a numeric value with its measurement context (significant or fracti | Plural selection | **Amount** (Decimal may lose relevant data) | | Unit conversion | **Amount** | +### Other allied proposals + +Decimal and Amount sit within a small family of TC39 proposals that together cover working with decimal quantities. Beyond Amount: + +- [**Keep trailing zeroes**](https://github.com/tc39/proposal-intl-keep-trailing-zeros) (Stage 3): ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings (e.g., unintentionally normalizing `"1.20"` to `"1.2"`). +- [**Smart Units**](https://github.com/tc39/proposal-smart-unit-preferences): a follow-on to Amount for locale- and usage-aware unit preferences and conversion. + ## Implementations - Experimental implementation in [QuickJS](https://bellard.org/quickjs/), from release 2020-01-05 (use the `--bignum` flag) diff --git a/RELATED.md b/RELATED.md deleted file mode 100644 index 095459f5..00000000 --- a/RELATED.md +++ /dev/null @@ -1,78 +0,0 @@ -## History and related work - -The need for accurately representing decimal quantities is not new or unique to our current circumstances. That's why there are a number of popular JS ecosystem libraries for decimal, why many other programming languages, databases and standards have built-in data types for this purpose, and why TC39 has been considering adding Decimal for at least 12 years. - -### Allied TC39 proposals - -Decimal is part of a small family of TC39 proposals that, together, cover working with decimal quantities. Decimal supplies the exact-arithmetic core; the others handle measurement context and formatting: - -- [**Amount**](https://github.com/tc39/proposal-amount) (Stage 2): a wrapper around a numeric value and an optional unit/currency, with support for precision (significant/fraction digits). Amount deliberately does no arithmetic, and Decimal deliberately tracks neither display precision nor units. See the [Decimal and Amount](./README.md#decimal-and-amount) section of the README for the division of labor. -- [**Keep trailing zeroes**](https://github.com/tc39/proposal-intl-keep-trailing-zeros) (Stage 3): ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings (e.g., unintentionally normalizing `"1.20"` to `"1.2"`). -- [**Smart Units**](https://github.com/tc39/proposal-smart-unit-preferences): a follow-on to Amount for locale- and usage-aware unit preferences and conversion. - -### Related JS ecosystem libraries - -JavaScript programmers are using decimal data types today with various ecosystem libraries. The most popular three on npm are each by [MikeMcl](https://github.com/mikemcl): - -- [decimal.js](http://mikemcl.github.io/decimal.js/) -- [big.js](https://mikemcl.github.io/big.js/) -- [bignumber.js](https://mikemcl.github.io/bignumber.js/) - -These packages have some [interesting differences](https://github.com/MikeMcl/big.js/wiki), but there are also many similarities: - -- APIs are based on JavaScript objects and method calls -- Rounding modes and precision limits are settings in the constructor -- Inherently rounding operations like sqrt, exponentiation, division are supported - -The initial proposal in this document suggests some differences, described above. - -We plan to investigate the experiences and feedback developers have had with these and other existing JavaScript libraries so that we can learn from them in the design of BigDecimal. The discussion continues in [#22](https://github.com/littledan/proposal-bigdecimal/issues/22). - -### Related features in other systems - -Due to the overwhelming evidence listed above that decimal is an important data type in programming, many different programming languages, databases and standards include decimal, rationals, or similar features. Below is a partial listing, with a summary of the semantics in each. - -- Standards - - **IEEE 754**-2008 and later: 32-bit, 64-bit and 128-bit decimal; see [explanation](https://en.wikipedia.org/wiki/Decimal_floating_point#IEEE_754-2008_encoding) (recommends against using 32-bit decimal) - - (plus many of the below are standardized) -- Programming languages - - Fixed-size decimals: - - **[C](http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1312.pdf)**: 32, 64 and 128-bit IEE 754 decimal types, with a global settings object. Still a proposal, but has a GCC implementation. - - **[C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3871.html)**: Early proposal work in progress, to be based on IEEE 64 and 128-bit decimal. Still a proposal, but has a GCC implementation. - - **[C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/decimal)**/**[.NET]()**: Custom 128-bit decimal semantics with slightly different sizes for the mantissa vs exponent compared to IEEE. - - **[Swift](https://developer.apple.com/documentation/foundation/decimal)**/**[Obj-C](https://developer.apple.com/documentation/foundation/nsdecimal?language=objc)**: Yet another custom semantics for fixed-bit-size floating point decimal. - - Global settings for setting decimal precision - - **[Python](https://docs.python.org/2/library/decimal.html)**: Decimal with global settings to set precision. - - Rationals - - **[Perl6](https://docs.perl6.org/type/Rat)**: Literals like `1.5` are Rat instances! - - **[Common Lisp](http://www.lispworks.com/documentation/lw50/CLHS/Body/t_ratio.htm#ratio)**: Ratios live alongside floats; no decimal data type - - **[Scheme](http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.2)**: Analogous to Common Lisp, with different names for types (Racket is similar) - - **[Ruby](https://ruby-doc.org/core-2.6.5/Rational.html)**: Rational class alongside BigDecimal. - - Arbitrary-precision decimals (this proposal) - - **[Ruby](https://ruby-doc.org/stdlib-2.4.0/libdoc/bigdecimal/rdoc/BigDecimal.html)**: Arbitrary-precision Decimal, alongside Rational. - - **[PHP](http://php.net/manual/en/book.bc.php)**: A set of functions to bind to bc for mathematical calculations. An alternative community-driven [Decimal library](https://php-decimal.io/) is also available. - - **[Java](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/math/BigDecimal.html)**: Arbitrary-precision decimal based on objects and methods. Requires rounding modes and precision parameters for operations like division -- Databases - - Decimal with precision configurable in the schema - - [Microsoft SQL Server](https://docs.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql) - - [PostgreSQL](https://www.postgresql.org/docs/9.1/static/datatype-numeric.html) - - [MySQL](https://dev.mysql.com/doc/refman/5.7/en/precision-math-decimal-characteristics.html) - - IEEE 754-2008 decimal - - Bloomberg's [comdb2](https://bloomberg.github.io/comdb2/decimals.html) - - [MongoDB](https://docs.mongodb.com/manual/core/shell-types/#shell-type-decimal) -- Libraries - - Intel C [inteldfp](https://software.intel.com/en-us/articles/intel-decimal-floating-point-math-library): IEEE decimal - - Bloomberg C++ [bdldfp](https://github.com/bloomberg/bde/blob/master/groups/bdl/bdldfp/bdldfp_decimal.h): IEEE decimal - - IBM C [decnumber](http://speleotrove.com/decimal/decnumber.html): Configurable context with precision, rounding mode - - Rust crates [[1]](https://crates.io/crates/decimal) [[2]](https://crates.io/crates/bigdecimal) -- Hardware (all implementing IEEE decimal) - - [POWER6]() - - [RISC-V](https://en.wikichip.org/wiki/risc-v/standard_extensions) (planned) - -### History of discussion of decimal in TC39 - -Decimal has been under discussion in TC39 for a very long time, with proposals and feedback from many people including Sam Ruby, Mike Cowlishaw, Brendan Eich, Waldemar Horwat, Maciej Stachowiak, Dave Herman and Mark Miller. - -- A new `decimal` type was long planned for ES4, see [Proposed ECMAScript 4th Edition – Language Overview](https://www.ecma-international.org/activities/Languages/Language%20overview.pdf) -- In the following ES3.1/ES5 effort, discussions about a decimal type continued on es-discuss, e.g., [[1]](https://mail.mozilla.org/pipermail/es-discuss/2008-August/007244.html) [[2]](https://mail.mozilla.org/pipermail/es-discuss/2008-September/007466.html) -- Decimal was discussed at length in the development of ES6. It was eventually rolled into the broader typed objects/value types effort, which didn't make it into ES6, but is being incrementally developed now (see the below section about relationship to other TC39 proposals). From 296917318304be04fbe416c9ac4a832a28b26d2e Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 9 Jun 2026 11:23:17 +0200 Subject: [PATCH 14/17] Keep polishing --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d6078b84..8df08921 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The TC39 Decimal proposal aims to add exact decimal arithmetic to JavaScript, eliminating the rounding errors that occur with binary floating-point numbers. -There are two ways to see why this matters. The practical one is that end-user-visible bugs come from using binary floating-point Numbers for calculations that are intended to be in decimal arithmetic. The other is more foundational: we want a numeric type that behaves the way we were taught arithmetic works — `0.1 + 0.2` is `0.3`. Decimal is, in essence, a *do-what-I-mean* (DWIM) approach to numbers: the value and the everyday mental model line up, with no hidden binary representation oddities. +There are two ways to see why this matters. The practical one is that end-user-visible bugs come from using binary floating-point Numbers for calculations that are intended to be in decimal arithmetic. The other is more foundational: we want a numeric type that behaves the way we were taught arithmetic works: `0.1 + 0.2` is `0.3`. Decimal is, in essence, a *do-what-I-mean* (DWIM) approach to numbers: the value and the everyday mental model line up, with no hidden binary representation oddities. The champions welcome your participation in discussing the design space in the issues linked above. **We are seeking input for your needs around JavaScript decimal in [this survey](https://forms.gle/A2YaTr3Tn1o3D7hdA).** @@ -65,13 +65,13 @@ console.log(total.toFixed(2)); Let's convert USD to EUR, given the exchange rate EUR --> USD. Decimal does the exact arithmetic; we then hand the result to [Amount](https://github.com/tc39/proposal-amount) to round it for display, attach the currency, and localize it. ```js -// Exact arithmetic — Decimal's job: +// Exact arithmetic is Decimal's job: let exchangeRateEurToUsd = new Decimal("1.09"); let amountInUsd = new Decimal("450.27"); let exchangeRateUsdToEur = new Decimal("1").divide(exchangeRateEurToUsd); let amountInEur = amountInUsd.multiply(exchangeRateUsdToEur); -// Presentation — Amount's job (precision + currency + locale): +// Presentation is Amount's job (precision + currency + locale): let display = new Amount(amountInEur.toString(), { unit: "EUR", fractionDigits: 2, @@ -101,7 +101,7 @@ In other words, with JS increasingly being used in contexts and scenarios where JavaScript is often the glue between systems—databases, foreign-function interfaces, other services—that natively support decimal numbers. Decimal lets you take that data and compute on it exactly, preserving its mathematical value. -Note that Decimal preserves *value*, not representation: Trailing zeroes of a decimal String, converted to a Decimal, aren't exposed, so `1.20` and `1.2` are indistinguishable. Implementations of Decimal may indeed preserve that information, but the API here does not expose any properties or functions that would allow one to detect this difference. So if you only need to transport a value untouched, a String already does that losslessly. If you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate* and preserve mathematical value. +Note that Decimal preserves *value*, not representation: Trailing zeroes of a decimal String, converted to a Decimal, aren't exposed, so `1.20` and `1.2` are indistinguishable. Implementations of Decimal may indeed preserve that information, but the Decimal API has no properties or functions that would allow one to detect this difference. So if you only need to transport a value untouched, a String already does that losslessly. If you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate* and preserve mathematical value. The following example illustrates the idea. Note the `sql_decimal` configuration option, and how values returned from the DB arrive as Decimal values—ready for exact arithmetic—rather than as strings or JS `Number`s: @@ -291,7 +291,7 @@ Decimal is based on IEEE 754-2019 Decimal128, which is a standard for base-10 de These special values are exposed as ordinary Decimal values rather than causing exceptions, so that computation can continue in exceptional conditions. -Decimal does not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2: equality compares mathematical values (so `1.20` and `1.2` are equal), and `toString` always renders a normalized String. Crucially, this is a guarantee about what Decimal *exposes*, not a requirement on how a value is *stored*. An implementation is free to keep values unnormalized internally — preserving trailing zeroes — and to normalize only on the way out, when rendering a String or comparing values. A motto that captures this is "normalize on the way out". +Decimal does not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2: equality compares mathematical values (so `1.20` and `1.2` are equal), and `toString` always renders a normalized String. Crucially, this is a guarantee about what Decimal *exposes*, not a requirement on how a value is *stored*. An implementation is free to keep values unnormalized internally and to normalize only on the way out, when rendering a String or comparing values. A motto that captures this is "normalize on the way out". This hybrid stance is deliberate. Mike Cowlishaw, an author of IEEE 754's decimal arithmetic, explains in the [Decimal Arithmetic FAQ](https://speleotrove.com/decimal/decifaq4.html) why decimal arithmetic is typically performed *unnormalized*: addition and subtraction often need no alignment shifts, the coefficient and exponent computations stay independent, and results match what users expect from manual calculation. Because Decimal never exposes whether a value is normalized, an implementation can reap essentially all of those benefits (mainly performance) without forcing callers to reason about trailing zeroes. Not exposing trailing zeroes is, in turn, a deliberate omission from the capabilities defined by IEEE 754: trailing zeroes are display precision, not arithmetic, and tracking them is precisely the job of [Amount](https://github.com/tc39/proposal-amount). (Relatedly, the [keep trailing zeroes proposal](https://github.com/tc39/proposal-intl-keep-trailing-zeros), now at Stage 3, ensures that `Intl` does not silently strip trailing zeroes when formatting digit strings.) @@ -339,7 +339,7 @@ We intend to specify these methods in terms of abstract operations that render a Decimal also supports locale-aware formatting: `Decimal.prototype.toLocaleString` produces a localized string for a Decimal value, analogous to `Number.prototype.toLocaleString` ([#15](https://github.com/tc39/proposal-decimal/issues/15)). This is a basic need on the web, so Decimal offers it directly rather than requiring another type. -Decimal and Amount are designed to share the underlying formatting machinery rather than duplicate it; whether `toLocaleString` is specified by building an Amount internally or via common abstract operations is an implementation detail to be settled in the spec. The division of labor is about *what each type carries into the output*: a bare Decimal localizes just a number, whereas Amount localizes a number together with its precision and an optional unit or currency. So when the output needs units, currency, or a preserved display precision, build an Amount — it can take a Decimal, adjust its precision, attach a unit or currency, and format the result with its own `Intl` support; for a plain localized number, Decimal's `toLocaleString` is enough. +Decimal and Amount are designed to share the underlying formatting machinery rather than duplicate it; whether `toLocaleString` is specified by building an Amount internally or via common abstract operations is an implementation detail to be settled in the spec. The division of labor is about *what each type carries into the output*: a bare Decimal localizes just a number, whereas Amount localizes a number together with its precision and an optional unit or currency. So when the output needs units, currency, or a preserved display precision, build an Amount. It can take a Decimal, adjust its precision, attach a unit or currency, and format the result with its own `Intl` support; for a plain localized number, Decimal's `toLocaleString` is enough. ## Past discussions in TC39 plenaries @@ -433,7 +433,7 @@ Therefore, this proposal does not contain any options to set the precision from Mike Cowlishaw’s excellent [Decimal FAQ](http://speleotrove.com/decimal/decifaq.html) explains many of the core design principles for decimal data types, which this proposal attempts to follow. -One notable point concerns *exposing* trailing zeroes: although Mike presents some interesting use cases, the Decimal champion group does not see these as being worth the complexity both for JS developers and implementors. Decimal therefore never exposes whether a value carries trailing zeroes. `1.20` and `1.2` are indistinguishable here. As Mike explains in [FAQ part 4: “Why is decimal arithmetic unnormalized?”](https://speleotrove.com/decimal/decifaq4.html), the underlying arithmetic is most naturally performed on *unnormalized* values. This proposal takes a hybrid approach: implementations may store and compute on unnormalized values, thereby keeping the performance and simplicity benefits Mike describes, and need only “normalize on the way out” when rendering a String or comparing values. +One notable point concerns *exposing* trailing zeroes: the Decimal champion group does not see these as being worth revealing to JS programmers. As Mike explains in [FAQ part 4: “Why is decimal arithmetic unnormalized?”](https://speleotrove.com/decimal/decifaq4.html), the underlying arithmetic is most naturally performed on *unnormalized* values. This proposal takes a hybrid approach: implementations may store and compute on unnormalized values, thereby keeping the performance and simplicity benefits Mike describes, and need only “normalize on the way out” when rendering a String or comparing values. ## Relationship of Decimal to other TC39 proposals @@ -451,7 +451,7 @@ and **Amount carries a value's precision and unit.** -Amount wraps a numeric value with its measurement context (significant or fraction digits), as well as, optionally, a unit (including a currency). Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision and has no concept of units. Decimal does not expose trailing zeroes (so `1.20` and `1.2` are equal), which is exactly the information Amount preserves. Decimal values can be rendered in a locale-sensitive way with `toLocaleString`. Amount is what adds the precision and unit/currency data, and it can take a Decimal to do so. +Amount wraps a numeric value with its measurement context (significant or fraction digits), as well as, optionally, a unit (including a currency). Amount deliberately does *not* perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision and has no concept of units. Decimal values can be rendered in a locale-sensitive way with `toLocaleString`. Amount is what adds the precision and unit/currency data, and it can take a Decimal to do so. | Concern | Owner | |---|---| From eed8324fb64d552936d2ecebdd20f74cfa0ef372 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 9 Jun 2026 11:35:51 +0200 Subject: [PATCH 15/17] Keep polishing --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8df08921..dbb90edb 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ What’s the issue? Why doesn't `0.1 + 0.2` work out to `0.3`? In what sense are As currently defined in JavaScript, Numbers are 64-bit binary floating-point numbers. The conversion from most decimal values to binary floats rarely is an exact match. For instance: the decimal number 0.5 can be exactly represented in binary, but not 0.1; in fact, the 64-bit floating point number corresponding to 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. Same for 0.2, 0.3, … Statistically, most human-authored decimal numbers cannot be exactly represented as a binary floating-point number (AKA float). -The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. Much of that comfort comes from a simple property: decimal numbers should behave the way people already expect them to, matching everyday arithmetic rather than potentially surprising us with binary floating-point gotchas. +The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate. ### Exact decimal arithmetic for financial calculations @@ -93,7 +93,7 @@ In all of these environments, the lack of decimal number support means that vari - An external library could be used instead (introducing issues about choosing the library, coordinating on its use). - Calculations could be in terms of “cents” (fallible, as explained above) -- In some cases, developers end up using Number anyway, aware of its inherent limitations or believing it to be mostly safe, but in practice causing bugs, even if tries take care of any issues involving rounding or non-exact conversions from decimals to binary floats +- In some cases, developers end up using Number anyway—aware of its limitations or believing it to be mostly safe—but in practice cause bugs, even when they try to take care of rounding or non-exact conversions from decimals to binary floats. In other words, with JS increasingly being used in contexts and scenarios where it traditionally did not appear, the need for being able to natively handle basic data, such as decimal numbers, that other systems already natively handle is increasing. @@ -101,7 +101,7 @@ In other words, with JS increasingly being used in contexts and scenarios where JavaScript is often the glue between systems—databases, foreign-function interfaces, other services—that natively support decimal numbers. Decimal lets you take that data and compute on it exactly, preserving its mathematical value. -Note that Decimal preserves *value*, not representation: Trailing zeroes of a decimal String, converted to a Decimal, aren't exposed, so `1.20` and `1.2` are indistinguishable. Implementations of Decimal may indeed preserve that information, but the Decimal API has no properties or functions that would allow one to detect this difference. So if you only need to transport a value untouched, a String already does that losslessly. If you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate* and preserve mathematical value. +Note that Decimal preserves *value*, not representation: a Decimal built from `"1.20"` is indistinguishable from one built from `"1.2"` (see [Data model](#data-model)). So if you only need to transport a value untouched, a String already does that losslessly. If you need to carry a declared precision or scale across the boundary, that is the job of [Amount](https://github.com/tc39/proposal-amount). Decimal earns its place once you need to *calculate* and preserve mathematical value. The following example illustrates the idea. Note the `sql_decimal` configuration option, and how values returned from the DB arrive as Decimal values—ready for exact arithmetic—rather than as strings or JS `Number`s: @@ -228,13 +228,13 @@ Because JavaScript lacks native decimal support, developers have created numerou - [**bignumber.js**](https://www.npmjs.com/package/bignumber.js): ~800K weekly npm downloads - [**big.js**](https://www.npmjs.com/package/big.js): ~500K weekly npm downloads -There are also money-specific libraries such as [dinero.js](https://www.npmjs.com/package/dinero.js) with about 180K weekly NPM downloads. Each implementation has slightly different semantics, requires coordination across libraries, adds to total bundle size, presumably performs worse than native implementations could, and creates interoperability challenges. We believe Decimal will standardize existing practice. Developers are already using decimal libraries, converting numbers to "cents" (error-prone, confusing), using `Number` incorrectly/unsoundly (causing bugs). Decimal doesn't ask developers to adopt something new; it offers a better way to do what they're already struggling to do. +There are also money-specific libraries such as [dinero.js](https://www.npmjs.com/package/dinero.js) with about 180K weekly NPM downloads. Each implementation has slightly different semantics, requires coordination across libraries, adds to total bundle size, presumably performs worse than native implementations could, and creates interoperability challenges. We believe Decimal will standardize existing practice: developers are already reaching for these libraries, or falling back on the error-prone workarounds described [above](#why-use-javascript-for-this-case). Decimal doesn't ask developers to adopt something new; it offers a better way to do what they're already struggling to do. The three most popular libraries above are each by [MikeMcl](https://github.com/mikemcl). They have some [interesting differences](https://github.com/MikeMcl/big.js/wiki), but also share a common shape: an object-and-method API, with rounding modes and precision limits configured on the constructor, and support for inherently rounding operations like square root, exponentiation, and division. We plan to learn from developers' experiences with these and other ecosystem libraries as we refine Decimal's design; the discussion continues in [#22](https://github.com/littledan/proposal-bigdecimal/issues/22). ## Specification and standards -Based on feedback from JS developers, engine implementors, and the members of the TC39 committee, we have a concrete proposal. Please see the [spec text](https://github.com/tc39/proposal-decimal/blob/main/spec.emu) ([HTML version](https://github.com/tc39/proposal-decimal/blob/main/spec.emu)). are provided below. You’re encouraged to join the discussion by commenting on the issues linked below or [filing your own](https://github.com/tc39/proposal-decimal/issues/new). +Based on feedback from JS developers, engine implementors, and the members of the TC39 committee, we have a concrete proposal. Please see the [spec text](https://github.com/tc39/proposal-decimal/blob/main/spec.emu) ([rendered HTML](http://tc39.es/proposal-decimal/)). You’re encouraged to join the discussion by commenting on the issues linked below or [filing your own](https://github.com/tc39/proposal-decimal/issues/new). We will use the **Decimal128** data model for JavaScript decimals. Decimal128 is not a new standard; it was added to the IEEE 754 floating-point arithmetic standard in 2008 (and is present in the 2019 edition of IEEE 754, which JS normatively depends upon). It represents the culmination of decades of research on decimal floating-point numbers. Values in the Decimal128 universe take up 128 bits. In this representation, up to 34 significant digits (that is, decimal digits) can be stored, with an exponent (power of ten) of +/- 6143. @@ -279,11 +279,11 @@ Rational may still make sense as a separate data type, alongside Decimal. Furthe ## Syntax and semantics -With Decimal we do not envision a new literal syntax. One could consider one, such as `123.456_789m` is a Decimal value ([#7](https://github.com/tc39/proposal-decimal/issues/7)), but we are choosing not to add new syntax in light of feedback we have received from JS engine implementors as this proposal has been discussed in multiple TC39 plenary meetings. +With Decimal we do not envision new literal syntax—such as a `123.456_789m` suffix ([#7](https://github.com/tc39/proposal-decimal/issues/7)). In light of feedback from JS engine implementors across multiple TC39 plenary meetings, we are choosing not to add it. See [Decimal literals](#decimal-literals) under Future work for how a v1 stays compatible with adding literals later. ### Data model -Decimal is based on IEEE 754-2019 Decimal128, which is a standard for base-10 decimal numbers using 128 bits. We will offer a subset of the official Decimal128. There will be, in particular: +As noted [above](#specification-and-standards), Decimal uses the IEEE 754-2019 Decimal128 data model. We will offer a subset of the official Decimal128. There will be, in particular: - a single NaN value--distinct from the built-in `NaN` of JS. The difference between quiet and singaling NaNs will be collapsed into a single (quiet) Decimal NaN. - positive and negative infinity will be available, though, as with `NaN`, they are distinct from JS's built-in `Infinity` and `-Infinity`. @@ -339,7 +339,7 @@ We intend to specify these methods in terms of abstract operations that render a Decimal also supports locale-aware formatting: `Decimal.prototype.toLocaleString` produces a localized string for a Decimal value, analogous to `Number.prototype.toLocaleString` ([#15](https://github.com/tc39/proposal-decimal/issues/15)). This is a basic need on the web, so Decimal offers it directly rather than requiring another type. -Decimal and Amount are designed to share the underlying formatting machinery rather than duplicate it; whether `toLocaleString` is specified by building an Amount internally or via common abstract operations is an implementation detail to be settled in the spec. The division of labor is about *what each type carries into the output*: a bare Decimal localizes just a number, whereas Amount localizes a number together with its precision and an optional unit or currency. So when the output needs units, currency, or a preserved display precision, build an Amount. It can take a Decimal, adjust its precision, attach a unit or currency, and format the result with its own `Intl` support; for a plain localized number, Decimal's `toLocaleString` is enough. +Decimal and Amount are designed to share the underlying formatting machinery rather than duplicate it; whether `toLocaleString` is specified by building an Amount internally or via common abstract operations is an implementation detail to be settled in the spec. For when to reach for which type, see [Amount](#amount) below. ## Past discussions in TC39 plenaries @@ -433,13 +433,13 @@ Therefore, this proposal does not contain any options to set the precision from Mike Cowlishaw’s excellent [Decimal FAQ](http://speleotrove.com/decimal/decifaq.html) explains many of the core design principles for decimal data types, which this proposal attempts to follow. -One notable point concerns *exposing* trailing zeroes: the Decimal champion group does not see these as being worth revealing to JS programmers. As Mike explains in [FAQ part 4: “Why is decimal arithmetic unnormalized?”](https://speleotrove.com/decimal/decifaq4.html), the underlying arithmetic is most naturally performed on *unnormalized* values. This proposal takes a hybrid approach: implementations may store and compute on unnormalized values, thereby keeping the performance and simplicity benefits Mike describes, and need only “normalize on the way out” when rendering a String or comparing values. +One of those principles—that decimal arithmetic is most naturally performed on *unnormalized* values—shapes Decimal's "normalize on the way out" stance, discussed under [Data model](#data-model). ## Relationship of Decimal to other TC39 proposals ### BigInt -This proposal can be seen as a follow-on to [BigInt](https://github.com/tc39/proposal-bigint/), which brought arbitrary-sized integers to JavaScript, and will be fully standardized in ES2020. However, unlike BigInt, Decimal (i) does not propose to intrduce a new primitive data type, (ii) does not propose operator overloading (which BigInt does support), and (iii) does not offer new syntax (numeric literla), which BigInt does add (e.g., `2345n`). +This proposal can be seen as a follow-on to [BigInt](https://github.com/tc39/proposal-bigint/), which brought arbitrary-sized integers to JavaScript and was standardized in ES2020. However, unlike BigInt, Decimal (i) does not introduce a new primitive data type, (ii) does not propose operator overloading (which BigInt does support), and (iii) does not offer new syntax (numeric literals), which BigInt does add (e.g., `2345n`). ### Amount From 037a6594c070267eafcd99a03ab078960fadb7ab Mon Sep 17 00:00:00 2001 From: jessealama Date: Tue, 30 Jun 2026 07:42:26 +0000 Subject: [PATCH 16/17] fixup: [spec] `npm run build` --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 8fa887f0..15e8f30c 100644 --- a/index.html +++ b/index.html @@ -3231,7 +3231,7 @@

Stage 1 Draft / June 9, 2026

Decimal

+

Stage 1 Draft / June 30, 2026

Decimal

Introduction

From 526be366ae14dad56c5f64fad1608668c4e09231 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 30 Jun 2026 09:48:22 +0200 Subject: [PATCH 17/17] Clarify "normalize on the way out" --- docs/data-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-model.md b/docs/data-model.md index dd9746a3..409dc9b9 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -72,7 +72,7 @@ new Decimal("01.2").toString(); // => "1.2" new Decimal("1.2e0").toString(); // => "1.2" ``` -This is a guarantee about what Decimal *exposes*, not a requirement on how a value is *stored*. An implementation may keep values unnormalized internally — preserving trailing zeroes, as the full IEEE 754 standard does — and normalize only on the way out, when rendering a String or comparing values ("normalize on the way out"). This hybrid approach captures the performance benefits of [unnormalized decimal arithmetic](https://speleotrove.com/decimal/decifaq4.html) without exposing display precision to callers. +This is a guarantee about what Decimal *exposes*, not a requirement on how a value is *stored*. An implementation of Decimal may keep values unnormalized internally. But it must render a Decimal as a String, and compare Decimal values, by mathematical value (i.e., without trailing zeroes). We call this "normalize on the way out". This hybrid approach captures the performance benefits of [unnormalized decimal arithmetic](https://speleotrove.com/decimal/decifaq4.html) without exposing display precision to callers. ## Arithmetic Operations