@littledan
Decimal: Avoid Rounding
Errors on in JavaScript
Daniel Ehrenberg
Igalia
Node.TLV 2020
@littledan@littledan
‫אני‬ ‫מי‬
● Daniel Ehrenberg
● @littledan
● Delegate in TC39
@littledan@littledan
‫שלי‬ ‫הבית‬
Les Roquetes del Garraf, Europe
@littledan@littledan
‫שלי‬ ‫ה‬ ָ‫בר‬ֶ‫ח‬
Embedded WebKit and Chromium development
Mesa and GStreamer drivers
CSS, ARIA, WebAssembly, MathML, JavaScript
standards+implementation in web browsers+Node.js
@littledan@littledan
@littledan@littledan
Who is TC39?
● A committee of Ecma, with…
● JS developers
● JavaScript engines
● Transpilers
● Frameworks, libraries
● Academics
● Node.js collaborators
● Some big websites/app platforms
● (No Israeli companies ☹)
@littledan@littledan
@littledan@littledan
@littledan@littledan
Meetings
● Every two months
● For three days
● Discuss language changes
● Seek consensus on proposal
Stage advancement
@littledan@littledan
TC39 stages
● Stage 1: An idea under discussion
● Stage 2: We want to do this, and we have a first draft
● Stage 3: Basically final draft; ready to go
● Stage 4: 2+ implementations, tests ⇒ standard
@littledan@littledan
Consensus-based decision-making
● TC39 doesn't vote on what the language will be
● TC39 is consensus-seeking
○ We work together to meet everyone's goals
○ "Does anyone object to stage advancement?"
○ Objections must have rationale, appropriate to stage
● Goals: Listen to everyone engaging in process;
Don't choose one stakeholder over another as the "decider";
Avoid standardizing things which aren't ready yet
@littledan
BigInt: Stage 4!
@littledan@littledan
const x = 2 ** 53;
⇒ x === 9007199254740992
const y = x + 1;
⇒ y === 9007199254740992
@littledan
@littledan@littledan
const x = 2n ** 53n;
⇒ x === 9007199254740992n
const y = x + 1n;
⇒ y === 9007199254740993n
@littledan
@littledan@littledan
const x = 2n ** 53n;
⇒ x === 9007199254740992n
const y = x + 1n;
⇒ y === 9007199254740993n
nstands for BigInt
@littledan
@littledan@littledan
67 🚢 68 🚢
Flag 🏗
79 🚢
10.4 🚢
@littledan@littledan
Igalia's role in BigInt
● Wrote BigInt specification and some tests
● Standardized BigInt in TC39
● Implemented BigInt in SpiderMonkey (shipping)
and JSC (in progress)
@littledan
Private fields and
methods:
Stage 3
@littledan@littledan
Why?
● Private methods encapsulate
behavior
● You can access private fields inside
private methods
class Counter extends HTMLElement {
#x = 0;
connectedCallback() {
this.#render();
}
#render() {
this.textContent =
this.#x.toString();
}
}
@littledan@littledan
#
is the new
_
for strong encapsulation
@littledan@littledan
class PrivateCounter {
#x = 0;
}
let p = new PrivateCounter();
console.log(p.#x); // SyntaxError
class PublicCounter {
_x = 0;
}
let c = new PublicCounter();
console.log(c._x); // 0
Stage 3
@littledan
@littledan@littledan
Why not private keyword?
● In languages with types:
obj.x can check whether x is private by looking at the type of obj
● JavaScript is dynamically typed
● ⇒ private vs public distinction needed at access point,
not just definition
● # as part of the name was the cleanest, simplest solution we found
● We thought about many alternatives over 20 years; ask me later about any
further possibilities
@littledan
Stage 2 features
We want to do this, and we have a first draft
@littledan
Decorators: Stage 2
Yehuda Katz
Ron Buckton
@littledan@littledan
// Salesforce abstraction
import { api } from '@salesforce';
class InputAddress extends HTMLElement {
@api address = '';
}
Using decorators to track mutations and rehydrate
a web component when needed.
Syntax abstraction for attrs/props in Web Components
// Polymer abstraction
class XCustom extends PolymerElement {
@property({ type: String, reflect: true
})
address = '';
}
Example of using decorators to improve ergonomics.
@littledan
Stage 1 features
An idea under discussion
@littledan
Decimal!
@littledan@littledan
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
@littledan@littledan
What's wrong with Number?
● Number is an IEEE 754 64-bit float
@littledan@littledan
Number: an IEEE 754 64-bit float
From Wikimedia Commons, by Codekaizen, GFDL
@littledan@littledan
What's wrong with Number?
● Number is an IEEE 754 64-bit float
● Base 2 fractions can't represent .1 or .2 exactly!
> .1.toPrecision(70)
<< "0.1000000000000000055511151231257827021181583404541015625000000000000000"
> .2.toPrecision(70)
<< "0.2000000000000000111022302462515654042363166809082031250000000000000000"
● ⅓ in base 10: closest we can get is .3333333...,
but eventually we have to cut it off
@littledan@littledan
Use cases and requirements for decimal
Primarily: Dealing with human-written decimal quantities, like money
● Represent typical quantities precisely
● Arithmetic operations: + - * /
● Rounding w/ configurable precision, mode
● Serialize/deserialize with strings
● Display to users
● Easy to use correctly
@littledan@littledan
Why add this to JavaScript now?
We got by without JS decimal
● Server in another language
● JS fronts a thin client
● Don't trust the client anyway (still true!)
What changed?
● Richer clients doing local calculations
(definitively calculated on server, but should avoid ephemeral inaccuracies)
● Server-side in JS with serverless and Node.js!
@littledan
How JS decimal
would work
@littledan@littledan
Basic usage
● Literal syntax:
123.456m
● Operator overloading:
.1m + .2m === .3m
@littledan@littledan
Serialization: JSON.stringify() and toString()
● toJSON like BigInt: Throws by default :(
○ Sorry, changing JSON would break compatibility
○ JSON.stringify takes second parameter to customize
JSON.stringify(123.456m) // ⇒ TypeError
● toString like Number and BigInt:
○ Excludes the m suffix
(123.456m).toString() // ⇒ "123.456"
@littledan@littledan
Presentation to the user: Intl.NumberFormat
@littledan@littledan
Presentation to the user: Intl.NumberFormat
● Intl.NumberFormat.prototype.format, formatToParts:
overloaded for BigDecimal
(like BigInt)
● BigDecimal.prototype.toLocaleString works too
(123123.456m).toLocaleString("de-DE") // ⇒ "123.123,456"
@littledan@littledan
Rounding: the .round(decimal, options) method
● roundingMode: e.g., "up", "down", "half-up", "half-even" (more?). Required
explicitly
● Precision options (names matching Intl.NumberFormat, only one permitted):
○ maximumFractionDigits
○ maximumSignificantDigits
BigDecimal.round(3.25m, { roundingMode: "half-up",
maximumFractionDigits: 1 })
====> 3.3m
@littledan
The data model. Options:
● Fraction
● BigDecimal
● Decimal128
@littledan@littledan
Rationals, i.e., Fractions
Common Lisp, Scheme, OCaml, Ruby, Perl6 etc.
● 2.53m => 253/100
● ⅓ represented exactly
● No need to round for any arithmetic!
numerator
denominator
BigInt…………………...
BigInt…………………...
@littledan@littledan
In this proposal, we're not pursuing rationals
Core operations for the primary use case are not a good fit
● Rounding to a certain base-10 precision, with a rounding mode
○ "Round up to the nearest cent after adding the tax"
● Conversion to a localized, human-readable string
○ "And display that to the user"
● Fractions may be proposed separately
○ E.g., Python and Ruby have both fractions and decimal
○ Just different use cases, no one subsumes the other
@littledan@littledan
Fixed-size decimal, e.g., Decimal128
Python, C#, IEEE 754-2008, Swift, Decimal.js
● Pick a maximum number of digits
● Floating point, only in base 10
● Rounds if there isn't enough space
e.g. 32.5 ->
Value: sign * mantissa * 10exponent
sign (1 or -1)
exponent base 10
mantissa: decimal fraction
0
2
.325
@littledan@littledan
Arbitrary-size decimal, i.e., BigDecimal
Ruby, Java, Big.js
● Number of digits grows with the number
● Represent (almost) any decimal exactly
● Avoid rounding*
Value: sign * mantissa * 10exponent
sign
exponent
mantissa BigInt…………………...
@littledan@littledan
Troubles with precision and BigDecimal
● What should 1m/3m be?
● How long should the decimal go?
● Option: Cut it off at an arbitrary point (Ruby)
○ .33333333333333m
● Option: Force a rounding options parameter (Java)
○ BigDecimal.div(1m, 3m, { roundingMode: "half-up",
maximumFractionDigits: 2 })
@littledan@littledan
Plan: Why not both?
● There are pros and cons of BigDecimal and Decimal128
● Our plan: Try both and gather feedback
○ Implement two polyfills and write full documentation
○ Encourage people to try it out in their programs
○ Collect feedback through surveys, GitHub issues
● Results ⇒ Stage 2 proposal
@littledan
Participating in TC39
@littledan@littledan
Helping with existing proposals
● Often highly appreciated!
● What you want may already be a proposal
○ You might not be the only person with this problem
How you can help: participating on GitHub, with a proposal at a stage...
● Stage 0/1: Help determine use cases, very high level design
● Stage 2: Help work out all the details, early toy implementations
● Stage 3: Implement, document and test
● Stage 4: It's already done
@littledan@littledan
Participating internationally
● Many TC39 members are in a similar situation to you all:
○ English is a second language
■ You can participate with mostly written communication
○ Live outside the US
■ Many delegates live in Europe
○ Unable to attend most TC39 meetings in person
■ Instead, join by video call
● TC39's code of conduct prohibits discrimination on nationality
● Many TC39 members want to increase international participation
● TC39 participants represent ideas and organizations, not countries
@littledan@littledan
Participating asynchronously on GitHub
● Most tasks don't take place in meetings:
Most important technical work happens on GitHub
● Work on GitHub:
○ Some of the discussion about design
○ Specification text
○ Documentation
○ Tests
○ Implementations
● Non-members can contribute on GitHub! We encourage it.
○ Just sign non-member IPR form
● Meeting notes are published to GitHub
● Only members can take part in meetings and consensus process
@littledan@littledan
Joining TC39
● Join TC39 by joining Ecma
● Joining Ecma requires:
○ Signing IPR forms
■ Typical Ecma RAND policy
■ TC39-specific royalty-free agreement
○ Paying membership fee
○ Technically, a vote (typically a formality)
● TC39 delegates represent member organizations
● Companies considering joining can provisionally attend TC39 as
"prospective members"
@littledan
TC39 changes over time
@littledan@littledan
Older TC39 mode
● Specification: Big MS Word doc
● Communication: Meetings and es-discuss list
● Large, occasional specification; no stages
@littledan@littledan
@littledan@littledan
@littledan@littledan
@littledan@littledan
@littledan@littledan
@littledan@littledan
@littledan@littledan
@littledan@littledan
@littledan@littledan
JS Decimal's future in this context
● Aim to be participatory, rigorous and experience-driven
● Prototyping multiple alternatives: BigDecimal and Decimal128
● Developing proposals to generalize decimal:
Operator overloading, extended numerical literals
● This is still early, and we could use your help
@littledan
!‫תודה‬
Follow up on Decimal:
github.com/tc39/proposal-decimal
Tell us your Decimal use cases:
bit.ly/2PFDT2o
Get involved in TC39:
tc39.es
Stay in touch with me:
@littledan on Twitter (DMs open)
littledan@igalia.com
Learn more about Igalia: igalia.com

BigDecimal: Avoid rounding errors on decimals in JavaScript (Node.TLV 2020)

  • 1.
    @littledan Decimal: Avoid Rounding Errorson in JavaScript Daniel Ehrenberg Igalia Node.TLV 2020
  • 2.
    @littledan@littledan ‫אני‬ ‫מי‬ ● DanielEhrenberg ● @littledan ● Delegate in TC39
  • 3.
  • 4.
    @littledan@littledan ‫שלי‬ ‫ה‬ ָ‫בר‬ֶ‫ח‬ EmbeddedWebKit and Chromium development Mesa and GStreamer drivers CSS, ARIA, WebAssembly, MathML, JavaScript standards+implementation in web browsers+Node.js
  • 5.
  • 6.
    @littledan@littledan Who is TC39? ●A committee of Ecma, with… ● JS developers ● JavaScript engines ● Transpilers ● Frameworks, libraries ● Academics ● Node.js collaborators ● Some big websites/app platforms ● (No Israeli companies ☹)
  • 7.
  • 8.
  • 9.
    @littledan@littledan Meetings ● Every twomonths ● For three days ● Discuss language changes ● Seek consensus on proposal Stage advancement
  • 10.
    @littledan@littledan TC39 stages ● Stage1: An idea under discussion ● Stage 2: We want to do this, and we have a first draft ● Stage 3: Basically final draft; ready to go ● Stage 4: 2+ implementations, tests ⇒ standard
  • 11.
    @littledan@littledan Consensus-based decision-making ● TC39doesn't vote on what the language will be ● TC39 is consensus-seeking ○ We work together to meet everyone's goals ○ "Does anyone object to stage advancement?" ○ Objections must have rationale, appropriate to stage ● Goals: Listen to everyone engaging in process; Don't choose one stakeholder over another as the "decider"; Avoid standardizing things which aren't ready yet
  • 12.
  • 13.
    @littledan@littledan const x =2 ** 53; ⇒ x === 9007199254740992 const y = x + 1; ⇒ y === 9007199254740992 @littledan
  • 14.
    @littledan@littledan const x =2n ** 53n; ⇒ x === 9007199254740992n const y = x + 1n; ⇒ y === 9007199254740993n @littledan
  • 15.
    @littledan@littledan const x =2n ** 53n; ⇒ x === 9007199254740992n const y = x + 1n; ⇒ y === 9007199254740993n nstands for BigInt @littledan
  • 16.
    @littledan@littledan 67 🚢 68🚢 Flag 🏗 79 🚢 10.4 🚢
  • 17.
    @littledan@littledan Igalia's role inBigInt ● Wrote BigInt specification and some tests ● Standardized BigInt in TC39 ● Implemented BigInt in SpiderMonkey (shipping) and JSC (in progress)
  • 18.
  • 19.
    @littledan@littledan Why? ● Private methodsencapsulate behavior ● You can access private fields inside private methods class Counter extends HTMLElement { #x = 0; connectedCallback() { this.#render(); } #render() { this.textContent = this.#x.toString(); } }
  • 20.
  • 21.
    @littledan@littledan class PrivateCounter { #x= 0; } let p = new PrivateCounter(); console.log(p.#x); // SyntaxError class PublicCounter { _x = 0; } let c = new PublicCounter(); console.log(c._x); // 0 Stage 3 @littledan
  • 22.
    @littledan@littledan Why not privatekeyword? ● In languages with types: obj.x can check whether x is private by looking at the type of obj ● JavaScript is dynamically typed ● ⇒ private vs public distinction needed at access point, not just definition ● # as part of the name was the cleanest, simplest solution we found ● We thought about many alternatives over 20 years; ask me later about any further possibilities
  • 23.
    @littledan Stage 2 features Wewant to do this, and we have a first draft
  • 24.
  • 25.
    @littledan@littledan // Salesforce abstraction import{ api } from '@salesforce'; class InputAddress extends HTMLElement { @api address = ''; } Using decorators to track mutations and rehydrate a web component when needed. Syntax abstraction for attrs/props in Web Components // Polymer abstraction class XCustom extends PolymerElement { @property({ type: String, reflect: true }) address = ''; } Example of using decorators to improve ergonomics.
  • 26.
    @littledan Stage 1 features Anidea under discussion
  • 27.
  • 28.
    @littledan@littledan 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
  • 29.
    @littledan@littledan What's wrong withNumber? ● Number is an IEEE 754 64-bit float
  • 30.
    @littledan@littledan Number: an IEEE754 64-bit float From Wikimedia Commons, by Codekaizen, GFDL
  • 31.
    @littledan@littledan What's wrong withNumber? ● Number is an IEEE 754 64-bit float ● Base 2 fractions can't represent .1 or .2 exactly! > .1.toPrecision(70) << "0.1000000000000000055511151231257827021181583404541015625000000000000000" > .2.toPrecision(70) << "0.2000000000000000111022302462515654042363166809082031250000000000000000" ● ⅓ in base 10: closest we can get is .3333333..., but eventually we have to cut it off
  • 32.
    @littledan@littledan Use cases andrequirements for decimal Primarily: Dealing with human-written decimal quantities, like money ● Represent typical quantities precisely ● Arithmetic operations: + - * / ● Rounding w/ configurable precision, mode ● Serialize/deserialize with strings ● Display to users ● Easy to use correctly
  • 33.
    @littledan@littledan Why add thisto JavaScript now? We got by without JS decimal ● Server in another language ● JS fronts a thin client ● Don't trust the client anyway (still true!) What changed? ● Richer clients doing local calculations (definitively calculated on server, but should avoid ephemeral inaccuracies) ● Server-side in JS with serverless and Node.js!
  • 34.
  • 35.
    @littledan@littledan Basic usage ● Literalsyntax: 123.456m ● Operator overloading: .1m + .2m === .3m
  • 36.
    @littledan@littledan Serialization: JSON.stringify() andtoString() ● toJSON like BigInt: Throws by default :( ○ Sorry, changing JSON would break compatibility ○ JSON.stringify takes second parameter to customize JSON.stringify(123.456m) // ⇒ TypeError ● toString like Number and BigInt: ○ Excludes the m suffix (123.456m).toString() // ⇒ "123.456"
  • 37.
  • 38.
    @littledan@littledan Presentation to theuser: Intl.NumberFormat ● Intl.NumberFormat.prototype.format, formatToParts: overloaded for BigDecimal (like BigInt) ● BigDecimal.prototype.toLocaleString works too (123123.456m).toLocaleString("de-DE") // ⇒ "123.123,456"
  • 39.
    @littledan@littledan Rounding: the .round(decimal,options) method ● roundingMode: e.g., "up", "down", "half-up", "half-even" (more?). Required explicitly ● Precision options (names matching Intl.NumberFormat, only one permitted): ○ maximumFractionDigits ○ maximumSignificantDigits BigDecimal.round(3.25m, { roundingMode: "half-up", maximumFractionDigits: 1 }) ====> 3.3m
  • 40.
    @littledan The data model.Options: ● Fraction ● BigDecimal ● Decimal128
  • 41.
    @littledan@littledan Rationals, i.e., Fractions CommonLisp, Scheme, OCaml, Ruby, Perl6 etc. ● 2.53m => 253/100 ● ⅓ represented exactly ● No need to round for any arithmetic! numerator denominator BigInt…………………... BigInt…………………...
  • 42.
    @littledan@littledan In this proposal,we're not pursuing rationals Core operations for the primary use case are not a good fit ● Rounding to a certain base-10 precision, with a rounding mode ○ "Round up to the nearest cent after adding the tax" ● Conversion to a localized, human-readable string ○ "And display that to the user" ● Fractions may be proposed separately ○ E.g., Python and Ruby have both fractions and decimal ○ Just different use cases, no one subsumes the other
  • 43.
    @littledan@littledan Fixed-size decimal, e.g.,Decimal128 Python, C#, IEEE 754-2008, Swift, Decimal.js ● Pick a maximum number of digits ● Floating point, only in base 10 ● Rounds if there isn't enough space e.g. 32.5 -> Value: sign * mantissa * 10exponent sign (1 or -1) exponent base 10 mantissa: decimal fraction 0 2 .325
  • 44.
    @littledan@littledan Arbitrary-size decimal, i.e.,BigDecimal Ruby, Java, Big.js ● Number of digits grows with the number ● Represent (almost) any decimal exactly ● Avoid rounding* Value: sign * mantissa * 10exponent sign exponent mantissa BigInt…………………...
  • 45.
    @littledan@littledan Troubles with precisionand BigDecimal ● What should 1m/3m be? ● How long should the decimal go? ● Option: Cut it off at an arbitrary point (Ruby) ○ .33333333333333m ● Option: Force a rounding options parameter (Java) ○ BigDecimal.div(1m, 3m, { roundingMode: "half-up", maximumFractionDigits: 2 })
  • 46.
    @littledan@littledan Plan: Why notboth? ● There are pros and cons of BigDecimal and Decimal128 ● Our plan: Try both and gather feedback ○ Implement two polyfills and write full documentation ○ Encourage people to try it out in their programs ○ Collect feedback through surveys, GitHub issues ● Results ⇒ Stage 2 proposal
  • 47.
  • 48.
    @littledan@littledan Helping with existingproposals ● Often highly appreciated! ● What you want may already be a proposal ○ You might not be the only person with this problem How you can help: participating on GitHub, with a proposal at a stage... ● Stage 0/1: Help determine use cases, very high level design ● Stage 2: Help work out all the details, early toy implementations ● Stage 3: Implement, document and test ● Stage 4: It's already done
  • 49.
    @littledan@littledan Participating internationally ● ManyTC39 members are in a similar situation to you all: ○ English is a second language ■ You can participate with mostly written communication ○ Live outside the US ■ Many delegates live in Europe ○ Unable to attend most TC39 meetings in person ■ Instead, join by video call ● TC39's code of conduct prohibits discrimination on nationality ● Many TC39 members want to increase international participation ● TC39 participants represent ideas and organizations, not countries
  • 50.
    @littledan@littledan Participating asynchronously onGitHub ● Most tasks don't take place in meetings: Most important technical work happens on GitHub ● Work on GitHub: ○ Some of the discussion about design ○ Specification text ○ Documentation ○ Tests ○ Implementations ● Non-members can contribute on GitHub! We encourage it. ○ Just sign non-member IPR form ● Meeting notes are published to GitHub ● Only members can take part in meetings and consensus process
  • 51.
    @littledan@littledan Joining TC39 ● JoinTC39 by joining Ecma ● Joining Ecma requires: ○ Signing IPR forms ■ Typical Ecma RAND policy ■ TC39-specific royalty-free agreement ○ Paying membership fee ○ Technically, a vote (typically a formality) ● TC39 delegates represent member organizations ● Companies considering joining can provisionally attend TC39 as "prospective members"
  • 52.
  • 53.
    @littledan@littledan Older TC39 mode ●Specification: Big MS Word doc ● Communication: Meetings and es-discuss list ● Large, occasional specification; no stages
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
    @littledan@littledan JS Decimal's futurein this context ● Aim to be participatory, rigorous and experience-driven ● Prototyping multiple alternatives: BigDecimal and Decimal128 ● Developing proposals to generalize decimal: Operator overloading, extended numerical literals ● This is still early, and we could use your help
  • 63.
    @littledan !‫תודה‬ Follow up onDecimal: github.com/tc39/proposal-decimal Tell us your Decimal use cases: bit.ly/2PFDT2o Get involved in TC39: tc39.es Stay in touch with me: @littledan on Twitter (DMs open) littledan@igalia.com Learn more about Igalia: igalia.com