├── .gitignore ├── docs ├── .gitignore ├── tail.html.part ├── test-api.mjs ├── index.md ├── head.html.part ├── data-model.md ├── buildDocs.js ├── testExamples.js ├── testExamples.mjs ├── decimal.md ├── why-decimal.md └── cookbook.md ├── Makefile ├── .github └── workflows │ ├── build.yaml │ └── lint.yaml ├── package.json ├── RELATED.md ├── intl.emu ├── decimal128-reference.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | docs/out/ 3 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | *.log 4 | .DS_Store -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean 2 | 3 | all: index.html 4 | 5 | index.html: spec.emu intl.emu 6 | npx ecmarkup --lint-spec --strict --load-biblio @tc39/ecma262-biblio --load-biblio @tc39/ecma402-biblio spec.emu $@ 7 | 8 | clean: 9 | rm -f index.html 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy spec 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: "20.x" 14 | - run: npm install 15 | - run: npm run build 16 | - name: commit changes 17 | uses: elstudio/actions-js-build/commit@v4 18 | with: 19 | commitMessage: "fixup: [spec] `npm run build`" 20 | -------------------------------------------------------------------------------- /docs/tail.html.part: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: lint spec 3 | 4 | jobs: 5 | test: 6 | name: Ensure that we can build the spec 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | 13 | - name: Install ecmarkup 14 | - run: npm install --global ecmarkup 15 | 16 | - name: ensure no line starts with tabs 17 | - run: | 18 | for f in *emu; do 19 | if ! [ grep '^\t' "$f" ]; then 20 | echo "$f has lines that begin with a tab"; 21 | exit 1; 22 | fi 23 | done 24 | 25 | - name: Try to generate spec, being strict 26 | - run: ecmarkup --lint-spec --strict spec.emu out.html 27 | -------------------------------------------------------------------------------- /docs/test-api.mjs: -------------------------------------------------------------------------------- 1 | import { Decimal } from 'proposal-decimal'; 2 | 3 | console.log('=== Testing Decimal API ===\n'); 4 | 5 | // Basic creation 6 | console.log('Creating Decimal:'); 7 | const d1 = new Decimal('123.45'); 8 | console.log('new Decimal("123.45"):', d1.toString()); 9 | 10 | // Properties 11 | console.log('\nProperties:'); 12 | console.log('isNaN():', d1.isNaN()); 13 | console.log('isFinite():', d1.isFinite()); 14 | console.log('exponent():', d1.exponent()); 15 | console.log('significand():', d1.significand()); 16 | 17 | // Arithmetic 18 | console.log('\nArithmetic:'); 19 | const d2 = new Decimal('10'); 20 | console.log('d1.add(d2):', d1.add(d2).toString()); 21 | console.log('d1.multiply(d2):', d1.multiply(d2).toString()); 22 | 23 | // Rounding 24 | console.log('\nRounding:'); 25 | const d3 = new Decimal('123.456'); 26 | console.log('d3.round(2):', d3.round(2).toString()); 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proposal-decimal", 3 | "version": "1.0.0", 4 | "description": "Representing base-10 decimal numbers", 5 | "scripts": { 6 | "start": "npm run build-loose -- --watch", 7 | "build": "npm run build-loose -- --strict", 8 | "build-loose": "ecmarkup --verbose --lint-spec --load-biblio @tc39/ecma262-biblio --load-biblio @tc39/ecma402-biblio spec.emu index.html", 9 | "format": "prettier . --write", 10 | "lint": "prettier . --check", 11 | "docs:test": "node docs/testExamples.mjs", 12 | "docs:build": "npm run docs:test && node docs/buildDocs.js", 13 | "docs:build:force": "node docs/buildDocs.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/tc39/proposal-decimal.git" 18 | }, 19 | "keywords": [ 20 | "TC39", 21 | "Decimal", 22 | "BigDecimal", 23 | "Decimal128" 24 | ], 25 | "author": "Jesse Alama ", 26 | "license": "SEE LICENSE IN LICENSE.md", 27 | "bugs": { 28 | "url": "https://github.com/tc39/proposal-decimal/issues" 29 | }, 30 | "homepage": "https://tc39.es/proposal-decimal/", 31 | "devDependencies": { 32 | "@tc39/ecma262-biblio": "2.1", 33 | "@tc39/ecma402-biblio": "^2.1.1050", 34 | "ecmarkup": "github:jessealama/ecmarkup#permit-decimal-subscript-D", 35 | "marked": "^15.0.12", 36 | "mkdirp": "^3.0.1", 37 | "prettier": "^3.6.2", 38 | "prismjs": "^1.30.0", 39 | "proposal-decimal": "^20250613.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Decimal 2 | 3 | ## Introduction 4 | 5 | This is a proposal for `Decimal`, a new numeric type that brings exact base-10 decimal arithmetic to JavaScript. Decimal provides: 6 | 7 | - Exact representation of decimal values 8 | - Support for up to 34 significant digits 9 | - IEEE 754-2019 Decimal128 semantics 10 | - Integration with `Intl.NumberFormat` for locale-aware formatting 11 | 12 | To understand the motivation behind this proposal and why JavaScript needs a decimal type, see [Why Decimal?](./why-decimal.md) 13 | 14 | ## Cookbook 15 | 16 | A cookbook to help you get started with Decimal is available [here](./cookbook.md). 17 | 18 | ## API Documentation 19 | 20 | ### **Decimal** 21 | 22 | A `Decimal` represents an exact base-10 decimal number using IEEE 754-2019 Decimal128 format. This allows for up to 34 significant digits with an exponent range of ±6143. 23 | 24 | ```js 25 | const price = new Decimal("19.99"); 26 | const quantity = new Decimal("3"); 27 | const total = price.multiply(quantity).toString(); // "59.97" 28 | ``` 29 | 30 | Unlike JavaScript's `Number` type, Decimal provides exact decimal arithmetic: 31 | 32 | ```js 33 | // With Number (binary floating-point) 34 | 0.1 + 0.2; // => 0.30000000000000004 35 | 36 | // With Decimal 37 | const a = new Decimal("0.1"); 38 | const b = new Decimal("0.2"); 39 | const c = a.add(b); 40 | c.toString(); // "0.3" 41 | c.equals(new Decimal("0.3")); // true 42 | ``` 43 | 44 | See [Decimal Documentation](./decimal.md) for detailed documentation. 45 | 46 | ## Data Model 47 | 48 | Decimal uses a subset of the IEEE 754-2019 Decimal128 specification: 49 | 50 | - **128-bit representation** allowing up to 34 significant digits 51 | - **Base-10 exponent** range of -6143 to +6144 52 | - **Special values**: NaN, positive and negative infinity for compatibility with IEEE 754 and JS's `Number` 53 | - **Canonicalization**: values are normalized (e.g., "1.20" becomes "1.2") 54 | 55 | For a detailed explanation of the data model and design decisions, see [Data Model Documentation](./data-model.md). 56 | 57 | ## String Representation and Parsing 58 | 59 | All Decimal values can be converted to and from strings: 60 | 61 | ```js 62 | // Parsing 63 | const d1 = new Decimal("123.456"); 64 | const d2 = new Decimal("-0.0078"); 65 | const d3 = new Decimal("6.022e23"); 66 | 67 | // Formatting 68 | d1.toString(); // => "123.456" 69 | d1.toFixed(2); // => "123.46" 70 | d1.toExponential(2); // => "1.23e+2" 71 | d1.toPrecision(5); // => "123.46" 72 | ``` 73 | 74 | ## Common Use Cases 75 | 76 | ### Financial Calculations 77 | 78 | #### Calculate bill with tax 79 | 80 | ```js 81 | function calculateTotal(subtotal, taxRate) { 82 | const subtotalDec = new Decimal(subtotal); 83 | const taxRateDec = new Decimal(taxRate); 84 | const tax = subtotalDec.multiply(taxRateDec); 85 | return subtotalDec.add(tax); 86 | } 87 | 88 | const total = calculateTotal("99.99", "0.0825"); 89 | console.log(total.toFixed(2)); // => "108.24" 90 | ``` 91 | 92 | #### Currency Conversion 93 | 94 | ```js 95 | const amountUSD = new Decimal("100.00"); 96 | const exchangeRate = new Decimal("0.92545"); // USD to EUR 97 | const amountEUR = amountUSD.multiply(exchangeRate); 98 | console.log(amountEUR.toFixed(2)); // => "92.55" 99 | ``` 100 | 101 | ## Other Documentation 102 | 103 | - [Why Decimal?](./why-decimal.md) — Understanding the motivation and use cases for Decimal 104 | - [API Reference](./decimal.md) — Complete API documentation for Decimal 105 | - [Cookbook](./cookbook.md) — Common recipes and patterns for working with Decimal 106 | - [Data Model](./data-model.md) — Detailed explanation of the IEEE 754-2019 Decimal128 subset 107 | -------------------------------------------------------------------------------- /docs/head.html.part: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Decimal Documentation - TC39 Proposal 7 | 8 | 9 | 148 | 149 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /RELATED.md: -------------------------------------------------------------------------------- 1 | ## History and related work 2 | 3 | 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. 4 | 5 | ### Related JS ecosystem libraries 6 | 7 | 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): 8 | 9 | - [decimal.js](http://mikemcl.github.io/decimal.js/) 10 | - [big.js](https://mikemcl.github.io/big.js/) 11 | - [bignumber.js](https://mikemcl.github.io/bignumber.js/) 12 | 13 | These packages have some [interesting differences](https://github.com/MikeMcl/big.js/wiki), but there are also many similarities: 14 | 15 | - APIs are based on JavaScript objects and method calls 16 | - Rounding modes and precision limits are settings in the constructor 17 | - Inherently rounding operations like sqrt, exponentiation, division are supported 18 | 19 | The initial proposal in this document suggests some differences, described above. 20 | 21 | 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). 22 | 23 | ### Related features in other systems 24 | 25 | 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. 26 | 27 | - Standards 28 | - **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) 29 | - (plus many of the below are standardized) 30 | - Programming languages 31 | - Fixed-size decimals: 32 | - **[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. 33 | - **[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. 34 | - **[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. 35 | - **[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. 36 | - Global settings for setting decimal precision 37 | - **[Python](https://docs.python.org/2/library/decimal.html)**: Decimal with global settings to set precision. 38 | - Rationals 39 | - **[Perl6](https://docs.perl6.org/type/Rat)**: Literals like `1.5` are Rat instances! 40 | - **[Common Lisp](http://www.lispworks.com/documentation/lw50/CLHS/Body/t_ratio.htm#ratio)**: Ratios live alongside floats; no decimal data type 41 | - **[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) 42 | - **[Ruby](https://ruby-doc.org/core-2.6.5/Rational.html)**: Rational class alongside BigDecimal. 43 | - Arbitrary-precision decimals (this proposal) 44 | - **[Ruby](https://ruby-doc.org/stdlib-2.4.0/libdoc/bigdecimal/rdoc/BigDecimal.html)**: Arbitrary-precision Decimal, alongside Rational. 45 | - **[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. 46 | - **[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 47 | - Databases 48 | - Decimal with precision configurable in the schema 49 | - [Microsoft SQL Server](https://docs.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql) 50 | - [PostgreSQL](https://www.postgresql.org/docs/9.1/static/datatype-numeric.html) 51 | - [MySQL](https://dev.mysql.com/doc/refman/5.7/en/precision-math-decimal-characteristics.html) 52 | - IEEE 754-2008 decimal 53 | - Bloomberg's [comdb2](https://bloomberg.github.io/comdb2/decimals.html) 54 | - [MongoDB](https://docs.mongodb.com/manual/core/shell-types/#shell-type-decimal) 55 | - Libraries 56 | - Intel C [inteldfp](https://software.intel.com/en-us/articles/intel-decimal-floating-point-math-library): IEEE decimal 57 | - Bloomberg C++ [bdldfp](https://github.com/bloomberg/bde/blob/master/groups/bdl/bdldfp/bdldfp_decimal.h): IEEE decimal 58 | - IBM C [decnumber](http://speleotrove.com/decimal/decnumber.html): Configurable context with precision, rounding mode 59 | - Rust crates [[1]](https://crates.io/crates/decimal) [[2]](https://crates.io/crates/bigdecimal) 60 | - Hardware (all implementing IEEE decimal) 61 | - [POWER6]() 62 | - [RISC-V](https://en.wikichip.org/wiki/risc-v/standard_extensions) (planned) 63 | 64 | ### History of discussion of decimal in TC39 65 | 66 | 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. 67 | 68 | - 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) 69 | - 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) 70 | - 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). 71 | -------------------------------------------------------------------------------- /docs/data-model.md: -------------------------------------------------------------------------------- 1 | # Decimal Data Model 2 | 3 | ## Overview 4 | 5 | The Decimal proposal uses a subset of the IEEE 754-2019 Decimal128 format. This document explains the data model, design decisions, and technical details of how decimal values are represented and manipulated. 6 | 7 | ## IEEE 754-2019 Decimal128 8 | 9 | Decimal128 is part of the IEEE 754 standard for floating-point arithmetic, added in the 2008 revision. (In fact, JS's Number type is also based on IEEE 754: binary64) It provides a 128-bit representation(each decimal value is stored in exactly 128 bits) for base-10 arithmetic with up to 34 significant digits and an exponent range of -6143 to +6144. 10 | 11 | The choice of Decimal128 balances several considerations: 12 | 13 | 1. **Sufficient precision**: 34 digits covers virtually all practical use cases, including: 14 | - Financial calculations (even very large values that don't occur in everyday scenarios) 15 | - Scientific measurements 16 | 2. **Fixed size**: Unlike arbitrary-precision decimals, Decimal128 has predictable memory usage and performance characteristics 17 | 3. **Industry standard**: Widely supported in other languages 18 | 4. **Reasonable range**: Can represent values from ±10^-6143 to ±10^6144 19 | 20 | ### Special Values 21 | 22 | #### NaN (Not a Number) 23 | 24 | Decimal has a single, quiet NaN value, similar to JS's Number: 25 | 26 | ```javascript 27 | const nan = new Decimal("NaN"); 28 | nan.isNaN(); // => true 29 | 30 | // NaN propagates through operations 31 | nan.add(new Decimal("5")); // => NaN 32 | nan.multiply(new Decimal("0")); // => NaN 33 | ``` 34 | 35 | #### Infinity 36 | 37 | Positive and negative infinity are supported: 38 | 39 | ```javascript 40 | const posInf = new Decimal("Infinity"); 41 | const negInf = new Decimal("-Infinity"); 42 | 43 | posInf.isFinite(); // => false 44 | posInf.add(new Decimal("1")); // => Infinity 45 | posInf.negate(); // => -Infinity 46 | ``` 47 | 48 | #### Zero 49 | 50 | Decimal supports both positive and negative zero: 51 | 52 | ```javascript 53 | const posZero = new Decimal("0"); 54 | const negZero = new Decimal("-0"); 55 | 56 | // They are equal in value 57 | posZero.equals(negZero); // => true 58 | 59 | // But can be distinguished 60 | Object.is(posZero, negZero); // => false 61 | ``` 62 | 63 | ### Canonicalization 64 | 65 | Decimal values are canonicalized, meaning different representations of the same mathematical value are normalized: 66 | 67 | ```javascript 68 | // These all become the same Decimal value 69 | new Decimal("1.20").toString(); // => "1.2" 70 | new Decimal("1.200").toString(); // => "1.2" 71 | new Decimal("01.2").toString(); // => "1.2" 72 | new Decimal("1.2e0").toString(); // => "1.2" 73 | ``` 74 | 75 | This is a deliberate departure from the full IEEE 754 standard, which preserves trailing zeros. 76 | 77 | ## Arithmetic Operations 78 | 79 | ### Exact Arithmetic 80 | 81 | Within the range and precision limits, Decimal arithmetic is exact: 82 | 83 | ```javascript 84 | // These operations are exact 85 | new Decimal("0.1").add(new Decimal("0.2")); // => 0.3 (exactly) 86 | new Decimal("10").divide(new Decimal("3")).multiply(new Decimal("3")); // => 10 (exactly) 87 | ``` 88 | 89 | ### Rounding 90 | 91 | When a result would exceed 34 significant digits, the calculation is exact up to 34 digits with rounding occuring after the 34th digit: 92 | 93 | ```javascript 94 | // Division may require rounding 95 | const a = new Decimal("1"); 96 | const b = new Decimal("3"); 97 | const result = a.divide(b); // => 0.3333333333333333333333333333333333 (34 digits) 98 | ``` 99 | 100 | The default rounding mode is "half even" (banker's rounding), but other modes are available: 101 | 102 | - **halfEven**: Round to nearest, ties to even (default) 103 | - **halfExpand**: Round to nearest, ties away from zero 104 | - **ceil**: Round towards positive infinity 105 | - **floor**: Round towards negative infinity 106 | - **trunc**: Round towards zero 107 | 108 | ### Overflow and Underflow 109 | 110 | Operations that would exceed the exponent range result in infinity or zero: 111 | 112 | ```javascript 113 | // Overflow to infinity 114 | const big = new Decimal("1e6144"); 115 | big.multiply(new Decimal("10")); // => Infinity 116 | 117 | // Underflow to zero 118 | const small = new Decimal("1e-6143"); 119 | small.divide(new Decimal("10")); // => 0 120 | ``` 121 | 122 | ## Comparison with Other Numeric Types 123 | 124 | ### vs Number (binary64) 125 | 126 | | Aspect | Number (IEEE 754 binary64) | Decimal (IEEE 754 Decimal128) | 127 | | ---------------------------- | -------------------------- | ----------------------------- | 128 | | Base | Binary (base 2) | Decimal (base 10) | 129 | | Precision | ~15-17 decimal digits | Exactly 34 decimal digits | 130 | | Exact decimal representation | No | Yes | 131 | | Range | ±1.8×10^308 | ±9.999...×10^6144 | 132 | | Size | 64 bits | 128 bits | 133 | | Special values | ±0, ±Infinity, NaN | ±0, ±Infinity, NaN | 134 | 135 | ### vs BigInt 136 | 137 | | Aspect | BigInt | Decimal | 138 | | ----------------- | ---------------- | --------------------- | 139 | | Precision | Arbitrary | 34 significant digits | 140 | | Non-integers | No | Yes | 141 | | Memory usage | Variable | Fixed 128 bits | 142 | | Performance | Varies with size | Consistent | 143 | 144 | ## Design Decisions 145 | 146 | ### No Operator Overloading 147 | 148 | Unlike some proposals, Decimal does not overload arithmetic operators: 149 | 150 | ```javascript 151 | // This throws an error 152 | const a = new Decimal("10"); 153 | const b = new Decimal("20"); 154 | // a + b; // TypeError! 155 | 156 | // Use methods instead 157 | a.add(b); // => Decimal("30") 158 | ``` 159 | 160 | This decision was made based on implementer feedback and concerns about performance implication, confusion with implicit conversions (given that Decimal is not proposed as a new primitive type). 161 | 162 | ### No Literal Syntax 163 | 164 | There is no special literal syntax for Decimal values: 165 | 166 | ```javascript 167 | // No decimal literal like 10.5m 168 | // Use constructor instead 169 | const decimal = new Decimal("10.5"); 170 | ``` 171 | 172 | This simplifies the implementation and avoids syntax complexity. 173 | 174 | ### Canonicalization vs Cohort Preservation 175 | 176 | 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: 177 | 178 | ```javascript 179 | new Decimal("1.20").toString(); // => "1.2" (trailing zero lost) 180 | ``` 181 | -------------------------------------------------------------------------------- /docs/buildDocs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const { marked } = require('marked'); 3 | const { mkdirp } = require('mkdirp'); 4 | const path = require('path'); 5 | const Prism = require('prismjs'); 6 | const loadLanguages = require('prismjs/components/'); 7 | 8 | // Load additional languages (javascript is included by default) 9 | loadLanguages(['javascript', 'js', 'bash', 'typescript']); 10 | 11 | const encoding = 'utf-8'; 12 | 13 | // Configure marked options 14 | marked.use({ 15 | mangle: false, 16 | headerIds: true, 17 | headerPrefix: '', 18 | }); 19 | 20 | // Create a custom extension that processes links 21 | const linkExtension = { 22 | name: 'mdToHtml', 23 | level: 'inline', 24 | start(src) { return src.match(/\[/)?.index; }, 25 | tokenizer(src, tokens) { 26 | const rule = /^\[([^\]]+)\]\(([^)]+\.md)([^)]*)\)/; 27 | const match = rule.exec(src); 28 | if (match) { 29 | return { 30 | type: 'mdToHtml', 31 | raw: match[0], 32 | text: match[1], 33 | href: match[2].replace(/\.md$/, '.html'), 34 | title: match[3] 35 | }; 36 | } 37 | }, 38 | renderer(token) { 39 | const titleAttr = token.title ? ` title="${token.title}"` : ''; 40 | return `${token.text}`; 41 | } 42 | }; 43 | 44 | // Custom code renderer for syntax highlighting 45 | const codeRenderer = { 46 | name: 'codeRenderer', 47 | renderer: { 48 | code(token) { 49 | const code = token.text || ''; 50 | const language = token.lang || ''; 51 | 52 | if (!language) { 53 | // No language specified - escape the code 54 | const escaped = code.replace(/[&<>"']/g, (char) => { 55 | const escapes = { 56 | '&': '&', 57 | '<': '<', 58 | '>': '>', 59 | '"': '"', 60 | "'": ''' 61 | }; 62 | return escapes[char]; 63 | }); 64 | return `
${escaped}
\n`; 65 | } 66 | 67 | // Use Prism for syntax highlighting if language is supported 68 | const lang = language.toLowerCase(); 69 | // Map common aliases 70 | const langMap = { 71 | 'js': 'javascript', 72 | 'javascript': 'javascript', 73 | 'ts': 'typescript', 74 | 'typescript': 'typescript', 75 | 'bash': 'bash', 76 | 'sh': 'bash' 77 | }; 78 | const mappedLang = langMap[lang] || lang; 79 | 80 | if (Prism.languages[mappedLang]) { 81 | const highlighted = Prism.highlight(code, Prism.languages[mappedLang], mappedLang); 82 | return `
${highlighted}
\n`; 83 | } 84 | 85 | // Fallback for unsupported languages - escape the code 86 | const escaped = code.replace(/[&<>"']/g, (char) => { 87 | const escapes = { 88 | '&': '&', 89 | '<': '<', 90 | '>': '>', 91 | '"': '"', 92 | "'": ''' 93 | }; 94 | return escapes[char]; 95 | }); 96 | return `
${escaped}
\n`; 97 | } 98 | } 99 | }; 100 | 101 | // Register the extensions 102 | marked.use({ extensions: [linkExtension], renderer: codeRenderer.renderer }); 103 | 104 | // Override the heading renderer using the proper API 105 | const headingRenderer = { 106 | name: 'headingRenderer', 107 | renderer: { 108 | heading(token) { 109 | // Extract text content from the token 110 | let text = ''; 111 | if (token.tokens && token.tokens.length > 0) { 112 | text = token.tokens.map(t => t.text || t.raw || '').join(''); 113 | } else if (token.text) { 114 | text = token.text; 115 | } 116 | 117 | const level = token.depth || 1; 118 | 119 | // Check for explicit ID syntax {#id} 120 | let id = ''; 121 | let displayText = text; 122 | const idMatch = text.match(/^(.+?)\s*\{#(.+?)\}$/); 123 | 124 | if (idMatch) { 125 | displayText = idMatch[1].trim(); 126 | id = idMatch[2]; 127 | } else { 128 | // Generate ID from text 129 | id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ''); 130 | } 131 | 132 | return `${displayText}\n`; 133 | } 134 | } 135 | }; 136 | 137 | marked.use(headingRenderer); 138 | 139 | // Also use a walkTokens function to transform .md links in regular links 140 | marked.use({ 141 | walkTokens(token) { 142 | if (token.type === 'link' && token.href && token.href.endsWith('.md')) { 143 | token.href = token.href.replace(/\.md$/, '.html'); 144 | } 145 | } 146 | }); 147 | 148 | async function processMarkdownFile(markdownFile, outputDir, head, tail) { 149 | console.log(`Processing ${markdownFile}...`); 150 | 151 | // Read markdown content 152 | let content = await fs.readFile(markdownFile, { encoding }); 153 | 154 | // Parse markdown 155 | let html = marked(content); 156 | 157 | // Get output filename 158 | const baseName = path.basename(markdownFile, '.md'); 159 | const outputFile = path.join(outputDir, `${baseName}.html`); 160 | 161 | // Combine with header and footer 162 | const fullHtml = head + html + tail; 163 | 164 | // Write output file 165 | await fs.writeFile(outputFile, fullHtml, { encoding }); 166 | console.log(` → ${outputFile}`); 167 | } 168 | 169 | async function build() { 170 | try { 171 | console.log('Building Decimal documentation...\n'); 172 | 173 | // Create output directory 174 | const outputDir = path.join(__dirname, 'out'); 175 | await mkdirp(outputDir); 176 | 177 | // Read header and footer templates 178 | const head = await fs.readFile(path.join(__dirname, 'head.html.part'), { encoding }); 179 | const tail = await fs.readFile(path.join(__dirname, 'tail.html.part'), { encoding }); 180 | 181 | // Get all markdown files 182 | const files = await fs.readdir(__dirname); 183 | const mdFiles = files.filter(f => f.endsWith('.md')); 184 | 185 | // Process each markdown file 186 | for (const file of mdFiles) { 187 | await processMarkdownFile(path.join(__dirname, file), outputDir, head, tail); 188 | } 189 | 190 | // Copy prism.css - using okaidia theme for vibrant syntax highlighting 191 | const prismCss = path.join(__dirname, '..', 'node_modules/prismjs/themes/prism-okaidia.css'); 192 | const prismDest = path.join(outputDir, 'prism.css'); 193 | await fs.copyFile(prismCss, prismDest); 194 | console.log(` → ${prismDest}`); 195 | 196 | // Build and copy proposal-decimal polyfill as browser bundle 197 | const { execSync } = require('child_process'); 198 | const polyfillSrc = path.join(__dirname, '..', 'node_modules/proposal-decimal/src/Decimal.mjs'); 199 | const polyfillDest = path.join(outputDir, 'decimal-polyfill.js'); 200 | 201 | // Build IIFE bundle that exposes DecimalPolyfill globally 202 | execSync(`npx esbuild "${polyfillSrc}" --bundle --format=iife --global-name=DecimalPolyfill --outfile="${polyfillDest}"`, { 203 | cwd: __dirname, 204 | stdio: 'inherit' 205 | }); 206 | console.log(` → ${polyfillDest}`); 207 | 208 | // Also copy the original ES module 209 | const polyfillMjsDest = path.join(outputDir, 'decimal-polyfill.mjs'); 210 | await fs.copyFile(polyfillSrc, polyfillMjsDest); 211 | console.log(` → ${polyfillMjsDest}`); 212 | 213 | console.log('\nBuild complete!'); 214 | } catch (error) { 215 | console.error('Build failed:', error); 216 | process.exit(1); 217 | } 218 | } 219 | 220 | // Run the build 221 | build(); -------------------------------------------------------------------------------- /docs/testExamples.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | const { Decimal } = require('proposal-decimal'); 4 | 5 | // Color codes for terminal output 6 | const colors = { 7 | reset: '\x1b[0m', 8 | red: '\x1b[31m', 9 | green: '\x1b[32m', 10 | yellow: '\x1b[33m', 11 | blue: '\x1b[34m', 12 | gray: '\x1b[90m' 13 | }; 14 | 15 | // Extract code blocks from markdown content 16 | function extractCodeBlocks(content, filename) { 17 | const codeBlocks = []; 18 | const codeBlockRegex = /```(?:javascript|js)\n([\s\S]*?)```/g; 19 | let match; 20 | 21 | while ((match = codeBlockRegex.exec(content)) !== null) { 22 | const code = match[1].trim(); 23 | // Skip blocks that are obviously not meant to be run 24 | if (code.includes('// =>') || code.includes('console.log')) { 25 | codeBlocks.push({ 26 | code, 27 | filename, 28 | line: content.substring(0, match.index).split('\n').length 29 | }); 30 | } 31 | } 32 | 33 | return codeBlocks; 34 | } 35 | 36 | // Convert console.log assertions to actual tests 37 | function convertToTest(code) { 38 | const lines = code.split('\n'); 39 | const testLines = []; 40 | const assertions = []; 41 | 42 | for (let i = 0; i < lines.length; i++) { 43 | const line = lines[i]; 44 | 45 | // Skip empty lines and pure comments 46 | if (line.trim() === '' || line.trim().startsWith('//')) { 47 | continue; 48 | } 49 | 50 | // Check if this line has an expected output comment 51 | const outputMatch = line.match(/(.+?)(?:;)?\s*\/\/\s*=>\s*(.+)/); 52 | if (outputMatch) { 53 | const statement = outputMatch[1].trim(); 54 | const expected = outputMatch[2].trim(); 55 | 56 | // Handle console.log statements 57 | if (statement.startsWith('console.log(')) { 58 | const expr = statement.substring(12, statement.lastIndexOf(')')); 59 | testLines.push(`const result_${i} = ${expr};`); 60 | assertions.push({ 61 | expr: `result_${i}`, 62 | expected, 63 | line: i + 1 64 | }); 65 | } else { 66 | // Direct expression 67 | testLines.push(`const result_${i} = ${statement};`); 68 | assertions.push({ 69 | expr: `result_${i}`, 70 | expected, 71 | line: i + 1 72 | }); 73 | } 74 | } else { 75 | // Regular statement 76 | testLines.push(line); 77 | } 78 | } 79 | 80 | return { testLines, assertions }; 81 | } 82 | 83 | // Run a single test 84 | async function runTest(block) { 85 | const { testLines, assertions } = convertToTest(block.code); 86 | 87 | if (assertions.length === 0) { 88 | return { success: true, message: 'No assertions to test' }; 89 | } 90 | 91 | const fullCode = testLines.join('\n'); 92 | 93 | try { 94 | // Create a function with Decimal in scope 95 | const testFn = new Function('Decimal', fullCode + '\nreturn { ' + 96 | assertions.map((a, i) => `result_${a.line - 1}: result_${a.line - 1}`).join(', ') + 97 | ' };'); 98 | 99 | const results = testFn(Decimal); 100 | 101 | // Check each assertion 102 | for (const assertion of assertions) { 103 | const actual = results[`result_${assertion.line - 1}`]; 104 | const expected = assertion.expected; 105 | 106 | // Convert actual to string for comparison 107 | let actualStr; 108 | if (actual === undefined) { 109 | actualStr = 'undefined'; 110 | } else if (actual === null) { 111 | actualStr = 'null'; 112 | } else if (typeof actual === 'string') { 113 | actualStr = `"${actual}"`; 114 | } else if (actual instanceof Decimal) { 115 | // Handle Decimal objects 116 | if (actual.isNaN()) { 117 | actualStr = '"NaN"'; 118 | } else if (!actual.isFinite()) { 119 | actualStr = actual.toString().includes('-') ? '"-Infinity"' : '"Infinity"'; 120 | } else { 121 | actualStr = `Decimal("${actual.toString()}")`; 122 | } 123 | } else { 124 | actualStr = String(actual); 125 | } 126 | 127 | // Normalize expected value 128 | let normalizedExpected = expected; 129 | if (expected.startsWith('Decimal(')) { 130 | // Already in Decimal format 131 | } else if (expected === 'true' || expected === 'false') { 132 | // Boolean 133 | } else if (expected.match(/^-?\d+n$/)) { 134 | // BigInt 135 | } else if (expected.match(/^".*"$/)) { 136 | // Already quoted string 137 | } else if (!isNaN(expected)) { 138 | // Number 139 | } else { 140 | // Assume it's a string that needs quotes 141 | normalizedExpected = `"${expected}"`; 142 | } 143 | 144 | if (actualStr !== normalizedExpected) { 145 | return { 146 | success: false, 147 | message: `Line ${assertion.line}: Expected ${normalizedExpected}, got ${actualStr}`, 148 | actual: actualStr, 149 | expected: normalizedExpected 150 | }; 151 | } 152 | } 153 | 154 | return { success: true }; 155 | } catch (error) { 156 | return { 157 | success: false, 158 | message: `Error: ${error.message}`, 159 | error 160 | }; 161 | } 162 | } 163 | 164 | // Main test runner 165 | async function testAllExamples() { 166 | console.log(`${colors.blue}Testing Decimal examples...${colors.reset}\n`); 167 | 168 | const files = await fs.readdir(__dirname); 169 | const mdFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.') && !f.endsWith('~') && !(f.startsWith('#') && f.endsWith('#'))); 170 | 171 | let totalBlocks = 0; 172 | let passedBlocks = 0; 173 | let failedBlocks = 0; 174 | let skippedBlocks = 0; 175 | 176 | for (const file of mdFiles) { 177 | const content = await fs.readFile(path.join(__dirname, file), 'utf-8'); 178 | const blocks = extractCodeBlocks(content, file); 179 | 180 | if (blocks.length === 0) continue; 181 | 182 | console.log(`${colors.yellow}${file}:${colors.reset}`); 183 | 184 | for (const block of blocks) { 185 | totalBlocks++; 186 | 187 | // Skip blocks that don't use Decimal 188 | if (!block.code.includes('Decimal')) { 189 | skippedBlocks++; 190 | continue; 191 | } 192 | 193 | const result = await runTest(block); 194 | 195 | if (result.success) { 196 | if (result.message === 'No assertions to test') { 197 | skippedBlocks++; 198 | console.log(` ${colors.gray}Line ${block.line}: Skipped (no assertions)${colors.reset}`); 199 | } else { 200 | passedBlocks++; 201 | console.log(` ${colors.green}✓ Line ${block.line}: Passed${colors.reset}`); 202 | } 203 | } else { 204 | failedBlocks++; 205 | console.log(` ${colors.red}✗ Line ${block.line}: Failed${colors.reset}`); 206 | console.log(` ${colors.red}${result.message}${colors.reset}`); 207 | 208 | // Show code snippet for context 209 | const codeLines = block.code.split('\n'); 210 | const relevantLines = codeLines.slice(0, 5).join('\n '); 211 | console.log(` ${colors.gray}Code:\n ${relevantLines}${colors.reset}`); 212 | } 213 | } 214 | 215 | console.log(''); 216 | } 217 | 218 | // Summary 219 | console.log(`${colors.blue}Summary:${colors.reset}`); 220 | console.log(` Total code blocks: ${totalBlocks}`); 221 | console.log(` ${colors.green}Passed: ${passedBlocks}${colors.reset}`); 222 | console.log(` ${colors.red}Failed: ${failedBlocks}${colors.reset}`); 223 | console.log(` ${colors.gray}Skipped: ${skippedBlocks}${colors.reset}`); 224 | 225 | if (failedBlocks > 0) { 226 | console.log(`\n${colors.red}Some examples failed! Please fix them before building.${colors.reset}`); 227 | process.exit(1); 228 | } else { 229 | console.log(`\n${colors.green}All Decimal examples passed!${colors.reset}`); 230 | } 231 | } 232 | 233 | // Run tests 234 | testAllExamples().catch(error => { 235 | console.error(`${colors.red}Test runner error: ${error.message}${colors.reset}`); 236 | process.exit(1); 237 | }); -------------------------------------------------------------------------------- /docs/testExamples.mjs: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { Decimal } from 'proposal-decimal'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | // Color codes for terminal output 10 | const colors = { 11 | reset: '\x1b[0m', 12 | red: '\x1b[31m', 13 | green: '\x1b[32m', 14 | yellow: '\x1b[33m', 15 | blue: '\x1b[34m', 16 | gray: '\x1b[90m' 17 | }; 18 | 19 | // Extract code blocks from markdown content 20 | function extractCodeBlocks(content, filename) { 21 | const codeBlocks = []; 22 | const codeBlockRegex = /```(?:javascript|js)\n([\s\S]*?)```/g; 23 | let match; 24 | 25 | while ((match = codeBlockRegex.exec(content)) !== null) { 26 | const code = match[1].trim(); 27 | // Skip blocks that are obviously not meant to be run 28 | if (code.includes('// =>') || code.includes('console.log')) { 29 | codeBlocks.push({ 30 | code, 31 | filename, 32 | line: content.substring(0, match.index).split('\n').length 33 | }); 34 | } 35 | } 36 | 37 | return codeBlocks; 38 | } 39 | 40 | // Convert console.log assertions to actual tests 41 | function convertToTest(code) { 42 | const lines = code.split('\n'); 43 | const testLines = []; 44 | const assertions = []; 45 | 46 | for (let i = 0; i < lines.length; i++) { 47 | const line = lines[i]; 48 | 49 | // Skip empty lines and pure comments 50 | if (line.trim() === '' || line.trim().startsWith('//')) { 51 | continue; 52 | } 53 | 54 | // Check if this line has an expected output comment 55 | const outputMatch = line.match(/(.+?)(?:;)?\s*\/\/\s*=>\s*(.+)/); 56 | if (outputMatch) { 57 | const statement = outputMatch[1].trim(); 58 | const expected = outputMatch[2].trim(); 59 | 60 | // Handle console.log statements 61 | if (statement.startsWith('console.log(')) { 62 | const expr = statement.substring(12, statement.lastIndexOf(')')); 63 | testLines.push(`const result_${i} = ${expr};`); 64 | assertions.push({ 65 | expr: `result_${i}`, 66 | expected, 67 | line: i + 1 68 | }); 69 | } else { 70 | // Direct expression 71 | testLines.push(`const result_${i} = ${statement};`); 72 | assertions.push({ 73 | expr: `result_${i}`, 74 | expected, 75 | line: i + 1 76 | }); 77 | } 78 | } else { 79 | // Regular statement 80 | testLines.push(line); 81 | } 82 | } 83 | 84 | return { testLines, assertions }; 85 | } 86 | 87 | // Run a single test 88 | async function runTest(block) { 89 | const { testLines, assertions } = convertToTest(block.code); 90 | 91 | if (assertions.length === 0) { 92 | return { success: true, message: 'No assertions to test' }; 93 | } 94 | 95 | const fullCode = testLines.join('\n'); 96 | 97 | try { 98 | // Create a function with Decimal in scope 99 | const testFn = new Function('Decimal', fullCode + '\nreturn { ' + 100 | assertions.map((a, i) => `result_${a.line - 1}: result_${a.line - 1}`).join(', ') + 101 | ' };'); 102 | 103 | const results = testFn(Decimal); 104 | 105 | // Check each assertion 106 | for (const assertion of assertions) { 107 | const actual = results[`result_${assertion.line - 1}`]; 108 | const expected = assertion.expected; 109 | 110 | // Convert actual to string for comparison 111 | let actualStr; 112 | if (actual === undefined) { 113 | actualStr = 'undefined'; 114 | } else if (actual === null) { 115 | actualStr = 'null'; 116 | } else if (typeof actual === 'string') { 117 | actualStr = `"${actual}"`; 118 | } else if (actual instanceof Decimal) { 119 | // Handle Decimal objects 120 | if (actual.isNaN()) { 121 | actualStr = '"NaN"'; 122 | } else if (!actual.isFinite()) { 123 | actualStr = actual.toString().includes('-') ? '"-Infinity"' : '"Infinity"'; 124 | } else { 125 | actualStr = `Decimal("${actual.toString()}")`; 126 | } 127 | } else { 128 | actualStr = String(actual); 129 | } 130 | 131 | // Normalize expected value 132 | let normalizedExpected = expected; 133 | if (expected.startsWith('Decimal(')) { 134 | // Already in Decimal format 135 | } else if (expected === 'true' || expected === 'false') { 136 | // Boolean 137 | } else if (expected.match(/^-?\d+n$/)) { 138 | // BigInt 139 | } else if (expected.match(/^".*"$/)) { 140 | // Already quoted string 141 | } else if (!isNaN(expected)) { 142 | // Number 143 | } else { 144 | // Assume it's a string that needs quotes 145 | normalizedExpected = `"${expected}"`; 146 | } 147 | 148 | if (actualStr !== normalizedExpected) { 149 | return { 150 | success: false, 151 | message: `Line ${assertion.line}: Expected ${normalizedExpected}, got ${actualStr}`, 152 | actual: actualStr, 153 | expected: normalizedExpected 154 | }; 155 | } 156 | } 157 | 158 | return { success: true }; 159 | } catch (error) { 160 | return { 161 | success: false, 162 | message: `Error: ${error.message}`, 163 | error 164 | }; 165 | } 166 | } 167 | 168 | // Main test runner 169 | async function testAllExamples() { 170 | console.log(`${colors.blue}Testing Decimal examples...${colors.reset}\n`); 171 | 172 | const files = await fs.readdir(__dirname); 173 | const mdFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.') && !f.startsWith('#') && !f.endsWith('~')); 174 | 175 | let totalBlocks = 0; 176 | let passedBlocks = 0; 177 | let failedBlocks = 0; 178 | let skippedBlocks = 0; 179 | 180 | for (const file of mdFiles) { 181 | const content = await fs.readFile(path.join(__dirname, file), 'utf-8'); 182 | const blocks = extractCodeBlocks(content, file); 183 | 184 | if (blocks.length === 0) continue; 185 | 186 | console.log(`${colors.yellow}${file}:${colors.reset}`); 187 | 188 | for (const block of blocks) { 189 | totalBlocks++; 190 | 191 | // Skip blocks that don't use Decimal 192 | if (!block.code.includes('Decimal')) { 193 | skippedBlocks++; 194 | continue; 195 | } 196 | 197 | const result = await runTest(block); 198 | 199 | if (result.success) { 200 | if (result.message === 'No assertions to test') { 201 | skippedBlocks++; 202 | console.log(` ${colors.gray}Line ${block.line}: Skipped (no assertions)${colors.reset}`); 203 | } else { 204 | passedBlocks++; 205 | console.log(` ${colors.green}✓ Line ${block.line}: Passed${colors.reset}`); 206 | } 207 | } else { 208 | failedBlocks++; 209 | console.log(` ${colors.red}✗ Line ${block.line}: Failed${colors.reset}`); 210 | console.log(` ${colors.red}${result.message}${colors.reset}`); 211 | 212 | // Show code snippet for context 213 | const codeLines = block.code.split('\n'); 214 | const relevantLines = codeLines.slice(0, 5).join('\n '); 215 | console.log(` ${colors.gray}Code:\n ${relevantLines}${colors.reset}`); 216 | } 217 | } 218 | 219 | console.log(''); 220 | } 221 | 222 | // Summary 223 | console.log(`${colors.blue}Summary:${colors.reset}`); 224 | console.log(` Total code blocks: ${totalBlocks}`); 225 | console.log(` ${colors.green}Passed: ${passedBlocks}${colors.reset}`); 226 | console.log(` ${colors.red}Failed: ${failedBlocks}${colors.reset}`); 227 | console.log(` ${colors.gray}Skipped: ${skippedBlocks}${colors.reset}`); 228 | 229 | if (failedBlocks > 0) { 230 | console.log(`\n${colors.red}Some examples failed! Please fix them before building.${colors.reset}`); 231 | process.exit(1); 232 | } else { 233 | console.log(`\n${colors.green}All Decimal examples passed!${colors.reset}`); 234 | } 235 | } 236 | 237 | // Run tests 238 | testAllExamples().catch(error => { 239 | console.error(`${colors.red}Test runner error: ${error.message}${colors.reset}`); 240 | process.exit(1); 241 | }); -------------------------------------------------------------------------------- /intl.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Amendments to the ECMAScript® 2024 Internationalization API Specification

5 | 6 | 7 |

8 | This section lists amendments which must be made to ECMA-402, the ECMAScript® 2024 Internationalization API Specification. 9 | Text to be added is marked like this, and text to be deleted is marked like this. 10 | Blocks of unmodified text between modified sections are marked by [...]. 11 |

12 |
13 | 14 | 15 |

Properties of the Decimal128 Prototype Object

16 | 17 | 18 |

Decimal128.prototype.toLocaleString ( [ _locales_ [ , _options_ ] ] )

19 | 20 |

21 | This definition supersedes the definition provided in es2024, . 22 |

23 | 24 |

25 | This function performs the following steps when called: 26 |

27 | 28 | 29 | 1. Let _O_ be the *this* value. 30 | 1. Perform ? RequireInternalSlot(_O_, [[Decimal128Data]]). 31 | 1. Let _numberFormat_ be ? Construct(%Intl.NumberFormat%, « _locales_, _options_ »). 32 | 1. Return FormatNumeric(_numberFormat_, _O_.[[Decimal128Data]]). 33 | 34 |
35 |
36 | 37 | 38 |

NumberFormat Objects

39 | 40 |

Abstract Operations for NumberFormat Objects

41 | 42 | 43 |

44 | ToIntlMathematicalValue ( 45 | _value_: an ECMAScript language value, 46 | ): either a normal completion containing an Intl mathematical value or a throw completion 47 |

48 |
49 |
skip global checks
50 |
true
51 |
description
52 |
53 | It returns _value_ converted to an Intl mathematical value, which is either a mathematical value, or one of ~positive-infinity~, ~negative-infinity~, ~not-a-number~, and ~negative-zero~. 54 | This abstract operation is similar to , but a mathematical value can be returned instead of a Number or BigInt, so that exact decimal values can be represented. 55 |
56 |
57 | 58 | 1. If _value_ has a [[Decimal128Data]] internal slot, then 59 | 1. Let _d_ be _value_.[[Decimal128Data]]. 60 | 1. If _d_ is *NaN*𝔻, return ~not-a-number~. 61 | 1. If _d_ is *+∞*𝔻, return ~positive-infinity~. 62 | 1. If _d_ is *-∞*𝔻, return ~negative-infinity~. 63 | 1. Return _d_. 64 | 1. Let _primValue_ be ? ToPrimitive(_value_, ~number~). 65 | 1. If Type(_primValue_) is BigInt, return ℝ(_primValue_). 66 | 1. If Type(_primValue_) is String, then 67 | 1. Let _str_ be _primValue_. 68 | 1. Else, 69 | 1. Let _x_ be ? ToNumber(_primValue_). 70 | 1. If _x_ is *-0*𝔽, return ~negative-zero~. 71 | 1. Let _str_ be Number::toString(_x_, 10). 72 | 1. Let _text_ be StringToCodePoints(_str_). 73 | 1. Let _literal_ be ParseText(_text_, |StringNumericLiteral|). 74 | 1. If _literal_ is a List of errors, return ~not-a-number~. 75 | 1. Let _intlMV_ be the StringIntlMV of _literal_. 76 | 1. If _intlMV_ is a mathematical value, then 77 | 1. Let _rounded_ be RoundMVResult(abs(_intlMV_)). 78 | 1. If _rounded_ is *+∞*𝔽 and _intlMV_ < 0, return ~negative-infinity~. 79 | 1. If _rounded_ is *+∞*𝔽, return ~positive-infinity~. 80 | 1. If _rounded_ is *+0*𝔽 and _intlMV_ < 0, return ~negative-zero~. 81 | 1. If _rounded_ is *+0*𝔽, return 0. 82 | 1. Return _intlMV_. 83 | 84 |
85 |
86 |
87 | 88 | 89 |

PluralRules Objects

90 | 91 | 92 |

Abstract Operations for PluralRules Objects

93 | 94 | 95 |

96 | ResolvePlural ( 97 | _pluralRules_: an Intl.PluralRules, 98 | _n_: a Number or an Object with an [[Decimal128Data]] internal slot, 99 | ): a Record with fields [[PluralCategory]] (*"zero"*, *"one"*, *"two"*, *"few"*, *"many"*, or *"other"*) and [[FormattedString]] (a String) 100 |

101 |
102 |
description
103 |
The returned Record contains two string-valued fields describing _n_ according to the effective locale and the options of _pluralRules_: [[PluralCategory]] characterizing its plural category, and [[FormattedString]] containing its formatted representation.
104 |
105 | 106 | 1. If _n_ is *+∞*𝔽 or *-∞*𝔽, then 107 | 1. Let _s_ be ! ToString(_n_). 108 | 1. Return the Record { [[PluralCategory]]: *"other"*, [[FormattedString]]: _s_ }. 109 | 1. If _n_ is an Object with a [[Decimal128Data]] internal slot, then 110 | 1. Let _d_ be _n_.[[Decimal128Data]]. 111 | 1. If _d_ is *NaN*𝔻, return the Record { [[PluralCategory]]: *"other"*, [[FormattedString]]: *"NaN"* } . 112 | 1. If _d_ is *+∞*𝔻 or *-∞*𝔻, then 113 | 1. If _n_ is *+∞*𝔻, let _s_ be *"Infinity"*, otherwise let _s_ be *"-Infinity"*. 114 | 1. Return the Record { [[PluralCategory]]: *"other"*, [[FormattedString]]: _s_ }. 115 | 1. Let _n_ be 𝔽(_n_). 116 | 1. Let _s_ be ! ToString(_n_). 117 | 1. Let _locale_ be _pluralRules_.[[Locale]]. 118 | 1. Let _type_ be _pluralRules_.[[Type]]. 119 | 1. If _n_ is a Number, then 120 | 1. Let _res_ be FormatNumericToString(_pluralRules_, ℝ(_n_)). 121 | 1. Else, 122 | 1. Let _res_ be FormatNumericToString(_pluralRules_, _n_). 123 | 1. Let _s_ be _res_.[[FormattedString]]. 124 | 1. Let _operands_ be GetOperands(_s_). 125 | 1. Let _p_ be PluralRuleSelect(_locale_, _type_, _n_, _operands_). 126 | 1. Return the Record { [[PluralCategory]]: _p_, [[FormattedString]]: _s_ }. 127 | 128 |
129 | 130 | 131 |

132 | ResolvePluralRange ( 133 | _pluralRules_: an Intl.PluralRules, 134 | _x_: a Number or an Object with a [[Decimal128Data]] internal slot, 135 | _y_: a Number or an Object with a [[Decimal128Data]] internal slot, 136 | ): either a normal completion containing either *"zero"*, *"one"*, *"two"*, *"few"*, *"many"*, or *"other"*, or a throw completion 137 |

138 |
139 |
description
140 |
The returned String value represents the plural form of the range starting from _x_ and ending at _y_ according to the effective locale and the options of _pluralRules_.
141 |
142 | 143 | 1. If _x_ is *NaN* or *NaN*𝔻 or _y_ is *NaN*or *NaN*𝔻, throw a *RangeError* exception. 144 | 1. Let _xp_ be ResolvePlural(_pluralRules_, _x_). 145 | 1. Let _yp_ be ResolvePlural(_pluralRules_, _y_). 146 | 1. If _xp_.[[FormattedString]] is _yp_.[[FormattedString]], then 147 | 1. Return _xp_.[[PluralCategory]]. 148 | 1. Let _locale_ be _pluralRules_.[[Locale]]. 149 | 1. Let _type_ be _pluralRules_.[[Type]]. 150 | 1. Return PluralRuleSelectRange(_locale_, _type_, _xp_.[[PluralCategory]], _yp_.[[PluralCategory]]). 151 | 152 |
153 |
154 |
155 |
156 | -------------------------------------------------------------------------------- /docs/decimal.md: -------------------------------------------------------------------------------- 1 | # Decimal 2 | 3 | A `Decimal` represents an exact base-10 decimal number, stored in IEEE 754-2019 Decimal128 format. This provides up to 34 significant digits of precision for exact decimal arithmetic. 4 | 5 | 6 | ```javascript 7 | // Exact decimal arithmetic 8 | const price = new Decimal("19.99"); 9 | const tax = new Decimal("0.0825"); 10 | const total = price.multiply(new Decimal("1").add(tax)); // => Decimal("21.64") 11 | total.toString(); // => "21.64" 12 | ``` 13 | 14 | 15 | `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. 16 | 17 | ## Constructor 18 | 19 | ### **new Decimal**(_value_: string | number | bigint) : Decimal 20 | 21 | **Parameters:** 22 | 23 | - `value` (string | number | bigint): The value to convert to a Decimal. 24 | 25 | **Returns:** a new `Decimal` object. 26 | 27 | Creates a new `Decimal` object representing an exact decimal number. 28 | 29 | The most reliable way to create a `Decimal` is from a string, which preserves the exact decimal representation. Creating from a `Number` may introduce rounding errors if that number cannot be exactly represented in binary floating-point. 30 | 31 | Example usage: 32 | 33 | ```js 34 | // From string (recommended for exact values) 35 | d1 = new Decimal("123.456"); 36 | d2 = new Decimal("-0.0078"); 37 | d3 = new Decimal("6.022e23"); // Scientific notation 38 | 39 | // From number (be aware of binary floating-point limitations) 40 | d4 = new Decimal(42); 41 | d5 = new Decimal(0.1); // Actually Decimal("0.1000000000000000055511151231257827...") 42 | 43 | // From bigint 44 | d6 = new Decimal(123n); 45 | ``` 46 | 47 | ## Special Values 48 | 49 | ### NaN (Not a Number) 50 | 51 | `Decimal` has its own NaN value, distinct from JavaScript's global `NaN`: 52 | 53 | ```js 54 | const notANumber = new Decimal("NaN"); 55 | notANumber.toString(); // => "NaN" 56 | 57 | // Operations with NaN propagate 58 | notANumber.add(new Decimal("5")).toString(); // => "NaN" 59 | ``` 60 | 61 | ### Infinity 62 | 63 | `Decimal` supports positive and negative infinity, distinct from JavaScript's `Infinity`: 64 | 65 | ```js 66 | const posInf = new Decimal("Infinity"); 67 | const negInf = new Decimal("-Infinity"); 68 | 69 | posInf.toString(); // => "Infinity" 70 | negInf.toString(); // => "-Infinity" 71 | 72 | // Arithmetic with infinity 73 | posInf.add(new Decimal("1")).toString(); // => "Infinity" 74 | new Decimal("1").divide(new Decimal("0")).toString(); // => "Infinity" 75 | ``` 76 | 77 | ## Methods 78 | 79 | ### **significand()** : bigint 80 | 81 | Returns the significand (mantissa) of the decimal number as a bigint, without regard to the exponent. 82 | 83 | ```js 84 | new Decimal("123.45").significand(); // => 12345n 85 | new Decimal("0.00123").significand(); // => 123n 86 | ``` 87 | 88 | ### **exponent()** : number 89 | 90 | Returns the base-10 exponent of the decimal number. 91 | 92 | ```js 93 | new Decimal("123.45").exponent(); // => -2 (representing 12345 × 10^-2) 94 | new Decimal("1.23e5").exponent(); // => 3 (representing 123 × 10^3) 95 | ``` 96 | 97 | ### **isNaN()** : boolean 98 | 99 | Returns true if the value is NaN (Not a Number). 100 | 101 | ```js 102 | new Decimal("NaN").isNaN(); // => true 103 | new Decimal("123").isNaN(); // => false 104 | ``` 105 | 106 | ### **isFinite()** : boolean 107 | 108 | Returns true if the value is finite (not NaN or infinity). 109 | 110 | ```js 111 | new Decimal("123").isFinite(); // => true 112 | new Decimal("Infinity").isFinite(); // => false 113 | new Decimal("NaN").isFinite(); // => false 114 | ``` 115 | 116 | ## Arithmetic Methods 117 | 118 | All arithmetic methods return a new `Decimal` object. The original object is never modified. 119 | 120 | ### **add**(_other_: Decimal) : Decimal 121 | 122 | Returns the sum of this decimal and another Decimal. 123 | 124 | ```js 125 | const a = new Decimal("10.25"); 126 | const b = new Decimal("5.75"); 127 | a.add(b).toString(); // => "16" 128 | 129 | const c = new Decimal("3.5"); 130 | a.add(c).toString(); // => "13.75" 131 | ``` 132 | 133 | ### **subtract**(_other_: Decimal) : Decimal 134 | 135 | Returns the difference between this decimal and another Decimal. 136 | 137 | ```js 138 | const a = new Decimal("10.25"); 139 | const b = new Decimal("5.75"); 140 | a.subtract(b).toString(); // => "4.5" 141 | 142 | const c = new Decimal("3.5"); 143 | a.subtract(c).toString(); // => "6.75" 144 | ``` 145 | 146 | ### **multiply**(_other_: Decimal) : Decimal 147 | 148 | Returns the product of this decimal and another Decimal. 149 | 150 | ```js 151 | const price = new Decimal("19.99"); 152 | const quantity = new Decimal("3"); 153 | price.multiply(quantity).toString(); // => "59.97" 154 | ``` 155 | 156 | ### **divide**(_other_: Decimal) : Decimal 157 | 158 | Returns the quotient of this decimal divided by another Decimal. 159 | 160 | ```js 161 | const total = new Decimal("100"); 162 | const parts = new Decimal("3"); 163 | total.divide(parts).toString(); // => "33.33333333333333333333333333333333" 164 | ``` 165 | 166 | ### **remainder**(_other_: Decimal) : Decimal 167 | 168 | Returns the remainder of dividing this decimal by another Decimal. 169 | 170 | ```js 171 | const a = new Decimal("10"); 172 | const b = new Decimal("3"); 173 | a.remainder(b).toString(); // => "1" 174 | ``` 175 | 176 | ### **abs**() : Decimal 177 | 178 | Returns the absolute value. 179 | 180 | ```js 181 | new Decimal("-123.45").abs().toString(); // => "123.45" 182 | new Decimal("123.45").abs().toString(); // => "123.45" 183 | ``` 184 | 185 | ### **negate**() : Decimal 186 | 187 | Returns the negation of this value. 188 | 189 | ```js 190 | new Decimal("123.45").negate().toString(); // => "-123.45" 191 | new Decimal("-123.45").negate().toString(); // => "123.45" 192 | ``` 193 | 194 | ## Rounding Methods 195 | 196 | All rounding methods follow IEEE 754-2019 rounding modes. 197 | 198 | ### **round**(_scale_?: number, _roundingMode_?: string) : Decimal 199 | 200 | Rounds to a given number of decimal places. 201 | 202 | **Parameters:** 203 | 204 | - `scale` (number): Number of decimal places to round to (default: 0) 205 | - `roundingMode` (string): One of "ceil", "floor", "trunc", "halfEven" (default), or "halfExpand" 206 | 207 | ```js 208 | const d = new Decimal("123.456"); 209 | d.round().toString(); // => "123" 210 | d.round(2).toString(); // => "123.46" 211 | d.round(2, "floor").toString(); // => "123.45" 212 | ``` 213 | 214 | ## Comparison Methods 215 | 216 | ### **equals**(_other_: Decimal) : boolean 217 | 218 | Returns true if this decimal equals another Decimal. 219 | 220 | ```js 221 | const a = new Decimal("123.45"); 222 | const b = new Decimal("123.45"); 223 | const c = new Decimal("123.46"); 224 | a.equals(b); // => true 225 | a.equals(c); // => false 226 | ``` 227 | 228 | ### **lessThan**(_other_: Decimal) : boolean 229 | 230 | Returns true if this decimal is less than another Decimal. 231 | 232 | ```js 233 | const a = new Decimal("10"); 234 | const b = new Decimal("20"); 235 | a.lessThan(b); // => true 236 | b.lessThan(a); // => false 237 | ``` 238 | 239 | ### **lessThanOrEqual**(_other_: Decimal) : boolean 240 | 241 | Returns true if this decimal is less than or equal to another Decimal. 242 | 243 | ### **greaterThan**(_other_: Decimal) : boolean 244 | 245 | Returns true if this decimal is greater than another Decimal. 246 | 247 | ### **greaterThanOrEqual**(_other_: Decimal) : boolean 248 | 249 | Returns true if this decimal is greater than or equal to another Decimal. 250 | 251 | ### **compare**(_other_: Decimal) : number 252 | 253 | Returns -1, 0, or 1 depending on whether this decimal is less than, equal to, or greater than another Decimal. 254 | 255 | ```js 256 | const a = new Decimal("10"); 257 | const b = new Decimal("20"); 258 | const c = new Decimal("30"); 259 | 260 | a.compare(b); // => -1 261 | b.compare(b); // => 0 262 | c.compare(b); // => 1 263 | ``` 264 | 265 | ## Conversion Methods 266 | 267 | ### **toString**() : string 268 | 269 | Returns a string representation of the decimal value. 270 | 271 | ```js 272 | new Decimal("123.45").toString(); // => "123.45" 273 | new Decimal("1.23e5").toString(); // => "123000" 274 | ``` 275 | 276 | ### **toFixed**({_digits_: number}) : string 277 | 278 | Returns a string with a fixed number of decimal places. 279 | 280 | ```js 281 | const d = new Decimal("123.456"); 282 | d.toFixed({ digits: 0 }); // => "123" 283 | d.toFixed({ digits: 2 }); // => "123.46" 284 | d.toFixed({ digits: 5 }); // => "123.45600" 285 | ``` 286 | 287 | ### **toExponential**({_digits_: number}?) : string 288 | 289 | Returns a string in exponential notation. 290 | 291 | ```js 292 | const d = new Decimal("123.456"); 293 | d.toExponential(); // => "1.23456e+2" 294 | d.toExponential({ digits: 2 }); // => "1.23e+2" 295 | ``` 296 | 297 | ### **toPrecision**({ _digits_: number}) : string 298 | 299 | Returns a string with the specified number of significant digits. 300 | 301 | ```js 302 | const d = new Decimal("123.456"); 303 | d.toPrecision({ digits: 2 }); // => "1.2e+2" 304 | d.toPrecision({ digits: 5 }); // => "123.46" 305 | ``` 306 | 307 | ### **toNumber**() : number 308 | 309 | Converts to a JavaScript Number. Note: This may lose precision. 310 | 311 | ```js 312 | new Decimal("123.45").toNumber(); // => 123.45 313 | new Decimal("123.456789012345678901234567890123").toNumber(); // => 123.45678901234568 314 | ``` 315 | 316 | ### **toBigInt**() : bigint 317 | 318 | Converts to a BigInt, throwing if the number isn't actually an integer: 319 | 320 | ```js 321 | new Decimal("123").toBigInt(); // => 123n 322 | new Decimal("123.99").toBigInt(); // throws 323 | ``` 324 | 325 | ### **valueOf**() 326 | 327 | This method always throws, since there is currently no primitive that would exactly match the numeric representation of a Decimal. 328 | ``` 329 | -------------------------------------------------------------------------------- /docs/why-decimal.md: -------------------------------------------------------------------------------- 1 | # Why Decimal? 2 | 3 | ## The Problem with Number 4 | 5 | JavaScript's `Number` type has served the language well for 6 | decades, but it has a fundamental limitation: it uses binary 7 | floating-point arithmetic (IEEE 754 binary64), which means 8 | that many decimal values that humans work with every day 9 | cannot be represented exactly. 10 | 11 | ### The Classic Example 12 | 13 | Many JS programmers know this one: 14 | 15 | ```javascript 16 | 0.1 + 0.2; // => 0.30000000000000004 17 | ``` 18 | 19 | This isn't a bug—it's a consequence of how binary 20 | floating-point works. Just as 1/3 cannot be represented 21 | exactly as a finite decimal (it's 0.333…), many decimal 22 | values cannot be represented exactly in binary. In fact, 23 | _none_ of the values in the example 24 | above—0.1, 0.2, 25 | or 0.3—can be represented exactly in 26 | binary floating-point: 27 | 28 | ```javascript 29 | // None of these are exact in binary 30 | (0.1).toPrecision(20); // => "0.10000000000000000555" 31 | (0.2).toPrecision(20); // => "0.20000000000000001110" 32 | (0.3).toPrecision(20); // => "0.29999999999999998890" 33 | ``` 34 | 35 | ### Real-World Impact 36 | 37 | This inherent limitation of base-2 floating-point numbers 38 | affects many common use cases for JS programmers, especially 39 | financial calculations, where the need for precision is very 40 | high. In these settings, rounding errors—which begin when 41 | converting from decimal notation to binary floating-point 42 | and which get compounded as more and more arithmetic gets 43 | done on these values—can be exposed, despite careful 44 | programming. 45 | 46 | When systems exchange decimal data (coming from, e.g., 47 | databases, APIs, spreadsheets), the binary representation 48 | can introduce subtle errors that accumulate over time or 49 | cause validation failures. We have seen that using numeric 50 | equality `===` can fail in very simple cases, so we need to 51 | have some kind of alternative such as string 52 | comparison. Using `toString` on `Number` might be one 53 | approach, but this can switch between decimal and 54 | exponential syntax, so one should perhaps uniformly use 55 | `toFixed` or `toPrecision`. But what should the arguments 56 | be? 57 | 58 | ## How Decimal Solves These Problems 59 | 60 | The `Decimal` type uses base-10 arithmetic, storing numbers 61 | in the same decimal format that humans use. This provides 62 | exact representation for decimal values within its precision 63 | range (34 significant digits). The developer knows that 0.1, 64 | 0.2, and 0.3 really _are_ those values, internally. No 65 | rounding needed: 66 | 67 | ```javascript 68 | const a = new Decimal("0.1"); 69 | const b = new Decimal("0.2"); 70 | a.add(b).toString(); // => "0.3" (exactly!) 71 | a.add(b).equals(new Decimal("0.3")); // true (finally!) 72 | ``` 73 | 74 | ## The Technical Foundation 75 | 76 | ### IEEE 754-2019 Decimal128 77 | 78 | Decimal is based on the IEEE 754-2019 Decimal128 standard, which provides: 79 | 80 | - **34 significant digits of precision**: More than enough for financial calculations, scientific measurements, and other common use cases 81 | - **Base-10 arithmetic**: Calculations are performed in decimal, matching human expectations 82 | - **Industry standard**: Already implemented in other languages and databases 83 | 84 | ### Why Not Arbitrary Precision? 85 | 86 | While arbitrary-precision decimal libraries exist, Decimal128 offers important advantages: 87 | 88 | 1. **Predictable performance**: Fixed 128-bit size means consistent memory usage and performance 89 | 2. **Interoperability**: Matches decimal types in databases and other languages 90 | 3. **Sufficient precision**: 34 digits handles virtually all real-world use cases 91 | 4. **Simpler implementation**: Easier for JavaScript engines to optimize 92 | 93 | ## Common Workarounds and Their Limitations 94 | 95 | ### Working with Cents 96 | 97 | A common workaround is to use integers representing cents: 98 | 99 | ```javascript 100 | const priceInCents = 1999; // $19.99 101 | const taxRate = 0.0825; 102 | const taxInCents = Math.round(priceInCents * taxRate); 103 | ``` 104 | 105 | But this approach has limitations: it requires constant conversion between cents and dollars, it doesn't work for all currencies (some use 3 decimal places). Calculations (e.g., currency conversion), especially complex ones, can still introduce floating-point errors. 106 | 107 | ### Why Not Just Use Helper Functions? 108 | 109 | Some suggest that we could solve decimal arithmetic problems with helper functions like `decimalAdd`: 110 | 111 | ```javascript 112 | // Attempt 1: Using toPrecision 113 | Number.prototype.decimalAdd = function (operand) { 114 | return Number((this + operand).toPrecision(15)); 115 | }; 116 | ``` 117 | 118 | And this can work for simple cases: 119 | 120 | ```javascript 121 | (0.1).decimalAdd(0.2); // => 0.3 122 | ``` 123 | 124 | But it fails for others: 125 | 126 | ```javascript 127 | (1.551).decimalAdd(-1.55)); // => 0.00099999999999989 128 | // Should be 0.001! 129 | ``` 130 | 131 | The problem? We're trying to round a binary approximation back to decimal, but the damage is already done. Let's try being smarter: 132 | 133 | ```javascript 134 | // Attempt 2: Dynamic precision based on magnitude 135 | Number.prototype.decimalAdd = function (x) { 136 | let numFraction = 137 | 15 - 138 | Math.max( 139 | Math.ceil(Math.log10(Math.abs(this))), 140 | Math.ceil(Math.log10(Math.abs(x))), 141 | ); 142 | return Number((this + x).toFixed(numFraction)); 143 | } 144 | ``` 145 | 146 | This approach is clever and can handle many straightforward cases, but it still has edge cases when dealing with values that have more than 15 significant digits, such as those arising in complex financial calculations and scientific measurements. The fundamental issue is that once you've converted to binary floating-point, you've already lost information. No amount of clever rounding can reliably recover the original decimal intent. 147 | 148 | ### Using toFixed() 149 | 150 | Another workaround is aggressive use of `toFixed()`: 151 | 152 | ```javascript 153 | const result = (0.1 + 0.2).toFixed(2); // => "0.30" 154 | ``` 155 | 156 | But this has problems too. Even with, say, `toFixed(2)`, you can still be off by a penny (when thinking of financial calculations in a currency that has pennies). The fundamental problem: `toFixed()` is a display function, not a solution for exact arithmetic. It masks errors rather than preventing them. 157 | 158 | ### Epsilon Comparisons 159 | 160 | Some developers use "close enough" comparisons: 161 | 162 | ```javascript 163 | function areEqual(a, b, epsilon = 0.0001) { 164 | return Math.abs(a - b) < epsilon; 165 | } 166 | ``` 167 | 168 | But this is fragile and can fail even with simple arithmetic. The fundamental flaw: epsilon comparison doesn't fix the errors—it just ignores them until they grow too large to ignore. 169 | 170 | ## The Fundamental Incompatibility 171 | 172 | The core issue isn't just about precision—it's about the fundamental incompatibility between how humans write numbers (base 10) and how binary floating-point stores them (base 2). It's true that, through careful programming and working in a domain with simple calculations whose complexity is limited out-of-band, inherent rounding issues *might* be avoided. But if the complexity of a calculation is not known, rounding issues are always going to be lurking in the shadows, so to speak, waiting to expose a rounding error to the programmer. 173 | 174 | ### Most Decimal Numbers Cannot Be Exactly Represented 175 | 176 | Here's a startling fact: statistically, most human-authored decimal numbers cannot be exactly represented as binary floating-point numbers. This isn't a bug or limitation of JavaScript—it's mathematics. 177 | 178 | ```javascript 179 | const decimals = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]; 180 | 181 | decimals.forEach((d) => { 182 | // Convert to binary and back to see if it's exact 183 | const binary = d.toString(2); 184 | const isExact = d === Number(d.toPrecision(17)); 185 | console.log(`${d}: ${isExact ? "exact" : "NOT exact"}`); 186 | }); 187 | 188 | // Output: 189 | // 0.1: NOT exact 190 | // 0.2: NOT exact 191 | // 0.3: NOT exact 192 | // 0.4: NOT exact 193 | // 0.5: exact (0.5 = 1/2, a power of 2!) 194 | // 0.6: NOT exact 195 | // 0.7: NOT exact 196 | // 0.8: NOT exact 197 | // 0.9: NOT exact 198 | ``` 199 | 200 | Out of the single-digit decimals, only 0.5 can be exactly represented! This is because 0.5 = 1/2, and powers of 2 are the only fractions that binary can represent exactly. Looking at, say, the numbers 0.00, 0.01, …, 1.00, the results are even more startling. 201 | 202 | ## What Decimal Doesn't Solve 203 | 204 | We have argued that Decimal is an important addition to JS. In some applications, the inherent difficulties of rounding might get exposed. Nonetheless, it's important to understand that Decimal isn't a silver bullet for all numeric issues. 205 | 206 | ### Rational Numbers and Range Limits 207 | 208 | Decimal is based on IEEE 754 Decimal128, which has specific limitations: 209 | 210 | **Range limits:** Decimal can only represent values within its range: 211 | 212 | ```javascript 213 | // Maximum value: approximately ±9.999...×10^6144 (34 digits) 214 | const maxValue = new Decimal("9.999999999999999999999999999999999e6144"); 215 | 216 | // Minimum non-zero value: ±1×10^-6143 217 | const minValue = new Decimal("1e-6143"); 218 | 219 | // Values outside this range overflow or underflow 220 | const tooLarge = new Decimal("1e6145"); // => Infinity 221 | const tooSmall = new Decimal("1e-6144"); // => 0 222 | ``` 223 | 224 | **Rational numbers with infinite decimal expansions:** Decimal cannot exactly represent fractions like 1/3 or 1/7: 225 | 226 | ```javascript 227 | const oneThird = new Decimal("1").divide(new Decimal("3")); 228 | console.log(oneThird.toString()); 229 | // => "0.3333333333333333333333333333333333" (34 threes) 230 | ``` 231 | 232 | For applications requiring exact rational arithmetic, a rational number type (representing numerator/denominator) would be more appropriate. 233 | 234 | ### Performance 235 | 236 | Decimal operations are slower than native Number operations. For performance-critical code that doesn't need exact decimal arithmetic, Number remains the better choice. 237 | 238 | ### Existing Ecosystem 239 | 240 | The vast JavaScript ecosystem uses Number. Decimal values need to be converted when interfacing with existing libraries and APIs. We hope that, over time, with advocacy and developer education, many APIs might support Decimals as well as Numbers. 241 | 242 | ## When to Use Decimal 243 | 244 | Use Decimal when you need: 245 | 246 | - **Exact decimal arithmetic**: Financial calculations, currency, percentages 247 | - **Data integrity**: Preserving exact values from databases or user input 248 | - **Regulatory compliance**: Meeting precision requirements for financial reporting 249 | - **Cross-system consistency**: Matching decimal behavior of other systems 250 | 251 | Continue using Number when you need: 252 | 253 | - **Maximum performance**: Games, graphics, real-time systems 254 | - **Scientific calculations**: Trigonometry, logarithms, complex math 255 | - **Existing library compatibility**: Working with the current ecosystem 256 | - **Binary data**: Bit manipulation, binary protocols 257 | 258 | ## The Path Forward 259 | 260 | Decimal represents a pragmatic addition to JavaScript's numeric types. It complements, and doesn't replace, Number. Similarly, Decimal is based on the same IEEE 754 standard that Number is based on. Decimal solves problems without over-engineering. Moreover, we have designed Decimal to be future-friendly, to work with potential operator overloading in future editions of ECMAScript. 261 | 262 | ## Conclusion 263 | 264 | The addition of Decimal to JavaScript addresses a long-standing pain point in the language. By providing exact decimal arithmetic, it enables developers to write financial and business logic with confidence, eliminate subtle rounding bugs, and match the decimal behavior expected by users and external systems. 265 | 266 | While Number remains appropriate for many use cases, Decimal fills a critical gap for applications that work with human-readable decimal values. It's not about replacing JavaScript's existing numeric types—it's about having the right tool for the right job. 267 | -------------------------------------------------------------------------------- /docs/cookbook.md: -------------------------------------------------------------------------------- 1 | # Decimal Cookbook 2 | 3 | ## Overview 4 | 5 | This cookbook contains code examples for common use cases and patterns when working with the `Decimal` type. These examples demonstrate best practices for exact decimal arithmetic in JavaScript. 6 | 7 | ## Getting Started 8 | 9 | ### Basic Usage 10 | 11 | Create Decimal values from strings for exact representation: 12 | 13 | ```javascript 14 | // Create from string 15 | const price = new Decimal("19.99"); 16 | const quantity = new Decimal("3"); 17 | 18 | // Perform calculations 19 | const total = price.multiply(quantity); 20 | console.log(total.toString()); // => "59.97" 21 | ``` 22 | 23 | ## Prior Art: Decimal Arithmetic in Other Languages 24 | 25 | 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. 26 | 27 | ### Python 28 | 29 | Python includes the [`decimal`](https://docs.python.org/3/library/decimal.html) module in its standard library (since 2003): 30 | 31 | ```python 32 | from decimal import Decimal 33 | 34 | # Financial calculations 35 | price = Decimal("19.99") 36 | quantity = Decimal("3") 37 | tax_rate = Decimal("0.0825") 38 | 39 | subtotal = price * quantity 40 | tax = subtotal * tax_rate 41 | total = subtotal + tax 42 | 43 | print(f"${total:.2f}") # => "$64.92" 44 | ``` 45 | 46 | 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. 47 | 48 | ### Java 49 | 50 | 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: 51 | 52 | ```java 53 | import java.math.BigDecimal; 54 | import java.math.RoundingMode; 55 | 56 | BigDecimal price = new BigDecimal("19.99"); 57 | BigDecimal quantity = new BigDecimal("3"); 58 | BigDecimal taxRate = new BigDecimal("0.0825"); 59 | 60 | BigDecimal subtotal = price.multiply(quantity); 61 | BigDecimal tax = subtotal.multiply(taxRate); 62 | BigDecimal total = subtotal.add(tax); 63 | 64 | // Round to 2 decimal places 65 | total = total.setScale(2, RoundingMode.HALF_UP); 66 | System.out.println(total); // => "64.62" 67 | ``` 68 | 69 | `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). 70 | 71 | ### C\# 72 | 73 | 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: 74 | 75 | ```csharp 76 | decimal price = 19.99m; 77 | decimal quantity = 3m; 78 | decimal taxRate = 0.0825m; 79 | 80 | decimal subtotal = price * quantity; 81 | decimal tax = subtotal * taxRate; 82 | decimal total = subtotal + tax; 83 | 84 | Console.WriteLine($"${total:F2}"); // => "$64.92" 85 | ``` 86 | 87 | ### Ruby 88 | 89 | Ruby includes [`BigDecimal`]((https://ruby-doc.org/3.4.1/exts/json/BigDecimal.html)) in its standard library: 90 | 91 | ```ruby 92 | require 'bigdecimal' 93 | 94 | price = BigDecimal("19.99") 95 | quantity = BigDecimal("3") 96 | tax_rate = BigDecimal("0.0825") 97 | 98 | subtotal = price * quantity 99 | tax = subtotal * tax_rate 100 | total = subtotal + tax 101 | 102 | puts "$%.2f" % total # => "$64.92" 103 | ``` 104 | 105 | Ruby on Rails uses `BigDecimal` for handling database `decimal` columns by default, making it the standard approach for money in Rails applications. 106 | 107 | ### Swift 108 | 109 | Swift's Foundation framework includes [`Decimal`](https://developer.apple.com/documentation/foundation/decimal) (formerly [`NSDecimalNumber`](https://developer.apple.com/documentation/foundation/nsdecimalnumber)): 110 | 111 | ```swift 112 | import Foundation 113 | 114 | let price = Decimal(string: "19.99")! 115 | let quantity = Decimal(string: "3")! 116 | let taxRate = Decimal(string: "0.0825")! 117 | 118 | let subtotal = price * quantity 119 | let tax = subtotal * taxRate 120 | let total = subtotal + tax 121 | 122 | let formatter = NumberFormatter() 123 | formatter.numberStyle = .currency 124 | print(formatter.string(from: total as NSDecimalNumber)!) // => "$64.92" 125 | ``` 126 | 127 | Swift's `Decimal` is the recommended type for financial calculations in iOS and macOS applications. 128 | 129 | ### SQL 130 | 131 | Most SQL databases (e.g., [PostgreSQL](https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL)) treat decimal arithmetic as fundamental: 132 | 133 | ```sql 134 | SELECT 135 | price, 136 | quantity, 137 | price * quantity AS subtotal, 138 | (price * quantity) * 0.0825 AS tax, 139 | (price * quantity) * 1.0825 AS total 140 | FROM products 141 | WHERE id = 1; 142 | ``` 143 | 144 | 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. 145 | 146 | ### Why This Matters for JavaScript 147 | 148 | JavaScript forces developers working with numeric data in a setting where exactness matters to choose between: 149 | 150 | - Binary floats (precision errors, bugs, non-trivial knowledge of binary float problems and some countermeasures that may not always work) 151 | - Userland libraries (bundle size, coordination, performance) 152 | - Integer "cents" (cognitive overhead, internationalization issues) 153 | 154 | 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. 155 | 156 | ## Common Patterns 157 | 158 | This section demonstrates common patterns and best practices when working with Decimal. 159 | 160 | ### Working with Monetary Values 161 | 162 | Create Decimal values from strings to ensure exact representation: 163 | 164 | ```javascript 165 | const price = new Decimal("19.99"); 166 | const tax = new Decimal("0.0825"); 167 | 168 | // Calculate with confidence 169 | const taxAmount = price.multiply(tax); 170 | const total = price.add(taxAmount); 171 | console.log(total.toFixed(2)); // => "21.64" 172 | ``` 173 | 174 | ### Accumulating Values 175 | 176 | When accumulating many values in the course of a calculation with several steps, use Decimal to avoid rounding errors: 177 | 178 | ```javascript 179 | const transactions = ["10.50", "25.75", "3.99", "100.00", "45.25"]; 180 | 181 | // Sum all transactions 182 | const total = transactions.reduce((sum, amount) => { 183 | return sum.add(new Decimal(amount)); 184 | }, new Decimal(0)); 185 | 186 | console.log(total.toString()); // => "185.49" 187 | ``` 188 | 189 | ### Splitting Bills 190 | 191 | Handle bill splitting with proper rounding: 192 | 193 | ```javascript 194 | function splitBill(totalAmount, numberOfPeople) { 195 | const total = new Decimal(totalAmount); 196 | const people = new Decimal(numberOfPeople); 197 | 198 | // Calculate per-person amount 199 | const perPerson = total.divide(people); 200 | 201 | // Round down to cents for most people 202 | const roundedPerPerson = perPerson.round(2, "floor"); 203 | 204 | // Last person pays the remainder to ensure exact total 205 | const lastPersonPays = total.subtract( 206 | roundedPerPerson.multiply(people.subtract(new Decimal(1))), 207 | ); 208 | 209 | return { 210 | perPerson: roundedPerPerson.toFixed(2), 211 | lastPerson: lastPersonPays.toFixed(2), 212 | }; 213 | } 214 | 215 | const split = splitBill("100.00", "3"); 216 | console.log(split); // => { perPerson: "33.33", lastPerson: "33.34" } 217 | ``` 218 | 219 | ### Percentage Calculations 220 | 221 | Calculate percentages accurately: 222 | 223 | ```javascript 224 | function calculateWithPercentage(amount, percentage) { 225 | const base = new Decimal(amount); 226 | const percent = new Decimal(percentage).divide(new Decimal(100)); 227 | const percentageAmount = base.multiply(percent); 228 | 229 | return { 230 | amount: percentageAmount.toFixed(2), 231 | total: base.add(percentageAmount).toFixed(2), 232 | }; 233 | } 234 | 235 | // Calculate 7.25% sales tax 236 | const tax = calculateWithPercentage("19.99", "7.25"); 237 | console.log(tax); // => { amount: "1.45", total: "21.44" } 238 | 239 | // Calculate 15% tip 240 | const tip = calculateWithPercentage("45.50", "15"); 241 | console.log(tip); // => { amount: "6.83", total: "52.33" } 242 | ``` 243 | 244 | ### Creating Decimal values 245 | 246 | The most reliable way to create Decimal values is from strings: 247 | 248 | ```javascript 249 | // From string (recommended for exact values) 250 | const price = new Decimal("19.99"); 251 | const tax = new Decimal("0.0825"); 252 | const small = new Decimal("0.000001"); 253 | const large = new Decimal("9999999999999999999999999999999999"); // 34 digits 254 | ``` 255 | 256 | You can also create Decimals from Numbers, but be aware of _binary_ floating-point limitations. When converting a Number to a Decimal, the Number is first converted to a string using `toExponential()`, then that string is parsed as a Decimal: 257 | 258 | ```javascript 259 | // From Number 260 | const fromNumber = new Decimal(0.1); 261 | // The Number 0.1 is actually 0.1000000000000000055511151231257827... 262 | // It's converted to "1e-1" via toExponential(), then parsed as Decimal "0.1" 263 | 264 | const integer = new Decimal(42); // Exact for integers within safe range 265 | ``` 266 | 267 | BigInt values can also be used to create Decimals: 268 | 269 | ```javascript 270 | // From BigInt 271 | const fromBigInt = new Decimal(123n); 272 | const largeBigInt = new Decimal(999999999999999999999n); 273 | ``` 274 | 275 | ### Converting to and from BigInt 276 | 277 | Decimal can convert to and from BigInt, with some limitations. 278 | 279 | Creating Decimal from BigInt values: 280 | 281 | ```javascript 282 | const bigIntValue = 123456789012345678901234567890n; 283 | const decimal = new Decimal(bigIntValue); 284 | console.log(decimal.toString()); // => "123456789012345678901234567890" 285 | ``` 286 | 287 | Converting integer Decimals to BigInt: 288 | 289 | ```javascript 290 | const integerDecimal = new Decimal("12345"); 291 | const toBigInt = integerDecimal.toBigInt(); 292 | console.log(toBigInt); // => 12345n 293 | ``` 294 | 295 | Non-integer Decimals cannot be converted to BigInt: 296 | 297 | ```javascript 298 | const fractionalDecimal = new Decimal("123.45"); 299 | try { 300 | fractionalDecimal.toBigInt(); 301 | } catch (e) { 302 | console.log(e.message); // => "Cannot convert 123.45 to a BigInt" 303 | } 304 | ``` 305 | 306 | If one wants to ensure that a Decimal value represents, semantically, an integer, use `round` before convering to BigInt: 307 | 308 | ```javascript 309 | const fractionalDecimal = new Decimal("123.45"); // same as previous example 310 | fractionalDecimal.round().toBigInt(); // ...but doesn't throw 311 | ``` 312 | 313 | Very large integers beyond `Number`'s safe range work perfectly: 314 | 315 | ```javascript 316 | const largeDecimal = new Decimal("99999999999999999999999999999999"); 317 | const largeBigInt = largeDecimal.toBigInt(); 318 | console.log(largeBigInt); // => 99999999999999999999999999999999n 319 | ``` 320 | 321 | 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. 322 | 323 | ## Financial Calculations 324 | 325 | ### Calculate invoice total with tax 326 | 327 | A common pattern for calculating totals with tax: 328 | 329 | ```javascript 330 | function calculateInvoice(items, taxRate) { 331 | // Sum all line items using reduce 332 | const subtotal = items.reduce((sum, item) => { 333 | const itemTotal = new Decimal(item.price).multiply( 334 | new Decimal(item.quantity), 335 | ); 336 | return sum.add(itemTotal); 337 | }, new Decimal(0)); 338 | 339 | // Calculate tax 340 | const tax = subtotal.multiply(new Decimal(taxRate)); 341 | const total = subtotal.add(tax); 342 | 343 | return { 344 | subtotal: subtotal.toFixed(2), 345 | tax: tax.toFixed(2), 346 | total: total.toFixed(2), 347 | }; 348 | } 349 | ``` 350 | 351 | Example usage: 352 | 353 | ```javascript 354 | const items = [ 355 | { price: "19.99", quantity: "2" }, 356 | { price: "5.50", quantity: "3" }, 357 | { price: "12.00", quantity: "1" }, 358 | ]; 359 | 360 | const result = calculateInvoice(items, "0.0825"); 361 | console.log(result); 362 | // => { subtotal: "68.48", tax: "5.65", total: "74.13" } 363 | ``` 364 | 365 | ### Currency conversion 366 | 367 | Converting between currencies with proper rounding: 368 | 369 | ```javascript 370 | function convertCurrency(amount, rate, decimals = 2) { 371 | const amountDec = new Decimal(amount); 372 | const rateDec = new Decimal(rate); 373 | const converted = amountDec.multiply(rateDec); 374 | return converted.round(decimals).toString(); 375 | } 376 | ``` 377 | 378 | Example conversions: 379 | 380 | ```javascript 381 | // Convert 100 USD to EUR at rate 0.92545 382 | const eurAmount = convertCurrency("100.00", "0.92545"); 383 | console.log(eurAmount); // => "92.55" 384 | 385 | // Convert with more precision for crypto 386 | const btcAmount = convertCurrency("1000.00", "0.000024", 8); 387 | console.log(btcAmount); // => "0.02400000" 388 | ``` 389 | 390 | ### Percentage calculations 391 | 392 | Working with percentages and discounts: 393 | 394 | ```javascript 395 | function applyDiscount(price, discountPercent) { 396 | const priceDec = new Decimal(price); 397 | const discount = new Decimal(discountPercent).divide(new Decimal(100)); 398 | const discountAmount = priceDec.multiply(discount); 399 | const finalPrice = priceDec.subtract(discountAmount); 400 | 401 | return { 402 | originalPrice: priceDec.toFixed(2), 403 | discountAmount: discountAmount.toFixed(2), 404 | finalPrice: finalPrice.toFixed(2), 405 | savedPercent: discountPercent + "%", 406 | }; 407 | } 408 | 409 | const result = applyDiscount("49.99", "15"); 410 | console.log(result); 411 | // => { originalPrice: "49.99", discountAmount: "7.50", finalPrice: "42.49", savedPercent: "15%" } 412 | ``` 413 | 414 | ### Compound interest calculation 415 | 416 | Calculate compound interest with exact decimal arithmetic. The compound interest formula is: 417 | 418 | 419 | A 420 | = 421 | P 422 | 423 | 424 | ( 425 | 1 426 | + 427 | 428 | r 429 | n 430 | 431 | ) 432 | 433 | 434 | n 435 | t 436 | 437 | 438 | 439 | 440 | Where: 441 | 442 | - A = Final amount 443 | - P = Principal (initial investment) 444 | - r = Annual interest rate (as a decimal) 445 | - n = Number of times interest is compounded per year 446 | - t = Number of years 447 | 448 | ```javascript 449 | function calculateCompoundInterest( 450 | principal, 451 | annualRate, 452 | years, 453 | compoundingPerYear = 12, 454 | ) { 455 | const p = new Decimal(principal); 456 | const r = new Decimal(annualRate).divide(new Decimal(100)); // Convert percentage to decimal 457 | const n = new Decimal(compoundingPerYear); 458 | const t = new Decimal(years); 459 | 460 | const rDivN = r.divide(n); 461 | const onePlusRate = new Decimal(1).add(rDivN); 462 | const exponent = n.multiply(t); 463 | 464 | // For this example, we'll approximate the power operation 465 | // In a real implementation, you'd want a proper power function for Decimal 466 | let result = new Decimal(1); 467 | for (let i = 0; i < exponent.toNumber(); i++) { 468 | result = result.multiply(onePlusRate); 469 | } 470 | 471 | const finalAmount = p.multiply(result); 472 | const interest = finalAmount.subtract(p); 473 | 474 | return { 475 | principal: p.toFixed(2), 476 | finalAmount: finalAmount.toFixed(2), 477 | totalInterest: interest.toFixed(2), 478 | }; 479 | } 480 | ``` 481 | 482 | Let's calculate the growth of a $1,000 investment at 5% annual interest, compounded monthly for 10 years: 483 | 484 | ```javascript 485 | const investment = calculateCompoundInterest("1000", "5", "10"); 486 | console.log(investment); 487 | // => { principal: "1000.00", finalAmount: "1628.89", totalInterest: "628.89" } 488 | ``` 489 | 490 | ## Rounding Strategies 491 | 492 | ### Different rounding modes 493 | 494 | Decimal supports multiple rounding modes for different use cases: ceiling, floor, truncate, round-ties-away-from-zero, and round-ties-to-even (AKA banker's rounding). 495 | 496 | ```javascript 497 | const value = new Decimal("123.456"); 498 | 499 | // Default rounding (halfEven - banker's rounding) 500 | console.log(value.round(2).toString()); // => "123.46" 501 | 502 | // Always round up (ceiling) 503 | console.log(value.round(2, "ceil").toString()); // => "123.46" 504 | 505 | // Always round down (floor) 506 | console.log(value.round(2, "floor").toString()); // => "123.45" 507 | 508 | // Round towards zero (truncate) 509 | console.log(value.round(2, "trunc").toString()); // => "123.45" 510 | 511 | // Round half away from zero 512 | console.log(value.round(2, "halfExpand").toString()); // => "123.46" 513 | 514 | // Negative number example 515 | const negative = new Decimal("-123.456"); 516 | console.log(negative.round(2, "floor").toString()); // => "-123.46" 517 | console.log(negative.round(2, "ceil").toString()); // => "-123.45" 518 | ``` 519 | 520 | ### Financial rounding 521 | 522 | Common rounding patterns for financial applications. 523 | 524 | #### Round to nearest cent 525 | 526 | ```javascript 527 | function roundToCents(amount) { 528 | return new Decimal(amount).round(2); 529 | } 530 | 531 | console.log(roundToCents("19.9749").toString()); // => "19.97" 532 | console.log(roundToCents("19.9751").toString()); // => "19.98" 533 | ``` 534 | 535 | #### Round to nearest nickel (0.05) 536 | 537 | ```javascript 538 | function roundToNickel(amount) { 539 | const decimal = new Decimal(amount); 540 | const twentieths = decimal.multiply(new Decimal(20)); 541 | const rounded = twentieths.round(); 542 | return rounded.divide(new Decimal(20)); 543 | } 544 | 545 | console.log(roundToNickel("19.97").toString()); // => "19.95" 546 | console.log(roundToNickel("19.98").toString()); // => "20" 547 | ``` 548 | 549 | ## Comparisons and Sorting 550 | 551 | Are two Decimal values equal? Is one less than another? 552 | 553 | ### Comparing decimal values 554 | 555 | Safe comparison of decimal values: 556 | 557 | ```javascript 558 | const a = new Decimal("0.1").add(new Decimal("0.2")); 559 | const b = new Decimal("0.3"); 560 | 561 | // Direct comparison 562 | console.log(a.equals(b)); // => true 563 | console.log(a.lessThan(b)); // => false 564 | console.log(a.greaterThanOrEqual(b)); // => true 565 | ``` 566 | 567 | Now let's sort: 568 | 569 | ```javascript 570 | const values = [ 571 | new Decimal("10.5"), 572 | new Decimal("2.3"), 573 | new Decimal("10.05"), 574 | new Decimal("-5"), 575 | ]; 576 | 577 | // Sort ascending 578 | values.sort((a, b) => a.compare(b)); 579 | console.log(values.map((v) => v.toString())); 580 | // => ["-5", "2.3", "10.05", "10.5"] 581 | 582 | // Sort descending 583 | values.sort((a, b) => b.compare(a)); 584 | console.log(values.map((v) => v.toString())); 585 | // => ["10.5", "10.05", "2.3", "-5"] 586 | ``` 587 | 588 | ### Finding min/max values 589 | 590 | Working with arrays of decimal values: 591 | 592 | ```javascript 593 | function findMinMax(values) { 594 | const decimals = values.map((v) => new Decimal(v)); 595 | 596 | const min = decimals.reduce((min, current) => 597 | current.lessThan(min) ? current : min, 598 | ); 599 | 600 | const max = decimals.reduce((max, current) => 601 | current.greaterThan(max) ? current : max, 602 | ); 603 | 604 | return { min: min.toString(), max: max.toString() }; 605 | } 606 | 607 | const prices = ["19.99", "5.50", "105.00", "0.99", "50.00"]; 608 | console.log(findMinMax(prices)); 609 | // => { min: "0.99", max: "105.00" } 610 | ``` 611 | -------------------------------------------------------------------------------- /decimal128-reference.md: -------------------------------------------------------------------------------- 1 | # Decimal128 introduction and reference 2 | 3 | This page is JS developer-oriented documentation for using `Decimal128`, a TC39 proposal for JavaScript. It's 4 | a work in progress--PRs welcome! For a broader introduction and rationale to the project, see 5 | [README.md](./README.md). 6 | 7 | ## Introductory example 8 | 9 | `Decimal128` is a new numerical type proposed for JavaScript which can be used to represent decimal 10 | quantities, like money. Since `Number` is represented as binary float-point type, there are decimal numbers 11 | that can't be represented by them, causing problems on rounding that happens on arithmetic operations like 12 | `+`. 13 | 14 | ```js 15 | let a = 0.1 * 8; 16 | // 0.8000000000000000444089209850062616169452667236328125 17 | let b = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1; 18 | // 0.79999999999999993338661852249060757458209991455078125 19 | 20 | a === b; // evaluates to false 21 | ``` 22 | 23 | The example above illustrate why using binary floating-point numbers is problematic when we use them to 24 | represent and manipulate decimal fractions. The expectation on both arithmetic operations is that the final 25 | result is 0.8, and they also should be equivalent. However, since the result for some of those operations 26 | can't be exactly represented by binary floating-point numbers, the results diverge. For this example, the 27 | reason for such difference on results comes from the fact that multiple additions using binary floating-point 28 | numbers will carry more errors from rounding than a single multiplication. It's possible to see more examples 29 | of issues like that on this [Decimal FAQ section](http://speleotrove.com/decimal/decifaq1.html#inexact). Such 30 | issue isn't a problem with Decimal128, because we are able to represent all those decimal fractions exactly, 31 | including the intermediate results for arithmetic operations. 32 | 33 | ```js 34 | let a = 0.1m * 8m; 35 | // This results in 0.8m exactly 36 | let b = 0.1m + 0.1m + 0.1m + 0.1m + 0.1m + 0.1m + 0.1m + 0.1m; 37 | // This also results on 0.8m 38 | 39 | a === b; // evaluates to true 40 | ``` 41 | 42 | Now, let's take the example of a function to add up a bill with a number of items, and add sales tax: 43 | 44 | ```js 45 | function calculateBill(items, tax) { 46 | let total = 0m; 47 | for (let {price, count} of items) { 48 | total += price * Decimal128(count); 49 | } 50 | return Decimal128.round(total * (1m + tax), 51 | {maximumFractionDigits: 2, round: "up"}); 52 | } 53 | 54 | let items = [{price: 1.25m, count: 5}, {price: 5m, count: 1}]; 55 | let tax = .0735m; 56 | console.log(calculateBill(items, tax)); 57 | ``` 58 | 59 | Here, you can see several elements of `Decimal128` at work: 60 | 61 | - Create a `Decimal128` as a literal, e.g., `1.25m`, or convert one from another type, as `Decimal128(value)`. 62 | - Add and multiply `Decimal128`s with the `+` and `*` operators. 63 | - Round decimals with `Decimal128.round`, based on an options bag describing how to round. 64 | 65 | This article describes how these work in more detail, and the rest of the `Decimal128` API. 66 | 67 | ## What does Decimal128 represent? 68 | 69 | `Decimal128` represents a base-10 decimal number, internally represented as an IEEE 754 128-bit decimal. Its 70 | precision is defined by 34 decimal digits of significand and an exponent range from -6143 to +6144. The value 71 | of this type is calculated as follow `s * ( * 10 ** )`, where `s` represents the sign 72 | of the number and can be either `1` or `-1`. 73 | 74 | [IEEE 754 128-bit decimal](https://en.wikipedia.org/wiki/Decimal128_floating-point_format) allows represent 75 | different precisions of the same value considering trailing zeros (i.e it is possible to represent both 76 | `2.10000` and `2.1`), however Decimal128 values do not represent precision apart form its value. It means that 77 | two Decimal128 values are equal when they represent the same mathematical value (e.g. `2.10000m` is the same 78 | number as `2.1m` with the very same precision). 79 | 80 | Check [Decimal FAQ](http://speleotrove.com/decimal/decifaq.html) to learn more details about decimal 81 | arithmetics and representation. 82 | 83 | ## Creating Decimal128 values 84 | 85 | There are 2 main ways to create new Decimal128 values. We can use **literals** or **constructors**. 86 | 87 | **Literals** 88 | 89 | Literals can be declared using `m` suffix. It is possible to write a Decimal128 literal using scientific 90 | notation. Check example bellow. 91 | 92 | ```js 93 | let a = 0.123m 94 | let b = 456m; 95 | let c = -1e-10m; // scientific notation 96 | ``` 97 | 98 | **Constructor** 99 | 100 | The constructor `Decimal128` is available to be used as a coercing tool. 101 | 102 | ```js 103 | let a = Decimal128(3); // returns 3m 104 | let b = Decimal128("345"); // returns 345m 105 | let c = Decimal128("115e-10"); // returns 0.0000000115m 106 | let d = Decimal128(2545562323242232323n); // results 2545562323242232323m 107 | let e = Decimal128(true); // returns 1m 108 | let f = Decimal128(false); // returns 0m 109 | let g = Decimal128(null); // Throws TypeError 110 | let h = Decimal128(undefined); // Throws TypeError 111 | let i = Decimal128(0.1); // returns 0.1000000000000000055511151231257827021181583404541015625m 112 | ``` 113 | 114 | ## Operators on Decimal128 115 | 116 | Decimal128 primitives works with JS operators just like Numbers and BigInt. During the section we will 117 | describe the semantics of each operator, providing some examples. 118 | 119 | ### Unary `-` operators 120 | 121 | This operation changes the sign of the Decimal128 value. It can be applied to a literal or to a variable: 122 | 123 | ```js 124 | let a = -5m; 125 | console.log(-a === 5m); // prints true 126 | ``` 127 | 128 | ### Unary `+` operator 129 | 130 | This operation throws `TypeError` on `Decimal128`. 131 | 132 | ### Binary operators 133 | 134 | Decimal128 is supported on JS binary arithmetic and comparison operations and with logical operations as well. 135 | 136 | #### `+` operator 137 | 138 | This results in a Decimal128 value that represents the addition of `lhs` and `rhs` operands. 139 | 140 | ```js 141 | let sum = 0.2m + 0.1m; 142 | console.log(sum); // prints 0.3m 143 | ``` 144 | 145 | We also can mix a Decimal128 value with a String. The result is the concatenation of the String with a string 146 | representing the value of Decimal128. 147 | 148 | ```js 149 | let concat = 0.44m + "abc"; 150 | console.log(concat); // prints 0.44abc 151 | ``` 152 | 153 | #### `-` operator 154 | 155 | This results in a Decimal128 value that represents the difference of `rhs` and `lhs` operands. 156 | 157 | ```js 158 | let diff = 15.5m - 10m; 159 | console.log(diff); // prints 5.5m 160 | ``` 161 | 162 | #### `*` operator 163 | 164 | This results in a Decimal128 value that represents the product of `rhs` and `lhs` operands. 165 | 166 | ```js 167 | let prod = 0.5m * 2m; 168 | console.log(prod); // prints 1m 169 | ``` 170 | 171 | #### `/` operator 172 | 173 | This results in a Decimal128 value that represents the division of `rhs` and `lhs` operands. 174 | 175 | ```js 176 | let division = 3m / 2m; 177 | console.log(division); // prints 1.5m 178 | ``` 179 | 180 | #### `%` operator 181 | 182 | This results in a Decimal128 value that represents the modulos of `rhs` and `lhs` operands. 183 | 184 | ```js 185 | let mod = 9.5m % 2m; 186 | console.log(mod); // prints 1.5m 187 | 188 | mod = 9m % 2m; 189 | console.log(mod); // prints 1m 190 | ``` 191 | 192 | #### `**` operator 193 | 194 | This operator is not supported on `Decimal128`. 195 | 196 | #### Arithmetic operations of Decimal128 and other primitive types 197 | 198 | With the exception of addition operator `+`, mixing Decimal128 and other primitive types results in a 199 | `TypeError` (see [issue #39](https://github.com/tc39/proposal-decimal/issues/39) for reasoning behind this 200 | design decision). 201 | 202 | ```js 203 | let sum = 0.5m + 33.4; // throws TypeError 204 | let diff = 334m - 1n; // throws TypeError 205 | let prod = 234.6m * 1.5; // throws TypeError 206 | let div = 30m / 15n; // throws TypeError 207 | let mod = 35m % 5n; // throws TypeError 208 | ``` 209 | 210 | #### Rounding on arithmetic operations 211 | 212 | It is important to notice that Decimal128 precision is limited to 34 digits and every arithmetic operations 213 | like addition or multiplication can cause rounding towards that precision. The rounding algorithm used on 214 | Decimal128 arithmetics is the `half even` where it rounds towards the "nearest neighbor". If both neighbors 215 | are equidistant, it rounds towards the even neighbor. It is listed bellow examples of rounding for each 216 | operation: 217 | 218 | ```js 219 | let sumRounded = 1e35m + 1m; 220 | print(sumRounded); // prints 1e35 221 | 222 | let subRounded = -1e35m - 1m; 223 | print(subRounded); // prints -1e35 224 | 225 | let mulRounded = 1m * 1e-400m; 226 | print(mulRounded); // prints 0 227 | 228 | let mulRounded = 1m / 1e400m; 229 | print(mulRounded); // prints 0 230 | ``` 231 | 232 | Operations that can cause roundings are the ones where the precision of result is greater than 34. 233 | 234 | ### Comparison Operators 235 | 236 | `Decimal128` is also allowed to be used with comparison operators. In this case, since we are able to compare 237 | values between a `Decimal128` and other numeric types without any precision issue, this is also supported. 238 | 239 | #### `>` operator 240 | 241 | It is possible to compare Decimal128 values using `>` operator. It returns `true` if the value of `lhs` is 242 | greater than the value of `rhs` and `false` otherwise. 243 | 244 | ```js 245 | let greater = 0.5m > 0m; 246 | console.log(greater); // prints true 247 | 248 | let notGreater = 0.5m > 2.0m; 249 | console.log(notGreater); // prints false 250 | ``` 251 | 252 | #### `<` operator 253 | 254 | It is possible to compare Decimal128 values using `<` operator. It returns `true` if the value of `lhs` is 255 | lesser than the value of `rhs` and `false` otherwise. 256 | 257 | ```js 258 | let lesser = 0.5m < 2m; 259 | console.log(lesser); // prints true 260 | 261 | let notLesser = 0m < 0.5m; 262 | console.log(notLesser); // prints false 263 | ``` 264 | 265 | #### `>=` operator 266 | 267 | It is possible to compare Decimal128 values using `>=` operator. It returns `true` if the value of `lhs` is 268 | greater or equal than the value of `rhs` and `false` otherwise. 269 | 270 | ```js 271 | let greaterOrEqual = 0.5m >= 0.5m; 272 | console.log(greaterOrEqual); // prints true 273 | ``` 274 | 275 | #### `<=` operator 276 | 277 | It is possible to compare Decimal128 values using `<=` operator. It returns `true` if the value of `lhs` is 278 | lesser or equal than the value of `rhs` and `false` otherwise. 279 | 280 | ```js 281 | let lesserOrEqual = 0.5m <= 0.4m; 282 | console.log(lesserOrEqual); // prints false 283 | ``` 284 | 285 | #### `==` operator 286 | 287 | The equal operator can also compare Decimal128 values. It returns true if `lhs` has the same mathematical 288 | value of `rhs`. 289 | 290 | ```js 291 | let isEqual = 0.2m + 0.1m == 0.3m; 292 | console.log(isEqual); // prints true 293 | ``` 294 | 295 | #### `===` operator 296 | 297 | It is also possible to apply strict equals operator on Decimal128 value. 298 | 299 | ```js 300 | let isEqual = 0.2m + 0.1m === 0.3m; 301 | console.log(isEqual); // prints true 302 | 303 | let isNotEqual = 0.2 + 0.1 === 0.3m; 304 | console.log(isNotEqual); // prints false 305 | ``` 306 | 307 | #### Comparing Decimal128 with other primitive types 308 | 309 | Since comparison operators uses mathematical value of operands, it is possible to compare Decimal128 other 310 | types like Numbers, BigInt or Strings. 311 | 312 | ```js 313 | 567.00000000000001m < 567n; // false 314 | 998m == 998; // true 315 | 703.04 >= 703.0400001m; // false 316 | 9m <= "9"; // true 317 | 654m === 654.000m; // true 318 | 654m === 654; // false 319 | 0m > -1; // true 320 | ``` 321 | 322 | ### typeof 323 | 324 | The `typeof` operator returns `"decimal128"` when applied to a Decimal128 value. 325 | 326 | ```js 327 | let v = 0.5m 328 | console.log(typeof v); // prints decimal128 329 | ``` 330 | 331 | ### Falsiness 332 | 333 | When used into boolean operator like `&&`, `||` or `??`, a Decimal128 value is considered as `false` if it is 334 | `0m` or `true` otherwise. 335 | 336 | ```js 337 | if (0m) { 338 | console.log("hello"); // this is never executed 339 | } 340 | 341 | if (1m || 0) { 342 | console.log("world"); // prints world 343 | } 344 | 345 | let a = 1m && 0; 346 | console.log(a); // false 347 | 348 | let b = 0m || false; 349 | console.log(b); // false 350 | 351 | let c = 15m ?? 'hello world'; 352 | console.log(c); // 15m 353 | ``` 354 | 355 | ### Bitwise operators 356 | 357 | If a Decimal128 is an operand of a bitwise operator, it results in a `TypeError`. 358 | 359 | ## Standard library functions on Decimal128 360 | 361 | ### `Decimal128.round(value [, options])` 362 | 363 | This is the function to be used when there's need to round Decimal128 in some specific way. It rounds the 364 | Decimal128 passed as parameter, tanking in consideration `options`. 365 | 366 | - `value`: A Decimal128 value. If the value is from another type, it throws `TypeError`. 367 | - `options`: It is an object indicating how the round operation should be performed. It is an object that can 368 | contain `roundingMode` and `maximumFractionDigits` properties. 369 | - `maximumFractionDigits`: This options indicates the maximum of fractional digits the rounding operation 370 | should preserve. 371 | - `roundingMode`: This option indicates which algorithm is used to round a given Decimal128. Each possible 372 | option is described below. 373 | - `down`: round towards zero. 374 | - `half down`: round towards "nearest neighbor". If both neighbors are equidistant, it rounds down. 375 | - `half up`: round towards "nearest neighbor". If both neighbors are equidistant, it rounds up. 376 | - `half even`: round towards the "nearest neighbor". If both neighbors are equidistant, it rounds towards 377 | the even neighbor. 378 | - `up`: round away from zero. 379 | 380 | ```js 381 | let a = Decimal128.round(0.53m, {roundingMode: 'half up', maximumFractionDigits: 1}); 382 | assert(a, 0.5m); 383 | 384 | a = Decimal128.round(0.53m, {roundingMode: 'half down', maximumFractionDigits: 1}); 385 | assert(a, 0.5m); 386 | 387 | a = Decimal128.round(0.53m, {roundingMode: 'half even', maximumFractionDigits: 1}); 388 | assert(a, 0.5m); 389 | 390 | a = Decimal128.round(0.31m, {roundingMode: 'down', maximumFractionDigits: 1}); 391 | assert(a, 0.3m); 392 | 393 | a = Decimal128.round(0.31m, {roundingMode: 'up', maximumFractionDigits: 1}); 394 | assert(a, 0.4m); 395 | ``` 396 | 397 | ### Decimal128.add(lhs, rhs [, options]) 398 | 399 | This function can be used as an alternative to `+` binary operator that allows rounding the result after the 400 | calculation. It adds `rhs` and `lhs` and returns the result of such operation, applying the rounding rules 401 | based on `options` object, if given. `options` is an options bag that configures a custom rounding for this 402 | operation. 403 | 404 | - `lhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 405 | - `rhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 406 | - `options`: It is an object indicating how the round operation should be performed. It's the same options bag 407 | object described on [Decimal128.round](#decimal128roundvalue--options). If it's not given, the operation 408 | will use the same rounding rules of `+` binary operator described on [Rounding on arithmetic 409 | operations](#rounding-on-arithmetic-operations) section. 410 | 411 | ### Decimal128.subtract(lhs, rhs [, options]) 412 | 413 | This function can be used as an alternative to `-` binary operator that allows rounding the result after the 414 | calculation. It subtracts `rhs` from `lhs` and returns the result of such operation, applying the rounding 415 | rules based on `options` object, if given. `options` is an options bag that configures a custom rounding for 416 | this operation. 417 | 418 | - `lhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 419 | - `rhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 420 | - `options`: It is an object indicating how the round operation should be performed. It's the same options bag 421 | object described on [Decimal128.round](#decimal128roundvalue--options). If it's not given, the operation 422 | will use the same rounding rules of `-` binary operator described on [Rounding on arithmetic 423 | operations](#rounding-on-arithmetic-operations) section. 424 | 425 | ### Decimal128.multiply(lhs, rhs [, options]) 426 | 427 | This function can be used as an alternative to `*` binary operator that allows rounding the result after the 428 | calculation. It multiplies `rhs` by `lhs` and returns the result of such operation applying the rounding rules 429 | based on `options` object, if given. `options` is an options bag that configures a custom rounding for this 430 | operation. 431 | 432 | - `lhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 433 | - `rhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 434 | - `options`: It is an object indicating how the round operation should be performed. It's the same options bag 435 | object described on [Decimal128.round](#decimal128roundvalue--options). If it's not given, the operation 436 | will use the same rounding rules of `*` binary operator described on [Rounding on arithmetic 437 | operations](#rounding-on-arithmetic-operations) section. 438 | 439 | ### Decimal128.divide(lhs, rhs [, options]) 440 | 441 | This function can be used as an alternative to `/` binary operator that allows rounding the result after the 442 | calculation. It divides `lhs` by `rhs` and returns the result of such operation applying the rounding based on 443 | `options` object, if given. `options` is an options bag that configures a custom rounding for this operation. 444 | 445 | - `lhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 446 | - `rhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 447 | - `options`: It is an object indicating how the round operation should be performed. It's the same options bag 448 | object described on [Decimal128.round](#decimal128roundvalue--options). If it's not given, the operation 449 | will use the same rounding rules of `/` binary operator described on [Rounding on arithmetic 450 | operations](#rounding-on-arithmetic-operations) section. 451 | 452 | ### Decimal128.remainder(lhs, rhs [, options]) 453 | 454 | This function can be used as an alternative to `%` binary operator that allows rounding the result after the 455 | calculation. It returns the reminder of dividing `lhs` by `rhs`, applying the rounding based on `options` 456 | object, if given. `options` is an options bag that configures a custom rounding for this operation. 457 | 458 | - `lhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 459 | - `rhs`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 460 | - `options`: It is an object indicating how the round operation should be performed. It's the same options bag 461 | object described on [Decimal128.round](#decimal128roundvalue--options). If it's not given, the operation 462 | will use the same rounding rules of `%` binary operator described on [Rounding on arithmetic 463 | operations](#rounding-on-arithmetic-operations) section. 464 | 465 | ### Decimal128.pow(number, power [, options]) 466 | 467 | This function returns the power of `number` by `power`, applying the rounding based on `options` object, if 468 | given. `options` is an options bag that configures the rounding of this operation. `power` needs to be a 469 | positive integer. 470 | 471 | - `number`: A `Decimal128` value. If the value is from another type, it throws `TypeError`. 472 | - `power`: A positive integer `Number` value. If the value is from another type or not a positive integer, it 473 | throws `RangeError`. 474 | - `options`: It is an object indicating how the round operation should be performed. It's the same options bag 475 | object described on [BigDecimal.round](#bigdecimalroundvalue--options). If it's not given, no rounding 476 | operation will be applied, and the exact result will be returned. 477 | 478 | ## Decimal128 prototype 479 | 480 | There is a `Decimal128.prototype` that includes utility methods. 481 | 482 | ### `Decimal128.prototype.toString()` 483 | 484 | This method returns a string that is the representation of Decimal128 value. 485 | 486 | ```js 487 | let v = 0.55m; 488 | console.log(v.toString()); // prints "0.55" 489 | ``` 490 | 491 | ### `Decimal128.prototype.toLocaleString(locale [, options])` 492 | 493 | This method returns a string that is the locale sensitive representation of Decimal128 value. We get the same 494 | output of applying `locale` and `options` to `NumberFormat` on environments that supports Intl API. 495 | 496 | ```js 497 | let v = 1500.55m; 498 | console.log(v.toLocaleString("en")); // prints "1,500.55" 499 | console.log(v.toLocaleString("pt-BR")); // prints "1.500,55" 500 | ``` 501 | 502 | ### `Decimal128.prototype.toFixed([digits])` 503 | 504 | This function returns a string that represents fixed-point notation of Decimal128 value. There is an optional 505 | parameter digits that defines the number of digits after decimal point. It follows the same semantics of 506 | `Number.prototype.toFixed`. 507 | 508 | ```js 509 | let v = 100.456m; 510 | console.log(v.toFixed(2)); // prints 100.46 511 | v = 0m; 512 | console.log(v.toFixed(2)); // prints 0.00 513 | ``` 514 | 515 | ### `Decimal128.prototype.toExponential([fractionDigits])` 516 | 517 | This methods returns a string of Decimal128 in exponential representation. It takes an optional parameter 518 | `fractionDigits` that defines the number of digits after decimal point. It follows the same semantics of 519 | `Number.prototype.toExponential`. 520 | 521 | ```js 522 | let v = 1010m; 523 | console.log(v.toExponential(2)); // prints 1.01e+3 524 | ``` 525 | 526 | ### `Decimal128.prototype.toPrecision([precision])` 527 | 528 | This function returns a string that represents the Decimal128 in the specified precision. It follows the same 529 | semantics of `Number.prototype.toPrecision`. 530 | 531 | ```js 532 | let v = 111.22m; 533 | console.log(v.toPrecision()); // prints 111.22 534 | console.log(v.toPrecision(4)); // 111.2 535 | console.log(v.toPrecision(2)); //1.1e+2 536 | ``` 537 | 538 | ### Decimal128 and Intl.NumberFormat support 539 | 540 | [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) 541 | also supports `Decimal128` values, just like it already supports Numbers and BigInts. 542 | 543 | ```js 544 | const number = 123456.789m; 545 | 546 | console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(number)); 547 | // expected output: "123.456,79 €" 548 | 549 | // the Japanese yen doesn't use a minor unit 550 | console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(number)); 551 | // expected output: "¥123,457" 552 | 553 | // limit to three significant digits 554 | console.log(new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(number)); 555 | // expected output: "1,23,000" 556 | ``` 557 | 558 | ## Decimal128 as key for Map/Set 559 | 560 | Like other primitives we have, it's also possible to use Decimal128 values as keys for Maps and Sets. 561 | 562 | ```js 563 | let s = new Set(); 564 | let decimal = 3.55m; 565 | 566 | s.add(decimal); 567 | s.has(3.55m); // returns true 568 | s.has(3.55); // returns false 569 | s.has("3.55"); // returns false 570 | 571 | // Map 572 | let m = new Map(); 573 | 574 | m.set(decimal, "test"); 575 | m.get(3.55m); // returns "test" 576 | m.get(3.55); // returns undefined 577 | m.get("3.55"); // returns undefined 578 | ``` 579 | 580 | ### Decimal128 as property keys 581 | 582 | Decimal128 values can be used as property keys, like we also have support it for BigInts and Numbers. Its 583 | value is converted to a String to be used as a property key. 584 | 585 | ```js 586 | let o = {}; 587 | o[2.45m] = "decimal"; 588 | console.log(o["2.45"]); // prints "decimal" 589 | console.log(o[2.45m]); // prints "decimal" 590 | ``` 591 | 592 | ## TypedArrays 593 | 594 | TODO 595 | 596 | ## Using Decimal128 today 597 | 598 | It's not possible to use Decimal128 today, as the polyfill is not yet implemented. 599 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecma TC39 JavaScript Decimal proposal 2 | 3 | The TC39 Decimal proposal aims to add functionality to JavaScript to represent base-10 decimal numbers. 4 | 5 | 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).** 6 | 7 | **Champions**: 8 | 9 | - Jesse Alama (Igalia) 10 | - Jirka Maršík (Oracle) 11 | - Andrew Paprocki (Bloomberg) 12 | 13 | **Authors**: Jesse Alama, Waldemar Horwat 14 | 15 | **Stage**: Stage 1 of [the TC39 process](https://tc39.github.io/process-document/). A [draft specification](http://tc39.es/proposal-decimal/) is available. 16 | 17 | ## Use cases and goals 18 | 19 | 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 20 | errors. 21 | 22 | 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? 23 | 24 | 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). 25 | 26 | 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. 27 | 28 | ### Primary use case: Representing human-readable decimal values such as money 29 | 30 | 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: 31 | 32 | - 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. 33 | - 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. 36 | - Sometimes, fractional cents need to be represented too (e.g., as precise prices that occur, for instance, in stock trading or currency conversion). 37 | 38 | #### Sample code 39 | 40 | In the examples that follow, we'll use `Decimal` objects. 41 | 42 | ##### Add up the items of a bill, then add sales tax 43 | 44 | ```js 45 | function calculateBill(items, tax) { 46 | let total = new Decimal(0); 47 | for (let { price, count } of items) { 48 | total = total.add(new Decimal(price).times(new Decimal(count))); 49 | } 50 | return total.multiply(tax.add(new Decimal(1))); 51 | } 52 | 53 | let items = [ 54 | { price: "1.25", count: 5 }, 55 | { price: "5.00", count: 1 }, 56 | ]; 57 | let tax = new Decimal("0.0735"); 58 | let total = calculateBill(items, tax); 59 | console.log(total.toFixed(2)); 60 | ``` 61 | 62 | ##### Currency conversion 63 | 64 | Let's convert USD to EUR, given the exchange rate EUR --> USD. 65 | 66 | ```js 67 | let exchangeRateEurToUsd = new Decimal("1.09"); 68 | let amountInUsd = new Decimal("450.27"); 69 | let exchangeRateUsdToEur = new Decimal("1").divide(exchangeRateEurToUsd); 70 | 71 | let amountInEur = exchangeRateUsdToEur.multiply(amountInUsd); 72 | console.log(amountInEur.round(2).toString()); 73 | ``` 74 | 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 | 99 | #### Why use JavaScript for this case? 100 | 101 | 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: 102 | 103 | - **More complicated frontend architectures**: Rounding, localization or other presentational aspects may be performed on the frontend for better interactive performance. 104 | - **Serverless**: Many Serverless systems use JavaScript as a programming language in order to better leverage the knowledge of frontend engineers. 105 | - **Server-side programming in JavaScript**: Systems like Node.js and Deno have grown in popularity to do more traditional server-side programming in JavaScript. 106 | 107 | In all of these environments, the lack of decimal number support means that various workarounds have to be used (assuming, again, that programmers are even aware of the inherent mismatch between JS’s built-in binary floating-point numbers and proper decimal numbers): 108 | 109 | - An external library could be used instead (introducing issues about choosing the library, coordinating on its use). 110 | - Calculations could be in terms of “cents” (fallible, as explained above) 111 | - 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 112 | 113 | 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. 114 | 115 | #### Goals implied by the main use cases 116 | 117 | This use case implies the following goals: 118 | 119 | - Avoid unintentional rounding that causes user-visible errors 120 | - Basic mathematical functions such as addition, subtraction, multiplication, and division 121 | - 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 123 | - Sufficient ergonomics to enable correct usage 124 | - Be implementable with adequate performance/memory usage for applications 125 | - (Please file an issue to mention more requirements) 126 | 127 | ### Secondary use case: Data exchange 128 | 129 | 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. 130 | 131 | #### Why use JavaScript for this case? 132 | 133 | 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. 134 | 135 | #### Goals implied by the use case 136 | 137 | This use case implies the following goals: 138 | 139 | - Basic mathematical functions such as `+`, `-`, `*` should be available 140 | - Sufficient precision for these applications (unclear how high--would require more analysis of applications) 141 | - Be implementable with adequate performance/memory usage for applications 142 | - -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 143 | - (Please file an issue to mention more requirements) 144 | 145 | Interaction with other systems brings the following requirements: 146 | 147 | - Ability to round-trip decimal quantities from other systems 148 | - Serialization and deserialization in standard decimal formats, e.g., IEEE 754’s multiple formats 149 | - Precision sufficient for the applications on the other side 150 | 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 | 153 | #### Sample code 154 | 155 | ##### Configure a database adapter to use JS-native decimals 156 | 157 | 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: 158 | 159 | ```js 160 | const { Client } = require("pg"); 161 | 162 | const client = new Client({ 163 | user: "username", 164 | sql_decimal: "decimal", // or 'string', 'number' 165 | // ...more options 166 | }); 167 | 168 | const boost = new Decimal("1.05"); 169 | 170 | client.query("SELECT prices FROM data_with_numbers", (err, res) => { 171 | if (err) throw err; 172 | console.log(res.rows.map((row) => row.prices.times(boost))); 173 | client.end(); 174 | }); 175 | ``` 176 | 177 | ### Tertiary use case: Numerical calculations on more precise floats 178 | 179 | 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. 180 | 181 | 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. 182 | 183 | ### Language design goals 184 | 185 | In addition to the goals which come directly from use cases mentioned above: 186 | 187 | - Well-defined semantics, with the same result regardless of which implementation and context a piece of code is run in 188 | - Build a consistent story for numerics in JavaScript together with Numbers, BigInt, operator overloading, and 189 | potential future built-in numeric types 190 | - No global mutable state involved in operator semantics; dynamically scoped state also discouraged 191 | - Ability to be implemented across all JavaScript environment (e.g., embedded, server, browser) 192 | 193 | ### Interactions with other parts of the web platform 194 | 195 | If Decimal becomes a part of standard JavaScript, it may be used in some built-in APIs in host environments: 196 | 197 | - For the Web platform: 198 | ([#4](https://github.com/tc39/proposal-decimal/issues/4)) 199 | - HTML serialization would support Decimal, just as it supports BigInt, so Decimal could be used in `postMessage`, `IndexedDB`, etc. 200 | - For WebAssembly, if WebAssembly adds IEEE 64-bit and/or 128-bit decimal scalar types some day, then the WebAssembly/JS API could introduce conversions along the boundary, analogous to [WebAssembly BigInt/i64 integration](https://github.com/WebAssembly/JS-BigInt-integration) 201 | 202 | More host API interactions are discussed in [#5](https://github.com/tc39/proposal-decimal/issues/5). 203 | 204 | ## Prior Art 205 | 206 | 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. 207 | 208 | ### Decimal Support Across Languages 209 | 210 | | Language | Type Name | Location | Year Added | Notes | 211 | |----------|-----------|----------|------------|-------| 212 | | 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/) | 213 | | 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 | 214 | | 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 | 215 | | 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 | 216 | | 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 | 217 | 218 | SQL also has `NUMERIC`/`DECIMAL` as a built-in type, predating many programming languages. 219 | 220 | 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.) 221 | 222 | 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. 223 | 224 | ### Why JavaScript Lacks Decimal Support 225 | 226 | 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. 227 | 228 | ### The Cost of Not Having Decimal 229 | 230 | Because JavaScript lacks native decimal support, developers have created numerous userland solutions: 231 | 232 | - [**decimal.js**](https://www.npmjs.com/package/decimal.js): ~2M weekly npm downloads 233 | - [**bignumber.js**](https://www.npmjs.com/package/bignumber.js): ~800K weekly npm downloads 234 | - [**big.js**](https://www.npmjs.com/package/big.js): ~500K weekly npm downloads 235 | 236 | 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. 237 | 238 | ## Specification and standards 239 | 240 | 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). 241 | 242 | 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. 243 | 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 | 246 | ### Known alternatives 247 | 248 | #### Unlimited precision decimals (AKA "BigDecimal") 249 | 250 | The "BigDecimal" data model is based on unlimited-size decimals (no fixed bith-width), understood exactly as mathematical values. 251 | 252 | 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. 253 | 254 | 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. 255 | 256 | One can conceive of an arbitrary-precision version of decimals, and we have explored that route; historical information is available at [bigdecimal-reference.md](./bigdecimal-reference.md). 257 | 258 | One difficulty with BigDecimal is that division is not available as a two-argument function because a rounding parameter is, in general, required. A `BigDecimal.div` function would be needed, where some options would be mandatory. See [#13](https://github.com/tc39/proposal-decimal/issues/13) for further discussion of division in BigDecimal. 259 | 260 | Further discussion of the tradeoffs between BigDecimal and Decimal128 can be found in [#8](https://github.com/tc39/proposal-decimal/issues/8). 261 | 262 | #### Fixed-precision decimals 263 | 264 | Imagine that every decimal number has, say, ten digits after the decimal point. Anything requiring, say, eleven digits after the decimal point would be unrepresentable. This is the world of fixed-precision decimals. The number ten is just an example; some research would be required to find out what a good number would be. One could even imagine that the precision of such numbers could be parameterized. 265 | 266 | #### Rational numbers 267 | 268 | Rational numbers, AKA fractions, offer an adjacent approach to decimals. From a mathematical point of view, rationals are more expressive than decimals: every decimal is a kind of fraction (a signed integer divided by a power of ten), whereas some rationals, such as 1/3, cannot be (finitely) represented as decimals. So why not rationals? 269 | 270 | - The size of the numerators and denominators, in general, grows exponentially as one carries out operations. Performing just one multiplication or division will in general cause the size of the parts of the rational to be multiplied. Even addition and subtraction cause rapid growth. This means that a heavy cost is paid for the precision offered by rationals. 271 | - One must be vigilant about normalization of numerators and denominators, which involves repeatedly computing GCDs, dividing numerator and denominator by them, and continuing. The alternative to this is to not canonicalize rationals, canonicalize after, say, every five arithmetical operations, and so on. This can be an expensive operation, certainly much more expensive than, say, normalizing "1.20" to "1.2". 272 | - Various operations, such as exponentiation and logarithm, almost never produce rational numbers given a rational argument, so one would have to specify a certain amount of precision as a second argument to these operations. By contrast, in, say, Decimal128, these operations do not require a second argument. 273 | 274 | Fractions would be an interesting thing to pursue in TC39, and are in many ways complementary to Decimal. The use cases for rationals overlap somewhat with the use cases for decimals. Many languages in the Lisp tradition (e.g., [Racket](https://docs.racket-lang.org/guide/numbers.html)) include rationals as a basic data type, alongside IEEE 754 64-bit binary floating point numbers; Ruby and Python also include fractions in their standard library. 275 | 276 | We see rationals as complementary to Decimal because of a mismatch when it comes to two of the core operations on Decimals: 277 | 278 | - Rounding to a certain base-10 precision, with a rounding mode 279 | - Conversion to a localized, human-readable string 280 | 281 | These _could_ be defined on rationals, but are a bit of an inherent mismatch since rationals are not base 10. 282 | 283 | Rational may still make sense as a separate data type, alongside Decimal. Further discussion of rationals in [#6](https://github.com/tc39/proposal-decimal/issues/6). 284 | 285 | ## Syntax and semantics 286 | 287 | 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. 288 | 289 | ### Data model 290 | 291 | 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: 292 | 293 | - 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. 294 | - positive and negative infinity will be available, though, as with `NaN`, they are distinct from JS's built-in `Infinity` and `-Infinity`. 295 | 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`. 299 | 300 | ### Operator semantics 301 | 302 | - Arithmetic 303 | - Unary operations 304 | - Absolute value 305 | - Negation 306 | - Binary operations 307 | - Addition 308 | - Multiplication 309 | - Subtraction 310 | - Division 311 | - Remainder 312 | - Rounding: All five rounding modes of IEEE 754—floor, ceiling, truncate, round-ties-to-even, and round-ties-away-from-zero—will be supported. 313 | - (This implies that a couple of the rounding modes in `Intl.NumberFormat` and `Temporal` won't be supported.) 314 | - Comparisons 315 | - equals and not-equals 316 | - less-than, less-than-or-equal 317 | - greater-than, greater-than-or-equal 318 | - Mantissa, exponent, significand 319 | 320 | 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. 321 | 322 | ### Unsupported operations 323 | 324 | - Bitwise operators are not supported, as they don’t logically make sense on the Decimal domain ([#20](https://github.com/tc39/proposal-decimal/issues/20)) 325 | - We currently do not foresee Decimal values interacting with other Number values. That is, TypeErrors will be thrown when trying to add, say, a Number to a Decimal, similar to the situation with BigInt and Number. ([#10](https://github.com/tc39/proposal-decimal/issues/10)). 326 | 327 | ### Conversion to and from other data types 328 | 329 | Decimal objects can be constructed from Numbers, Strings, and BigInts. Similarly, there will be conversion from Decimal objects to Numbers, String, and BigInts. 330 | 331 | ### String formatting 332 | 333 | `Decimal` objects can be converted to Strings in a number of ways, similar to Numbers: 334 | 335 | - `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)) 336 | - `toFixed()` is similar to Number's `toFixed()` 337 | - `toPrecison()` is similar to Number's `toPrecision()` 338 | - `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. 343 | 344 | ## Past discussions in TC39 plenaries 345 | 346 | - [Decimal for stage 0](https://github.com/tc39/notes/blob/main/meetings/2017-11/nov-29.md#9ivb-decimal-for-stage-0) (November, 2017) 347 | - [BigDecimal for Stage 1](https://github.com/tc39/notes/blob/main/meetings/2020-02/february-4.md) (February, 2020) 348 | - [Decimal update](https://github.com/tc39/notes/blob/main/meetings/2020-03/march-31.md) (March, 2020) 349 | - [Decimal stage 1 update](https://github.com/tc39/notes/blob/main/meetings/2021-12/dec-15.md#decimals) (December, 2021) 350 | - [Decimal stage 1 update](https://github.com/tc39/notes/blob/main/meetings/2023-03/mar-22.md#decimal-stage-1-update) (March, 2023) 351 | - [Decimal open-ended discussion](https://github.com/tc39/notes/blob/main/meetings/2023-07/july-12.md#decimal-open-ended-discussion) (July, 2023) 352 | - [Decimal stage 1 update and open discussion](https://github.com/tc39/notes/blob/main/meetings/2023-09/september-27.md#decimal-stage-1-update-and-discussion) (September, 2023) 353 | - [Decimal stage 1 update and request for feedback](https://github.com/tc39/notes/blob/main/meetings/2023-11/november-27.md#decimal-stage-1-update--request-for-feedback) (November, 2023) 354 | - [Decimal for stage 2](https://github.com/tc39/notes/blob/main/meetings/2024-04/april-11.md#decimal-for-stage-2) (April, 2024) 355 | - [Decimal for stage 2](https://github.com/tc39/notes/blob/main/meetings/2024-06/june-13.md#decimal-for-stage-2) (June, 2024) 356 | 357 | ## Future work 358 | 359 | The vision of decimal sketched here represents the champions current thinking and goals. In our view, decimal as sketched so far is a valuable addition to the language. That said, we envision improvements and strive to achieve these, too, in a version 2 of the proposal. What follows is _not_ part of the proposal as of today, but we are working to make the first version compatible with these future additions. 360 | 361 | ### Arithmetic operator and comparison overloading 362 | 363 | In earlier discussions about decimal, we advocated for such overloading arithmetic operations (`+`, `*`, etc.) and comparisons (`==,` `<`, etc.), as well as `===`. But based on strong implementer feedback, we have decided to work with the following proposal: 364 | 365 | - In the first version of this proposal, we intend to make `+`, `*`, and so on throw when either argument is a decimal value. Instead, one will have to use the `add`, `multiply`, etc. methods. Likewise, comparison operators such as `==`, `<`, `<=`, etc. will also throw when either argument is a decimal. One should use the `equals` and `lessThan` methods instead. 366 | - The strict equality operator `===` will work (won't throw an exception), but it will have its default object semantics; nothing special about decimal values will be involved. 367 | 368 | However, the door is not _permanently_ closed to overloading. It is just that the bar for adding it to JS is very high. We may be able to meet that bar if we get enough positive developer feedback and work with implementors to find a path forward. 369 | 370 | ### Decimal literals 371 | 372 | In earlier discussions of this proposal, we had advocated for adding new decimal literals to the language: `1.289m` (notice the little `m` suffix). Indeed, since decimals are numbers—essentially, basic data akin to the existing binary floating-point numbers—it is quite reasonable to aim for giving them their own "space" in the syntax. 373 | 374 | However, as with operator overloading, we have received strong implementor feedback that this is very unlikely to happen. 375 | 376 | Nonetheless, we are working on making sure that the v1 version of the proposal, sketched here, is compatible with a future in which decimal literals exist. As with operator overloading, discussions with JS engine implementors need to be kept open to find out what can be done to add this feature. (On the assumption that a v1 of decimals exists, one can add support for literals fairly straightforwardly using a Babel transform.) 377 | 378 | ### Advanced mathematical functions 379 | 380 | In our discussions we have consistently emphasized the need for basic arithmetic. And in the v1 of the proposal, we in fact stop there. One can imagine Decimal having all the power of the `Math` standard library object, with mathematical functions such as: 381 | 382 | - trigonometric functions (normal, inverse/arc, and hyperbolic combinations) 383 | - natural exponentiation and logarithm 384 | - any others? 385 | 386 | 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. 387 | 388 | ## FAQ 389 | 390 | ### What about rational numbers? 391 | 392 | See the discussion above, about data models, where rationals are discussed. 393 | 394 | ### Will Decimal have good performance? 395 | 396 | This depends on implementations. Like BigInt, implementors 397 | may decide whether or not to optimize it, and what scenarios 398 | to optimize for. We believe that, with either alternative, 399 | it is possible to create a high-performance Decimal 400 | implementation. Historically, faced with a similar decision 401 | of BigInt vs Int64, TC39 decided on BigInt; such a decision 402 | might not map perfectly because of differences in the use 403 | cases. Further discussion: 404 | [#27](https://github.com/tc39/proposal-decimal/issues/27) 405 | 406 | ### Will Decimal have the same behavior across implementations and environments? 407 | 408 | One option that’s raised is allowing for greater precision in more capable environments. However, Decimal is all about avoiding unintended rounding. If rounding behavior depended on the environment, the goal would be compromised in those environments. Instead, this proposal attempts to find a single set of semantics that can be applied globally. 409 | 410 | ### How does this proposal relate to other TC39 proposals like operator overloading? 411 | 412 | See [RELATED.md](./RELATED.md) for details. 413 | 414 | ### Why not have the maximum precision or default rounding mode set by the environment? 415 | 416 | Many decimal implementations support a global option to set the maximum precision (e.g., Python, Ruby). In QuickJS, there is a “dynamically scoped” version of this: the `setPrec` method changes the maximum precision while a particular function is running, re-setting it after it returns. Default rounding modes could be set similarly. 417 | 418 | Although the dynamic scoping version is a bit more contained, both versions are anti-modular: Code does not exist with independent behavior, but rather behavior that is dependent on the surrounding code that calls it. A reliable library would have to always set the precision around it. 419 | 420 | There is further complexity when it comes to JavaScript’s multiple globals/Realms: a Decimal primitive value does not relate to anything global, so it would be inviable to store the state there. It would have to be across all the Decimals in the system. But then, this forms a cross-realm communication channel. 421 | 422 | Therefore, this proposal does not contain any options to set the precision from the environment. 423 | 424 | ### Where can I learn more about decimals in general? 425 | 426 | 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. 427 | 428 | 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”. 429 | 430 | ## Relationship of Decimal to other TC39 proposals 431 | 432 | 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`). 433 | 434 | ## Implementations 435 | 436 | - Experimental implementation in [QuickJS](https://bellard.org/quickjs/), from release 2020-01-05 (use the `--bignum` flag) 437 | - [decimal128.js](https://www.npmjs.com/package/decimal128) is an npm package that implements Decimal128 in JavaScript (more precisely, the variant of Decimal128 that we envision for this proposal) 438 | - We are looking for volunteers for writing a polyfill along the lines of [JSBI](https://github.com/GoogleChromeLabs/jsbi) for both alternatives, see [#17](https://github.com/tc39/proposal-decimal/issues/17) 439 | 440 | ## Getting involved in this proposal 441 | 442 | Your help would be really appreciated in this proposal! There are lots of ways to get involved: 443 | 444 | - Share your thoughts on the [issue tracker](https://github.com/tc39/proposal-decimal/issues) 445 | - Document your use cases, and write sample code with decimal, sharing it in an issue 446 | - Research how decimals are used in the JS ecosystem today, and document what works and what doesn’t, in an issue 447 | - Help us write and improve documentation, tests, and prototype implementations 448 | 449 | See a full list of to-do tasks at [#45](https://github.com/tc39/proposal-decimal/issues/45). 450 | --------------------------------------------------------------------------------