<!-- Summary provided to audience: --> <style> /* make code examples suck less */ pre code { margin: 0 !important; font-size: 1.25em !important; } pre { padding: 0 !important; text-align: left !important; } .align-left { text-align: left !important; } .small-li li { font-size: 0.85em !important; } </style> <!-- .slide: data-cover --> # Decimal & Measure ## A unified approach for numbers in JS Jesse Alama & Ben Allen (Igalia, in partnership with Bloomberg & Google) jesse@igalia.com, ben@igalia.com & Eemeli Aro (Mozilla) February 2025 --- ## [Decimal](https://github.com/tc39/proposal-decimal) Exact decimal numbers to eliminate rounding errors frequently seen with binary floats when handling "human" numeric data, especially when calculating with them. **Example:** 1.3 *really is* 1.3, not an appoximation thereof. **Example:** 0.1 + 0.2 and 0.3 represent the same mathematical value. --- ## [Measure](https://github.com/tc39/proposal-measure) Tag numbers with a unit (e.g., `"gram"`) and, optionally, a precision. ```javascript let m = new Measurement("30", {unit: "centimeter"}); m.toString(); // "30 centimeters" m.convertTo("centimeter", -2).toString(); // "30.00 centimeters" let f = new Measurement("5.5", {unit: "foot"}); f.toString(); // "5.5 feet" f.convertTo("foot-and-inch").toComponents(); // [ {value: 5, unit: "foot"}, {value: 6, unit: "inch"}] ``` --- ## Shared design space The two proposals address distinct needs, but share an interesting overlap: **both help representing numbers the way humans use numbers**. - base-10 rounding - precision - units <!-- * Decimal, as currently designed, lacks any notion of "precision" of a number. They're mathematical values. * Example: There's no detectable difference between (Decimals constructed from) "1.2" and "1.20" in this setting. * Measure allows us to handle numbers with precision. * Example: `new Measurement(1.2, {precision: -2}).toString()` gives us "1.20". --> --- ## Measure can use Decimal Measure needs to define its underlying numeric representation. Intl currently uses _mathematical values_ to avoid floating-point errors. Measure could instead directly use decimals, and Decimal objects could be upgradeable into Measure objects. ```javascript= new Measure(new Decimal("1.2"), {unit: "gram", precision: -1}) ``` --- # Learning from Temporal --- ## Learning from Temporal Temporal introduces multiple concepts, and users can mix-and-match them: _time_, _dates_, _timezones_. The API is strongly typed, with explicit conversion methods between the types, offering very good DX: `PlainTime`, `PlainDate`, `PlainDateTime`, `ZonedDateTime`, `Instant` --- ## Learning from Temporal Can we design a unified system across the two proposals, using a single underlying model but including different amount of information in different types? --- _Base-10 decimal numbers_ <style> .bl { border-left: thin solid black; } .br { border-right: thin solid black; } .bt { border-top: thin solid black; } .bb { border-bottom: thin solid black; } td:has(> table) { padding: 0 !important; } td table th { font-style: italic; } td table td, td table th { padding: 8px !important; width: 50%; } </style> <table> <tr> <th colspan=2 rowspan=2></th> <th colspan=2 class="br">Exposes precision?</th> </tr><tr> <th class="bl bb">No</th> <th class="bl bb br">Yes</th> </tr><tr> <th rowspan=2 class="bb">Has a unit?</th> <th class="bb bt">No</th> <td class="bl bb">Decimal (normalized)</td> <td class="bl br bb">Decimal (full IEEE-754)</td> </tr><tr> <th class="bb">Yes</th> <td class="bl bb">Exact measures <small>(e.g. speed of light)</small></td> <td class="bl br bb">Measure <small>(e.g. length of a stick)</small></td> </tr> </table> --- _All numeric types?_ <table> <colgroup> <col span="2" style="width: 20%"> <col span="2" style="width: 30%"> </colgroup> <tr> <th colspan=2 rowspan=2></th> <th colspan=2 class="br">Exposes precision?</th> </tr><tr> <th class="bl bb">No</th> <th class="bl bb br">Yes</th> </tr><tr> <th rowspan=3 class="bb">Numeric type</th> <th class="bb bt">float64</th> <td class="bl bb"> <table> <tr> <th colspan=2>Has a unit?</th> </tr><tr> <th>No</th> <th class="bl">Yes</th> </tr><tr> <td>Number</td> <td class="bl">?</td> </tr> </table> </td> <td class="bl br bb"> <table> <tr> <th colspan=2>Has a unit?</th> </tr><tr> <th>No</th> <th class="bl">Yes</th> </tr><tr> <td>?</td> <td class="bl">Measure?</td> </tr> </table> </td> </tr><tr> <th class="bb">integer</th> <td class="bl bb"> <table> <tr> <th colspan=2>Has a unit?</th> </tr><tr> <th>No</th> <th class="bl">Yes</th> </tr><tr> <td>BigInt</td> <td class="bl">?</td> </tr> </table> </td> <td class="bl br bb"> <table> <tr> <th colspan=2>Has a unit?</th> </tr><tr> <th>No</th> <th class="bl">Yes</th> </tr><tr> <td>?</td> <td class="bl">Measure?</td> </tr> </table> </td> </tr><tr> <th class="bb">base-10 float</th> <td class="bl bb"> <table> <tr> <th colspan=2>Has a unit?</th> <tr> <th>No</th> <th class="bl">Yes</th> <tr> <td>Decimal (normalized)</td> <td class="bl">Exact measures</td> </table> </td> <td class="bl br bb"> <table> <tr> <th colspan=2>Has a unit?</th> <tr> <th>No</th> <th class="bl">Yes</th> <tr> <td>Decimal (full IEEE-754)</td> <td class="bl">Measure</td> </table> </td> </tr> </table> --- ## Do we really need all of that? Everything could be expressed by a single class: - "1"/"u" could be a valid unit, meaning that the _unitless_ number 2.34 is actually 2.34 with unit 1. - _Infinity_ could be a valid precision, meaning that the an exact number is actually a number with infinite precision. --- ## Do we really need all of that? Having separate types depending on the level of information has significant DX advantages: - No need to manually validate which information is present/absent in your inputs every time - Better static type checking - _Additional information limits capabilities_ --- ## Additional information limits capabilities When it comes to "doing math with your numbers", having more information can mean you can do fewer operations with it. - 1.23 + 0.04 = 1.27 ✅ - 1.23 (3 significant digits) + 0.040 (2 significant digits) = ??? - 1.23 meters + 0.04 watts = 💥 <!-- Mention that for precision propagation there isn't a single good answer, and that with multiplication between units then you need to support compound units. --> --- ## We need _at least_ two classes <div style="display: flex"> <div style="flex: 1 1 0; border-right: thin solid black"> **Decimal (normalized)** - `.add()`, `.subtract()`, ... - based on IEEE-754 limits - no tracking of precision </div><div style="flex: 1 1 0"> **Measure (with precision)** - backed by a decimal - no arithmetic - `.convertTo("meters")` - has a _static_ notion of precision </div></div> --- ## We need to be able to convert between them ```javascript measure.toDecimalObject(); Measure.from({ value: decimal, significantDigits: 3, unit: "liters" }); decimal.withUnit("liters", 3); ``` <div style="border: 3px solid red; max-width: 50%; padding: 8px; float: right; font-size: .5em">⚠️ All code examples at this point are pure speculation</div> --- ## Open questions --- ## All numeric types? Especially _if_ Measure will support custom units, there is desire for it to also support being backed by a BigInt. Do we need it? `BigIntMeasure`/`DecimalMeasure`/`NumberMeasure`, or a single `Measure` with a `.type: "bigint" | "decimal" | "number"` property? <div style="border: 3px solid red; max-width: 50%; padding: 8px; float: right; font-size: .5em">⚠️ All names at this point are pure speculation</div> --- ## Do we need "decimal with precision" class? <span style="display:inline-block;text-align:left"> <code>Decimal</code> <code>.withPrecision(3)</code><br> → <code>FullDecimal</code> <code>.withUnit("feet")</code><br> → <code>Measure</code> </span> It would not support aritmetic, to avoid picking a "blessed" precision propagation strategy. <div style="border: 3px solid red; max-width: 50%; padding: 8px; float: right; font-size: .5em">⚠️ All names at this point are pure speculation</div> --- ## Next steps --- ## Moving the proposals forward Decimal & measure overlap, but they are justified independently. * **Option 1:** Keep them as separate proposals, designed in tight collaboration. * **Option 2:** Merge the two proposals (during stage 1? later?) A hypothetical unified proposal readme: https://notes.igalia.com/tc39-unified-measure-and-decimal <!-- **Option 2.7:** Keep them as separate proposals, designed in tight collaboration, and move them together through the process. --> --- ## Proposing a different first step: Amount (Eemeli) - Consider "amount" instead of "measure" - Opaque value, with separate fields for dimension + precision - Solves all presented Measure use cases - Provides a decimal representation of numerical values for interchange - Initially: No operations, no conversions - Extensible with methods for operations & conversions, or a Decimal value --- ```typescript class Amount { unit?: string; currency?: string; significantDigits?: number; // Validates value fits into Decimal restrictions // Freezes self constructor(value: number | BigInt | string, options?) // Stringifies value as numerical string toString(): string // Defined in ECMA-402 toLocaleString(locales, options): string } ``` --- Discusion
{"type":"slide","slideOptions":{"transition":"slide","theme":"igalia","controlsLayout":"edges","slideNumber":"c/t"}}