Skip to content

Commit 6b75abd

Browse files
committed
Begin working on a unified README for combining decimal and measure
1 parent da8a57b commit 6b75abd

File tree

1 file changed

+390
-0
lines changed

1 file changed

+390
-0
lines changed

unified-readme.md

+390
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
# Ecma TC39 Decimal & Measure Proposal
2+
3+
**Stage**: 1
4+
5+
**Champions**:
6+
7+
- Andrew Paprocki (Bloomberg)
8+
- Ben Allen (Igalia)
9+
- Jesse Alama (Igalia)
10+
- Jirka Maršík (Oracle)
11+
12+
**Authors**: Ben Allen, Jesse Alama, Waldemar Horwat
13+
14+
## Overview
15+
16+
The Decimal & Measure proposal aims to add two key capabilities to JavaScript:
17+
18+
1. Exact decimal arithmetic
19+
2. Representation of measurements with units and precision tracking
20+
21+
Currently, JavaScript developers face several challenges when working with numbers and measurements:
22+
23+
- Binary floating-point numbers (JavaScript's `Number` type) cannot exactly represent many decimal values, with errors propagating in arithmetic
24+
- No built-in way to track precision in measurements
25+
- No support for handling units and conversions between them
26+
- Difficulty in properly localizing measurements and preserving any underlying numerical precision
27+
28+
## Use Cases and Goals
29+
30+
### Exact Decimal Arithmetic and Financial Calculations
31+
32+
Key needs:
33+
34+
- Exact representation of decimal numbers
35+
- Preservation of trailing zeros when needed
36+
- Currency calculations without rounding errors
37+
- Data exchange with financial systems
38+
39+
Why JavaScript?
40+
41+
- Modern web applications should be able to handle financial calculations client-side for better interactivity
42+
- Growing use of JavaScript in financial backend systems (Node.js, Deno)
43+
- Serverless architectures often require JavaScript for financial logic
44+
- Need for consistent decimal handling across full-stack JavaScript applications, especially in a microservice setup
45+
46+
### Measurements with units
47+
48+
Key needs:
49+
50+
- Physical measurements with proper unit handling
51+
- Precision tracking
52+
- Unit conversions
53+
- Localization of measurements
54+
55+
Why JavaScript?
56+
57+
- Web interfaces increasingly handle unit conversions client-side for responsive UX
58+
- Scientific/technical web applications need to handle measurements
59+
- Cross-platform applications need consistent measurement handling, including fidelity with the underlying numeric value
60+
61+
### Data Exchange
62+
63+
Key needs:
64+
65+
- Preserving exact decimal values when communicating with external systems
66+
- Maintaining precision information in data pipelines
67+
- Round-trip compatibility with databases and APIs
68+
69+
Why JavaScript?
70+
71+
- JavaScript apps consuming and produce a lot of API data
72+
- Modern architectures often involve JavaScript microservices
73+
- Need to maintain numeric precision when interfacing with databases
74+
- Growing use of JavaScript for ETL (Extract, Transform, Load) processes
75+
76+
### Scientific and Technical Calculations
77+
78+
Key needs:
79+
80+
- Support for many significant digits, with verifiable digit-by-digit correctness
81+
- Unit conversions (e.g., feet to meters)
82+
- Precision tracking in calculations
83+
84+
Why JavaScript?
85+
86+
- Enabling JavaScript in scientific web applications, such as educational software or browser-based simulation and modeling tools
87+
- Need for scientific calculations in educational software
88+
- Web-based data visualization tools handling scientific data
89+
90+
## Design
91+
92+
We propose two new classes: `Decimal`, for storing exact decimal data, and `Measure`, for storing a number (not necessarily a JS `Number`), along with an optional unit and precision.
93+
94+
### Decimal Class
95+
96+
The `Decimal` class represents exact decimal values using a fixed 128-bit representation based on IEEE 754 Decimal128, with values are always normalized (that is, precision/quantum is not tracked, so that trailing zeros aren't supported).
97+
98+
With this model, we propose to support NaN, positive and negative infinity, and -0. This is to (1) ensure maximum compatibility with other systems that might also be based on IEEE 754. (However, as with JS's `Number`, we intend to support just *one* decimal NaN rather than boxed NaNs), and (2) advanced uses
99+
100+
#### API
101+
102+
##### Arithmetic
103+
104+
- absolute value
105+
- negation (sign switch)
106+
- addition and subtraction
107+
- multiplication and division
108+
109+
We also support rounding, with all five IEEE 754 rounding modes (round-ties-to-even being the default, as with `Number` and the various mathematical operations in `Math`).
110+
111+
##### Comparisons
112+
113+
- `lessThan`
114+
- `equals`
115+
116+
Becuase of NaN pollution, we also include:
117+
118+
- `notEqual`
119+
- `greaterThan`
120+
- `lessThanOrEqual`
121+
- `greaterThanOrEqual`
122+
123+
##### Serialization
124+
125+
- `toString(): string`
126+
- `toFixed(numDigits: number): string`
127+
- `toPrecision({ precision?: number})`
128+
- `toLocaleString(locale?: string): string`
129+
130+
#### Examples
131+
132+
```javascript
133+
const price = new Decimal("1234.50");
134+
console.log(price.toString()); // "1234.5" // Note: normalized
135+
console.log(price.toLocaleString("de-DE")); // "1.234,5"
136+
```
137+
138+
### Measure Class
139+
140+
The `Measure` class represents values with units and, optionally, precision. It can track precision either through fractional digits or significant digits.
141+
142+
```javascript
143+
interface MeasureOptions {
144+
unit?: string;
145+
precision?: number;
146+
precisionType?: 'fractionalDigits' | 'significantDigits';
147+
exponent?: number;
148+
usage?: string;
149+
}
150+
```
151+
152+
sets us up to discuss how to construct measurements:
153+
154+
```javascript
155+
class Measure {
156+
constructor(value: Decimal | string | number, options?: MeasureOptions)
157+
158+
// Get underlying mathematical value (normalized)
159+
getValue(): Decimal
160+
161+
// Formatting
162+
toString(): string
163+
toLocaleString(
164+
locales?: string | string[],
165+
options?: Intl.NumberFormatOptions & {
166+
unit?: 'long' | 'short' | 'narrow',
167+
localeMeasurementSystem?: boolean
168+
}
169+
): string
170+
}
171+
```
172+
173+
## Extended Examples
174+
175+
### Decimal Formatting and Localization
176+
177+
```javascript
178+
const price = new Decimal("1234.50");
179+
180+
// Basic locale formatting
181+
console.log(price.toLocaleString("en-US")); // "1,234.5"
182+
console.log(price.toLocaleString("de-DE")); // "1.234,5"
183+
console.log(price.toLocaleString("zh-CN")); // "1,234.5"
184+
185+
// Currency formatting
186+
console.log(
187+
price.toLocaleString("en-US", {
188+
style: "currency",
189+
currency: "USD",
190+
}),
191+
); // "$1,234.50"
192+
```
193+
194+
### Measurements with Precision and Pluralization
195+
196+
```javascript
197+
// Product ratings showing precision affects pluralization
198+
const weights = [
199+
new Measure("1", { unit: "kilogram" }),
200+
new Measure("1.0", { unit: "kilogram" }),
201+
new Measure("2.0", { unit: "kilogram" }),
202+
new Measure("4.5", { unit: "kilogram" }),
203+
];
204+
```
205+
206+
Precision affects pluralization:
207+
208+
```javascript
209+
console.log(weights.map((r) => r.toLocaleString("en-US")));
210+
// [
211+
// "1 kilogram", // singular
212+
// "1.0 kilograms", // plural due to decimal
213+
// "2.0 kilograms", // plural
214+
// "4.5 kilograms" // plural
215+
// ]
216+
```
217+
218+
### Scientific and Technical Measurements
219+
220+
Consider an example about temperature, using different precision types.
221+
222+
```javascript
223+
const temperatures = [
224+
new Measure("22.50", {
225+
unit: "celsius",
226+
precision: 2, // fractional digits (the default)
227+
}),
228+
new Measure("295.65", {
229+
unit: "kelvin",
230+
precision: 4,
231+
precisionType: "significantDigits", // <--***--<
232+
}),
233+
];
234+
235+
// Locale-aware temperature formatting
236+
console.log(temperatures[0].toLocaleString("en-US")); // "72.50°F"
237+
```
238+
239+
### Complex Calculations Preserving Precision
240+
241+
```javascript
242+
// Calculate total cost including tax with specific precision
243+
const items = [
244+
{ price: new Decimal("29.95"), quantity: 2 },
245+
{ price: new Decimal("9.99"), quantity: 1 },
246+
{ price: new Decimal("0.50"), quantity: 5 },
247+
];
248+
249+
const taxRate = new Decimal("0.0725"); // 7.25% tax
250+
251+
// Calculate subtotal
252+
const subtotal = items.reduce(
253+
(sum, item) => sum.add(item.price.multiply(new Decimal(item.quantity))),
254+
new Decimal("0"),
255+
);
256+
257+
// Calculate tax and total
258+
const tax = subtotal.multiply(taxRate);
259+
const total = subtotal.add(tax);
260+
261+
// Create measure objects for formatting
262+
const totalMeasure = new Measure(total, {
263+
precision: 2,
264+
unit: "USD",
265+
});
266+
267+
console.log(totalMeasure.toLocaleString("en-US")); // "$77.64"
268+
```
269+
270+
## Design Rationale
271+
272+
### Fixed Bit-Width Representation
273+
274+
The proposal uses IEEE 754 Decimal128 as its underlying representation for several reasons:
275+
276+
1. It provides sufficient precision for most real-world use cases (up to 34 significant decimal digits)
277+
2. Established standard with well-defined semantics (JS already builds on IEEE 754)
278+
3. Implementations exist in hardware and software, including some out-of-the-box compiler support
279+
4. Reasonable memory footprint compared to arbitrary-precision alternatives
280+
5. Backstopped by a maximum amount of data, preventing complex calculations from consuming too many resources
281+
282+
### Precision Handling
283+
284+
Precision is handled exclusively by the `Measure` class, which can represent it in two ways:
285+
286+
1. Fractional digits (default) - e.g., "1.20" has 2 fractional digits
287+
2. Significant digits - e.g., "1200" with 3 significant digits becomes "120e+1"
288+
289+
This design separates concerns:
290+
291+
- `Decimal` handles exact mathematical values
292+
- `Measure` handles units and precision tracking
293+
294+
### Alternative Considered: Three-Class Design
295+
296+
We considered an alternative design with three distinct classes: `Measure` and `Decimal`, as above, together with `Decimal128`, which is a representation of "full" (non-normalized) IEEE 754 Decimal128. By "non-normalizing" we mean data that encapsulates both a mathematical number and (following the terminology of IEEE 754) a quantum (intuitively understood as "precision"):
297+
298+
```javascript
299+
class Decimal128 {
300+
constructor(value: string | number)
301+
302+
// Returns precision based on trailing zeros
303+
getPrecision(): number // e.g., "1.30" -> -2
304+
305+
toString(): string // Preserves trailing zeros
306+
toLocaleString(locale?: string): string
307+
}
308+
```
309+
310+
Here's how the three types could interact.
311+
312+
```javascript
313+
// Decimal128 preserves trailing zeros
314+
const d128 = new Decimal128("1.30");
315+
console.log(d128.toString()); // "1.30"
316+
console.log(d128.getPrecision()); // -2 (recall: power of 10)
317+
318+
// Decimal normalizes
319+
const d = new Decimal("1.30");
320+
console.log(d.toString()); // "1.3"
321+
console.log(d.toDecimal128().toString()); // "1.3" // No trailing zero
322+
323+
// Measure can work with either type:
324+
const m1 = new Measure(new Decimal128("1.30"), { unit: "meter" });
325+
console.log(m1.getValue().toString()); // "1.30"
326+
console.log(m1.getNormalizedValue().toString()); // "1.3"
327+
328+
const m2 = new Measure(new Decimal("1.30"), { unit: "meter" });
329+
console.log(m2.getValue().toString()); // "1.3"
330+
```
331+
332+
This three-class approach would provide explicit separation between:
333+
334+
1. Full IEEE 754 Decimal128 values with precision (`Decimal128`)
335+
2. Normalized mathematical values (`Decimal`)
336+
3. Measurements with units (`Measure`)
337+
338+
It is unclear whether `Decimal128` should support arithmetic. We envision it largely as a contain for a number plus a precision. Although IEEE 754 does specify how the quanta of the arguments of mathematical operations determines the quantum of the result, it is difficult to understand these rules. On the one hand, one could provide these operations and simply follow IEEE 754, for the simple reason that IEEE 754 is a well-known standard. On the other hand, we believe that following these rules would be strange to JS programmers.
339+
340+
While this design offers more explicit control over precision and normalization, we opted for the two-class design because it has a simpler mental model (fewer types to learn and understand). Also, in the two-class approach, precision handling naturally belongs with measurement. The two-class approach has a reduced API surface area and fewer conversion paths, a clearer separation between mathematical values and measured quantities, and many use cases don't require the distinction between normalized and non-normalized decimal values outside of measurements.
341+
342+
That said, we don't consider these down arguments. We are open to the three-class approach.
343+
344+
## Emerging Trends and JavaScript's Expanding Role
345+
346+
The need for precise decimal and measurement handling in JavaScript is driven by several key trends in software development:
347+
348+
### 1. Evolution of Web Applications
349+
350+
Traditional web applications often deferred precise calculations to the server, treating JavaScript as a simple display layer. Modern applications are different. For instance, in JS-rich applications, the JS programmer and the enduser can reasonably expect, e.g., real-time price calculations with tax and shipping, dynamic unit conversions as users input values, immediate feedback on financial calculations. In offline settings, progressive beb apps need to handle calculations without server access.
351+
352+
### 2. JavaScript Beyond the Browser
353+
354+
JavaScript's role has expanded significantly. Server-side processing, in JS, means that JS needs to be able to do what, previously, languages that could properly handle decimals used to do. Moreover, whether frontend or backend, a JS-in-the-middle setup means that data exchange is critical. A JS application may have direct interaction with decimal types in (e.g.) PostgreSQL or MongoDB and need to preserve precision when reading/writing data, along with consistent handling of measurements across storage and application layers.
355+
356+
### 3. Modern Development Practices
357+
358+
Contemporary software practices are pushing more responsibility to JavaScript. Edge computing, for instance, means that calculations are moving closer to the user. In microservice architectures, there is an increased need for data fidelity as data moves among many systems.
359+
360+
- Measurement processing in distributed systems
361+
362+
## Implementation Considerations
363+
364+
### Internationalization
365+
366+
Both classes integrate with JavaScript's internationalization APIs:
367+
368+
- `Intl.NumberFormat` for number formatting
369+
- `Intl.PluralRules` for unit pluralization
370+
- Locale-aware unit conversion
371+
372+
### Performance
373+
374+
The fixed 128-bit representation allows for efficient implementation:
375+
376+
- Predictable memory usage (all values are 128 bits)
377+
- Potential hardware acceleration (though this remains rare)
378+
- Well-understood performance characteristics, even in the face of complex calculations
379+
380+
### Limitations
381+
382+
We do not argue that Decimal128, whether normalized or not, is an ideal solution. That title, perhaps, resides with rational numbers, but exponential growth of numerator and denominator, possibly moderated by repeated greated common divisor applications, makes rational numbrers a heavy solution.
383+
384+
- Maximum precision of 34 decimal digits (this is enough for a very large range of use cases, but some use cases for even more digits are conceivable)
385+
- No direct support for arbitrary-precision calculations
386+
- Unit conversion limited to known unit types (we wish to follow those appearing in units.xml, as well as known currencies)
387+
388+
## Open questions
389+
390+
- If we allow Decimal objects to be used to construct measures, what should we do with data like NaN, -0, and infinities (which will be supported by Decimal)?

0 commit comments

Comments
 (0)