Skip to content

Commit 927bcf6

Browse files
authored
Remove Decimal.Amount, refocus on exact arithmetic (#204)
1 parent bde7468 commit 927bcf6

File tree

3 files changed

+54
-226
lines changed

3 files changed

+54
-226
lines changed

README.md

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Ecma TC39 JavaScript Decimal proposal
22

3-
The TC39 Decimal proposal aims to add functionality to JavaScript to represent base-10 decimal numbers.
3+
The TC39 Decimal proposal aims to add exact decimal arithmetic to JavaScript, eliminating the rounding errors that occur with binary floating-point numbers.
44

55
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).**
66

@@ -25,14 +25,13 @@ As currently defined in JavaScript, Numbers are 64-bit binary floating-point num
2525

2626
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.
2727

28-
### Primary use case: Representing human-readable decimal values such as money
28+
### Primary use case: Exact decimal arithmetic for financial calculations
2929

3030
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:
3131

3232
- There’s a persistent mismatch between the way humans think about money and the way it’s manipulated in the program, causing mental overhead for the programmer aware of the issue.
3333
- Some programmers may not even be aware of this mismatch. This opens the door to rounding errors whose source is unknown. If calculations start to get more involved, the chance of error increases.
34-
- Different currencies use different numbers of decimal positions which is easy to get confused; the hack of working with quantities that are implicitly multiplied by 100 may not work when working with multiple currencies. For instance, it’s not correct to assume that all currencies have two decimal places, or that the only exception is JPY (Japanese yen); making such assumptions will make it hard to internationalize code to new countries. For this reason, it’s ideal if the number of decimal places is part of the data type.
35-
- In various contexts (e.g., presenting a quantity to the end user), the decimal point needs to be brought back in somehow. For example, `Intl.NumberFormat` only knows how to format JS Numbers, and can’t deal with an integer-and-exponent pair.
34+
- Different currencies use different numbers of decimal positions which is easy to get confused; the hack of working with quantities that are implicitly multiplied by 100 may not work when working with multiple currencies. For instance, it's not correct to assume that all currencies have two decimal places, or that the only exception is JPY (Japanese yen); making such assumptions will make it hard to internationalize code to new countries.
3635
- Sometimes, fractional cents need to be represented too (e.g., as precise prices that occur, for instance, in stock trading or currency conversion).
3736

