Description
The README throws around the word "exact". It discusses how 0.1 as a binary float is not actually 0.1 in memory. It states, "Statistically, most human-authored decimal numbers cannot be exactly represented as a binary floating-point number (AKA float)."
These are true statements.
However, floating point numbers do have a canonical decimal equivalent. This is easy to see, because all decimal numbers with less than approximately 15 significant digits have a unique, one-to-one mapping with a binary float, an equivalence relation. (For this post, I'll assume that f64 has enough significant digits to be sufficient for the use cases Decimal seeks to cover. This constraint can be relaxed, but it is not relevant to the main point of this thread.) This behavior is exposed already in JavaScript today via string conversion.
String(Number("0.12345"))
The algorithms for binary-to-decimal and decimal-to-binary are well researched, efficient, continue to evolve, and are already implemented in major browsers.
What does this mean?
The most common argument I see for the footgun of float arithmetic is this:
0.1 + 0.2
// ==> 0.30000000000000004
I understand that this is surprising behavior. However, there are ways to solve this problem without using decimals as the numeric type.
Rounding More Often
For most operations, it's totally fine and expected to round more often, which will essentially take the binary float and make it the expected decimal equivalent.
This is currently accessible in the language via Number.prototype.toPrecision
, which returns the string form of the equivalence relation.
Number((0.1 + 0.2).toPrecision(2)) === 0.3 // true
// 0.3
More developers should do this, and we should improve documentation and introductory computer science classes.
We could even add a helper function that behaves like toPrecision
but returns a Number
.
Number.prototype.decimalAdd
Alternatively, it is also possible to write a function that works like this:
(0.1).decimalAdd(0.2) === 0.3 // true
or even invent a new operator to make it look nicer
0.1 +~ 0.2 === 0.3 // true
The contract of Number.prototype.decimalAdd
is: add the binary floats as if adding their decimal equivalent, and return the binary float corresponding to the result.
I claim that this can be implemented efficiently. If the efficiency of this operation is the crux, I'm happy to go into this further.
My point is that the concept of "exactness" does not imply that Decimal128 is required as a numeric data type. Exactness can be achieved with Float64 in both representation (via the equivalence relation) and arithmetic (via either developer education or helper functions on Number).