1 of 15

Decimal: Stage 1 Update (September, 2023)

Jesse Alama (Igalia, in partnership with Bloomberg)

2 of 15

Motivation

  • When working with money in JS, these are your options:
    • Work with cents 😫
    • Reach for one of the many decimal arithmetic libraries out there 🤔
    • Cast exact decimal data to JS Number (64-bit binary floats) & hope for the best 😬
  • The same goes for other human-readable/manipulable decimal numeric quantities (e.g., units such as weight, distance, temperature, etc.)

3 of 15

Use cases

  • Computations with exact human-consumable numeric quantities (mainly, but not exclusively, money)
  • Data exchange

There are both frontend and backend scenarios for the above.

4 of 15

Code samples

5 of 15

Get the total for a bill, after applying tax

function calculateBill(items, tax) {

let total = new Decimal("0");

for (let {price, count} of items) {

total = total.add(price.times(new Decimal(count)));

}

return total.multiply(new Decimal(tax).add(1));

}

let items = [{price: "1.25", count: 5}, {price: "5.00", count: 1}];

let tax = "0.0735";

console.log(calculateBill(items, tax).toFixed(2));

6 of 15

Amortization schedule for a loan

const principal = new Decimal("500000");

const annualInterestRate = new Decimal("0.05");

const paymentsPerYear = new Decimal("12");

const monthlyInterestRate = annualInterestRate.divide(paymentsPerYear);

const years = new Decimal("30");

const one = new Decimal("1");

const paymentCount = paymentsPerYear.times(years);

const monthlyPaymentAmount = principal.times(monthlyInterestRate)

.divide(one.minus(monthlyInterestRate).pow(paymentCount).minus(one))

.times(one.add(monthlyInterestRate));

7 of 15

Stepping up/down a value by a little bit

function stepUp(d, n, x) {

let increment = new Decimal("10").pow(x);

return d.add(n.times(increment));

}

let starting = new Decimal("1.23");

let stepped = stepUp(starting, new Decimal("3"), new Decimal("-4"));

console.log(stepped.toFixed(4)); // 1.2305

8 of 15

Connect to DB, use SQL DECIMAL type in JS

const { Client } = require('pg');

const client = new Client({

user: 'username',

sql_decimal: 'decimal128', // or 'string', 'number'

// ...more options

});

const boost = new Decimal("1.05");

client.query('SELECT prices FROM data_with_numbers', (err, res) => {

console.log(res.rows.map(row => row.prices.times(boost)));

client.end();

});

9 of 15

Changes to the proposal since last time

Keep, drop, add, open

10 of 15

Keep

  • Data model: IEEE 754 Decimal128
    • But only normalized Decimal128 values will be emitted via e.g. toString
  • Support basic arithmetic operations only. No log, exp, trig
    • Decimal would likely be no better than Math’s version of these functions since they generally return irrational values
  • Throw if NaN would be the result of an operation

11 of 15

Drop

  • Operator overloading (+, *, etc.)
    • Based on strong implementer feedback.
    • Throw when any argument of any of these operators is a Decimal
  • Decimal literal syntax (123_456.789m)
    • If literals are available, mixing them with math operators is likely. But we decided that overloading won’t happen.
    • Also based on strong implementer feedback
  • New primitive type
    • If operator overloading won’t happen, having a primitive type for decimals would probably have no added value.
    • Also based on strong implementer feedback

12 of 15

Add

  • Support for the various rounding modes provided by Temporal and Intl.NumberFormat (goes beyond IEEE 754). Default is round-half-up.
    • Previously: “Support banker’s rounding only (like Math)”

13 of 15

Open questions

  • IEEE 754 +/- Infinity and -0 (minus zero)?
    • In earlier plenary discussions, the proposal to support these values received negative feedback
    • Some browsers already implement their own decimals (outside of their JS engines) and don’t worry about these
    • Pro: Calculations “just work”. No exceptions
    • Pro: More complete IEEE 754 conformance
    • Con: Is this just an important part of conformance? (Do we even claim to be conformant?)
    • Con: Many developers expect Decimal to handle only finite values. Few use cases for these values.
  • Do we want any “advanced” (non-exact) mathematical operators?
    • Two-argument exponentiation (pow) is a good candidate (it was included in code samples here!). But one-argument exponentiation? Logarithms? Isn’t Math good enough?

14 of 15

Current shape of the proposal

  • Data model: IEEE 754 Decimal128
  • New standard library object, Decimal with a constructor
  • Throw if an operation would produce NaN
  • Basic arithmetic only: Addition, multiplication, subtraction, division, remainder, round, absolute value
  • Support Temporal and Intl.NumberFormat rounding modes (this includes all IEEE 754 rounding modes). Default round is round-half-up.
  • Mathematical operators (+, *, etc.) and math-y comparisons (===, <, etc.) throw when given Decimal arguments
  • An npm version of the proposal exists. It tracks our latest thinking.

15 of 15

But wait, there’s more! (…in the future, maybe)

Looking ahead to a potential v2 or v3 of Decimal, we can consider:

  • Making mathematical operators (+, *, etc.) and math-y comparison operators (<, etc.) work when given Decimal arguments (this requires a lot of work, and needs support from implementers)
  • Possibly adding decimal literals (ditto)
  • Adding “advanced” math functions (sqrt, trig, exp, log) based on documented JS developer need