3837
#### Sample code
@@ -72,30 +71,6 @@ let amountInEur = exchangeRateUsdToEur.multiply(amountInUsd);
7271
console.log(amountInEur.round(2).toString());
7372
```
7473

75-
Here's the same example, this time using `Decimal.Amount`
76-
and `Intl.NumberFormat` to properly handle currency:
77-
78-
```js
79-
let exchangeRateEurToUsd = new Decimal("1.09");
80-
let amountInUsd = new Decimal("450.27");
81-
let exchangeRateUsdToEur = new Decimal(1).divide(exchangeRateEurToUsd);
82-
let amountInEur = exchangeRateUsdToEur.multiply(amountInUsd);
83-
let opts = { style: "currency", currency: "EUR" };
84-
let formatter = new Intl.NumberFormat("en-US", opts);
85-
let amount = Decimal.Amount(amountInEur).with({ fractionDigits: 2 });
86-
console.log(formatter.format(amount));
87-
```
88-
89-
##### Format decimals with Intl.NumberFormat
90-
91-
We propose a `Decimal.Amount` object to store a Decimal value together with precision information. This is especially useful in formatting Decimal values, especially in internationalization and localization contexts.
92-
93-
```js
94-
let a = Decimal.Amount.from("1.90").with({ fractionDigits: 4 });
95-
const formatter = new Intl.NumberFormat("de-DE");
96-
formatter.format(a); // "1,9000"
97-
```
98-
9974
#### Why use JavaScript for this case?
10075

10176
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:
@@ -119,7 +94,6 @@ This use case implies the following goals:
11994
- Avoid unintentional rounding that causes user-visible errors
12095
- Basic mathematical functions such as addition, subtraction, multiplication, and division
12196
- Sufficient precision for typical money and other human-readable quantities, including cryptocurrency (where many decimal digits are routinely needed)
122-
- Conversion to a string in a locale-sensitive manner
12397
- Sufficient ergonomics to enable correct usage
12498
- Be implementable with adequate performance/memory usage for applications
12599
- (Please file an issue to mention more requirements)
@@ -145,11 +119,9 @@ This use case implies the following goals:
145119
Interaction with other systems brings the following requirements:
146120

147121
- Ability to round-trip decimal quantities from other systems
148-
- Serialization and deserialization in standard decimal formats, e.g., IEEE 754s multiple formats
122+
- Serialization and deserialization in standard decimal formats, e.g., IEEE 754's multiple formats
149123
- Precision sufficient for the applications on the other side
150124

151-
The `Decimal.Amount` class also helps with data exchange, in cases where one needs to preserve all digits—including any trailing zeroes—in a digit string coming over the wire. That is, the `Decimal.Amount` class contains more information that a mathematical value.
152-
153125
#### Sample code
154126

155127
##### Configure a database adapter to use JS-native decimals
@@ -241,8 +213,6 @@ Based on feedback from JS developers, engine implementors, and the members of th
241213

242214
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.
243215

244-
In addition to proposing a new `Decimal` class, we propose a `Decimal.Amount` class for storing a Decimal number together with a precision. The `Decimal.Amount` class is important mainly for string formatting purposes, where one desires to have a notion of a number that “knows” how precise it is. We do not intend to support arithmetic on `Decimal.Amount` values, the thinking being that `Decimal` already supports arithmetic.
245-
246216
### Known alternatives
247217

248218
#### Unlimited precision decimals (AKA "BigDecimal")
@@ -293,9 +263,7 @@ Decimal is based on IEEE 754-2019 Decimal128, which is a standard for base-10 de
293263
- 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.
294264
- positive and negative infinity will be available, though, as with `NaN`, they are distinct from JS's built-in `Infinity` and `-Infinity`.
295265

296-
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. The `Decimal.Amount` class can be used to store an exact decimal value together with precision (number of significant digits), which can be used to track all digits of a number, including any trailing zeroes (which Decimal canonicalizes away).
297-
298-
The `Decimal.Amount` class will not support arithmetic or comparisons. Such operations should be delegated to the underlying Decimal value wrapped by a `Decimal.Amount`.
266+
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.
299267

300268
### Operator semantics
301269

@@ -336,10 +304,8 @@ Decimal objects can be constructed from Numbers, Strings, and BigInts. Similarly
336304
- `toFixed()` is similar to Number's `toFixed()`
337305
- `toPrecison()` is similar to Number's `toPrecision()`
338306
- `toExponential()` is similar to Number's `toExponential()`
339-
- `Intl.NumberFormat.prototype.format` should transparently support Decimal ([#15](https://github.com/tc39/proposal-decimal/issues/15)), which will be handled via `Decimal.Amount` objects
340-
- `Intl.PluralRules.prototype.select` should similarly support Decimal, in that it will support `Decimal.Amount` objects
341-
342-
In addition, the `Decimal.Amount` object will provide a `toString` method, which will render its underlying Decimal value according to its underlying precision.
307+
- `Intl.NumberFormat.prototype.format` will support Decimal values directly ([#15](https://github.com/tc39/proposal-decimal/issues/15))
308+
- `Intl.PluralRules.prototype.select` will similarly support Decimal values
343309

344310
## Past discussions in TC39 plenaries
345311

@@ -385,6 +351,10 @@ In our discussions we have consistently emphasized the need for basic arithmetic
385351

386352
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.
387353

354+
### Precision metadata for formatting
355+
356+
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.
357+
388358
## FAQ
389359

390360
### What about rational numbers?

0 commit comments

Comments
 (0)