Decimal:�For Stage 1
Andrew Paprocki: Bloomberg
Daniel Ehrenberg: Igalia
February 2020 TC39
Goal of this presentation
“Why are Numbers broken in JS?”
A recap of our discussion from November 2017
Problem and solution (?)
// Number (binary 64-bit floating point)
js> 0.1 + 0.2
=> 0.30000000000000004
// BigDecimal or Decimal128 (???)
js> 0.1m + 0.2m
=> 0.3m
Workarounds exist
Use cases
Primary use case: Interchange/calculations with money/decimal quantities
May use "cents", but:
Goals from human-readable decimal values
Secondary: High-precision floating point applications
This is a bit of a mismatch, and much lower priority/"nice to have
Larger (IEEE 754 128/256-bit) or arbitrary-precision binary floats (e.g., QuickJS) may be more efficient, but BigDecimal could also work
Lower priority goals from numerical use cases
Cross-cutting: Interaction with other systems using decimals
Related goals:
Language design goals
How should decimals be represented? Three data models
Rationals,�i.e., fractions
Rationals
Common Lisp, Scheme, OCaml, Ruby, Perl6 etc.
numerator
denominator
BigInt…………………...
BigInt…………………...
Rationals
In this proposal, we're not pursuing rationals
Core operations for the primary use case are not a good fit
Fixed-size decimal
Fixed-size decimal
e.g. 32.5 ->
Value: sign * mantissa * 10exponent
sign (1 or -1)
exponent base 10
mantissa: decimal fraction
0
2
.325
Python fixed-size decimal (docs)
Decimal.js behaves similarly
>>> from decimal import *�>>> Decimal("1") + Decimal("2.1")�Decimal('3.1')
>>> getcontext().prec = 6�>>> Decimal(1) / Decimal(7)�Decimal('0.142857')
C# fixed-size decimal (docs)
Value: sign * mantissa * 10exponent
sign
exponent: -28 to 28/29
mantissa: 96-bit binary integer
IEEE 754-2008 fixed-size decimal (spec)
Value: sign * mantissa * 10exponent
sign
exponent: up to 6144
mantissa: 34 base-10 digits
IEEE 754-2008 fixed-size decimal (spec)
�Note: most languages do not use IEEE 754 decimal, but it seems fine
In this proposal, if we use fixed-size decimal, we'd use IEEE 754 128-bit
Arbitrary-precision decimal
Value: sign * mantissa * 10exponent
sign
exponent
mantissa
BigInt…………………...
BigDecimal
Ruby division details�Big.js behaves similarly
> (BigDecimal(1) / BigDecimal(4096)).to_s�=> "0.244140625E-3"�> BigDecimal.limit(3)�=> 0�> (BigDecimal(1) / BigDecimal(4096)).to_s�=> "0.244E-3"
Java division details
Java division details
new BigDecimal(6).divide(new BigDecimal(3))� => 2
new BigDecimal(1).divide(new BigDecimal(3))� => java.lang.ArithmeticException
new BigDecimal(1).divide(� new BigDecimal(3), BigDecimal.ROUND_UP))� => 1
new BigDecimal(1).divide(new BigDecimal(3),� 10, BigDecimal.ROUND_UP)) // 10 is the scale� => .3333333334
BigDecimal division in this proposal
If we do arbitrary-precision decimal, then "doing division right" seems best.
⇒ no division operator, since it cannot accept the rounding options
Normalization
Normalization: Strip off trailing zero decimal places?
To normalize or not?
What’s at stake: the Trichotomy
For any numeric values a and b, one of the following is true:
Do we care about the === version?
Always Normalize
2.50m.toString() => “2.5”
2.5m.toString() => “2.5”
2.50m === 2.5m => true
2.50m == 2.5m => true
Object.is(2.50m, 2.5m) => true
No Auto-normalization:� == and === compare value
2.50m.toString() => “2.50”
2.5m.toString() => “2.5”
2.50m === 2.5m => true
2.50m == 2.5m => true
Object.is(2.50m, 2.5m) => false
No Auto-normalization:� == compares value� === compares precision
2.50m.toString() => “2.50”
2.5m.toString() => “2.5”
2.50m === 2.5m => false
2.50m == 2.5m => true
Object.is(2.50m, 2.5m) => false
No Auto-normalization:� == and === compare value
Object.is differentiates
2.50m.toString() => “2.50”
2.5m.toString() => “2.5”
2.50m === 2.5m => true
2.50m == 2.5m => true
Object.is(2.50m, 2.5m) => false
Normalization
Normalization in this proposal
Floating point options
Settings object
Settings/options in this proposal
Rationale
Decimal NaN/Infinity
Decimal Signed 0
Non-finite values and signed zero in this proposal
Primitive with operator overloading
Basic usage
Primitive type semantics
Operator overloading semantics
Standard library
BigDecimal/Decimal128� .round(decimal, options)
BigDecimal.round(3.25m, { roundingMode: "half-up",� maximumFractionDigits: 1 })
====> 3.3m
BigDecimal/Decimal128� .div(numerator, denominator, options)
BigDecimal.div(1m, 3m, { roundingMode: "down",� maximumFractionDigits: 3 })
====> .333m
BigDecimal/Decimal128� .partition(decimal, pieces, options)
BigDecimal.partition(20.2m, 5, { maximumFractionDigits: 1 })
====> [ 4.1m, 4.1m, 4.0m, 4.0m, 4.0m ]
Mathematical calculations
Serialization
JSON.stringify() and toString()
JSON.stringify(123.456m) // ⇒ TypeError
(123.456m).toString() // ⇒ "123.456"
TypedArrays and DataViews
Intl.NumberFormat integration
(123123.456m).toLocaleString("de-DE") // ⇒ "123.123,456"
Two starting points:�BigDecimal and Decimal128
BigDecimal data model
Value: sign * mantissa * 10exponent
sign
exponent
mantissa
BigInt…………………...
Decimal128 data model
sign
exponent: up to 6144
mantissa: 34 base-10 digits
Differences: Division semantics
Differences: Operator exactness
Differences: Standard library
Advantages of Decimal128
Advantages of BigDecimal
Relationship to other proposals
Value types
Operator overloading
Extended numeric literals
Moving forward with so much in flight
Developer feedback so far
Excitement
Some concerns from JS developers
More feedback highly welcomed!
This is just a starting point!
Let's consider alternatives
Some questions to consider (during Stage 1)
Plans to move to Stage 2
Stage 1 for Decimal?
Decimal128 or BigDecimal