diff --git a/README.md b/README.md index 32496113..ddb2fa84 100644 --- a/README.md +++ b/README.md @@ -201,11 +201,45 @@ If Decimal becomes a part of standard JavaScript, it may be used in some built-i More host API interactions are discussed in [#5](https://github.com/tc39/proposal-decimal/issues/5). +## Prior Art + +Adding decimal arithmetic would not be an instance of JS breaking new ground. Many major programming languages have recognized that decimal arithmetic is fundamental enough to include in the standard library, if not as a primitive type. + +### Decimal Support Across Languages + +| Language | Type Name | Location | Year Added | Notes | +|----------|-----------|----------|------------|-------| +| Python | [`decimal.Decimal`](https://docs.python.org/3/library/decimal.html) | Standard library | 2003 (Python 2.3) | Based on [General Decimal Arithmetic Specification](http://speleotrove.com/decimal/) | +| Java | [`java.math.BigDecimal`](https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html) | Standard library | 1998 (JDK 1.1) | Arbitrary precision, widely used for financial calculations | +| C# | [`decimal`](https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-9.0) | Primitive structure type | 2000 (C# 1.0) | 128-bit decimal type, first-class language support | +| Swift | [`Decimal`](https://developer.apple.com/documentation/foundation/decimal) (formerly [`NSDecimalNumber`](https://developer.apple.com/documentation/foundation/nsdecimalnumber)) | Foundation framework | 2016 (Swift 3.0) | Standard library type for financial calculations | +| Ruby | [`BigDecimal`](https://ruby-doc.org/stdlib-3.1.0/libdoc/bigdecimal/rdoc/BigDecimal.html) | Standard library | 1999 (Ruby 1.6) | Arbitrary precision decimal arithmetic | + +SQL also has `NUMERIC`/`DECIMAL` as a built-in type, predating many programming languages. + +Many languages added decimal support 10+ years ago. The IEEE 754-2008 standard for Decimal128 is now 17 years old. (The 2019 edition of IEEE 754 also has decimal arithmetic.) + +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. + +### 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. + +### The Cost of Not Having Decimal + +Because JavaScript lacks native decimal support, developers have created numerous userland solutions: + +- [**decimal.js**](https://www.npmjs.com/package/decimal.js): ~2M weekly npm downloads +- [**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. + ## 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). -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. It represents the culmination of decades of research, both theoretical and practical, 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. +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. 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. diff --git a/docs/cookbook.md b/docs/cookbook.md index d0561835..c4dd564a 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -20,16 +20,148 @@ const total = price.multiply(quantity); console.log(total.toString()); // => "59.97" ``` +## Prior Art: Decimal Arithmetic in Other Languages + +The patterns demonstrated in this cookbook are not unique to JavaScript - they reflect well-established practices across programming languages. This section shows how other languages handle the same decimal arithmetic challenges, demonstrating that native decimal support is standard across the industry. + +### Python + +Python includes the [`decimal`](https://docs.python.org/3/library/decimal.html) module in its standard library (since 2003): + +```python +from decimal import Decimal + +# Financial calculations +price = Decimal("19.99") +quantity = Decimal("3") +tax_rate = Decimal("0.0825") + +subtotal = price * quantity +tax = subtotal * tax_rate +total = subtotal + tax + +print(f"${total:.2f}") # => "$64.92" +``` + +Python's `Decimal` is widely used in web frameworks like [Django](https://www.djangoproject.com) and [Flask](https://flask.palletsprojects.com/en/stable/) for handling monetary values. Django includes a [`DecimalField`](https://docs.djangoproject.com/en/5.2/ref/models/fields/#decimalfield) specifically for such data. + +### Java + +Java's [`BigDecimal`](https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html) (in the standard library since 1998) is ubiquitous in enterprise applications: + +```java +import java.math.BigDecimal; +import java.math.RoundingMode; + +BigDecimal price = new BigDecimal("19.99"); +BigDecimal quantity = new BigDecimal("3"); +BigDecimal taxRate = new BigDecimal("0.0825"); + +BigDecimal subtotal = price.multiply(quantity); +BigDecimal tax = subtotal.multiply(taxRate); +BigDecimal total = subtotal.add(tax); + +// Round to 2 decimal places +total = total.setScale(2, RoundingMode.HALF_UP); +System.out.println(total); // => "64.62" +``` + +`BigDecimal` is the standard approach for financial applications in the Java ecosystem, including frameworks like [Spring](https://spring.io/projects/spring-framework) and [Hibernate](https://hibernate.org). + +### C\# + +C# includes [`decimal`](https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-9.0) as a primitive type (since 2000), giving it first-class language support: + +```csharp +decimal price = 19.99m; +decimal quantity = 3m; +decimal taxRate = 0.0825m; + +decimal subtotal = price * quantity; +decimal tax = subtotal * taxRate; +decimal total = subtotal + tax; + +Console.WriteLine($"${total:F2}"); // => "$64.92" +``` + +### Ruby + +Ruby includes [`BigDecimal`]((https://ruby-doc.org/3.4.1/exts/json/BigDecimal.html)) in its standard library: + +```ruby +require 'bigdecimal' + +price = BigDecimal("19.99") +quantity = BigDecimal("3") +tax_rate = BigDecimal("0.0825") + +subtotal = price * quantity +tax = subtotal * tax_rate +total = subtotal + tax + +puts "$%.2f" % total # => "$64.92" +``` + +Ruby on Rails uses `BigDecimal` for handling database `decimal` columns by default, making it the standard approach for money in Rails applications. + +### Swift + +Swift's Foundation framework includes [`Decimal`](https://developer.apple.com/documentation/foundation/decimal) (formerly [`NSDecimalNumber`](https://developer.apple.com/documentation/foundation/nsdecimalnumber)): + +```swift +import Foundation + +let price = Decimal(string: "19.99")! +let quantity = Decimal(string: "3")! +let taxRate = Decimal(string: "0.0825")! + +let subtotal = price * quantity +let tax = subtotal * taxRate +let total = subtotal + tax + +let formatter = NumberFormatter() +formatter.numberStyle = .currency +print(formatter.string(from: total as NSDecimalNumber)!) // => "$64.92" +``` + +Swift's `Decimal` is the recommended type for financial calculations in iOS and macOS applications. + +### SQL + +Most SQL databases (e.g., [PostgreSQL](https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL)) treat decimal arithmetic as fundamental: + +```sql +SELECT + price, + quantity, + price * quantity AS subtotal, + (price * quantity) * 0.0825 AS tax, + (price * quantity) * 1.0825 AS total +FROM products +WHERE id = 1; +``` + +Database `NUMERIC` and `DECIMAL` types provide exact decimal arithmetic. When JavaScript applications query these values, they currently must receive them as strings (or as `Number`s, which may lose precision from the get-go) that are then converted to `Number` (which also loses precision), or use a userland decimal library. + +### Why This Matters for JavaScript + +JavaScript forces developers working with numeric data in a setting where exactness matters to choose between: + +- Binary floats (precision errors, bugs, non-trivial knowledge of binary float problems and some countermeasures that may not always work) +- Userland libraries (bundle size, coordination, performance) +- Integer "cents" (cognitive overhead, internationalization issues) + +The patterns in this cookbook reflect industry-standard practices that JavaScript developers should be able to use natively, just as developers in other major languages can. + ## Common Patterns This section demonstrates common patterns and best practices when working with Decimal. ### Working with Monetary Values -Always create Decimal values from strings to ensure exact representation: +Create Decimal values from strings to ensure exact representation: ```javascript -// Good: Create from string const price = new Decimal("19.99"); const tax = new Decimal("0.0825"); @@ -41,7 +173,7 @@ console.log(total.toFixed(2)); // => "21.64" ### Accumulating Values -When accumulating many values over the course of a calculation with several steps, use Decimal to avoid rounding errors: +When accumulating many values in the course of a calculation with several steps, use Decimal to avoid rounding errors: ```javascript const transactions = ["10.50", "25.75", "3.99", "100.00", "45.25"]; @@ -111,7 +243,7 @@ console.log(tip); // => { amount: "6.83", total: "52.33" } ### Creating Decimal values -The most reliable way to create exact decimal values is from strings: +The most reliable way to create Decimal values is from strings: ```javascript // From string (recommended for exact values) @@ -171,7 +303,14 @@ try { } ``` -Very large integers beyond Number's safe range work perfectly: +If one wants to ensure that a Decimal value represents, semantically, an integer, use `round` before convering to BigInt: + +```javascript +const fractionalDecimal = new Decimal("123.45"); // same as previous example +fractionalDecimal.round().toBigInt(); // ...but doesn't throw +``` + +Very large integers beyond `Number`'s safe range work perfectly: ```javascript const largeDecimal = new Decimal("99999999999999999999999999999999"); @@ -179,7 +318,7 @@ const largeBigInt = largeDecimal.toBigInt(); console.log(largeBigInt); // => 99999999999999999999999999999999n ``` -(However, there are some digit strings that Number can handle just fine, namely those with more than 34 significant digits that have compact representations as sums of powers of two.) +However, there are some digit strings that Number can handle just fine, namely those with more than 34 significant digits that have compact representations as sums of powers of two. ## Financial Calculations diff --git a/index.html b/index.html index d44dbb4b..bbdc3da3 100644 --- a/index.html +++ b/index.html @@ -3231,7 +3231,7 @@

Stage 1 Draft / October 22, 2025

Decimal

+

Stage 1 Draft / October 23, 2025

Decimal

Introduction