Skip to content

Commit 8090b3e

Browse files
committed
types-grammar, ch2: correcting inaccurate discussion around 'Number.EPSILON' and dealing with floating-point skew
1 parent a1d8ddf commit 8090b3e

File tree

1 file changed

+30
-7
lines changed

1 file changed

+30
-7
lines changed

types-grammar/ch2.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -671,15 +671,17 @@ The temptation to make fun of JS for `0.1 + 0.2 !== 0.3` is strong, I know. But
671671
| :--- |
672672
| Pretty much all programmers need to be aware of IEEE-754 and make sure they are careful about these kinds of gotchas. It's somewhat amazing, in a disappointing way, how few of them have any idea how IEEE-754 works. If you've taken your time reading and understanding these concepts so far, you're now in that rare tiny percentage who actually put in the effort to understand the numbers in their programs! |
673673

674-
One way to work around such floating-point imprecision is this *very small* `number` value:
674+
#### Epsilon Threshold
675+
676+
A common piece of advice to work around such floating-point imprecision uses this *very small* `number` value defined by JS:
675677

676678
```js
677679
Number.EPSILON; // 2.220446049250313e-16
678680
```
679681

680-
*Epsilon* is defined as the smallest difference JS can represent between `1` and the next value greater than `1`. While this value is implementation/platform dependent, it's typically about `2.2E16`, or `2^-52`. This value is the maximum amount of floating-point representation error (as discussed earlier), so it represents the threshold above which two values are *actually* different rather just skewed by floating-point error.
682+
*Epsilon* is the smallest difference JS can represent between `1` and the next value greater than `1`. While this value is technically implementation/platform dependent, it's generally about `2.2E-16`, or `2^-52`.
681683

682-
Thus, `Number.EPSILON` can used as a *very small* tolerance value to ensure number comparisons are *safe*:
684+
To those not paying close enough attention to the details here -- including my past self! -- it's generally assumed that any skew in floating point precision from a single operation should never be greater than `Number.EPSILON`. Thus, in theory, we can use `Number.EPSILON` as a *very small* tolerance value to ensure number equality comparisons are *safe*:
683685

684686
```js
685687
function safeNumberEquals(a,b) {
@@ -693,17 +695,36 @@ point3b = 0.3;
693695
safeNumberEquals(point3a,point3b); // true
694696
```
695697

696-
Since JS cannot represent a difference between two values smaller than this `Number.EPSILON`, it should be safe to treat any two number values as "equal" (indistinguishable in JS, anyway) if their difference is less than `Number.EPSILON`.
697-
698698
| WARNING: |
699699
| :--- |
700-
| If your program needs to deal with smaller values than `2^-52`, or more specifically, smaller differences between values, you should absolutely *not use* the JS `number` value-type. There are decimal-emulation libraries that can offer arbitrary (small or large) precision. Or pick a different language than JS. |
700+
| In the first edition "Types & Grammar" book, I indeed recommended exactly this approach. I was wrong. I should have researched the topic more closely. |
701+
702+
But, it turns out, this approach isn't safe at all:
703+
704+
```js
705+
point3a = 10.1 + 0.2;
706+
point3b = 10.3;
707+
708+
safeNumberEquals(point3a,point3b); // false :(
709+
```
710+
711+
Well... that's a bummer!
712+
713+
Unfortunately, `Number.EPSILON` only works as a "safely equal" error threshold for certain small numbers/operations, and in other cases, it's far too small, and yields false negatives.
714+
715+
You could scale `Number.EPSILON` by some factor to produce a larger threshold that avoids false negatives but still filters out all the floating point skew in your program. But what factor to use is entirely a manual judgement call based on what magnitude of values, and operations on them, your program will entail. There's no automatic way to compute a reliable, universal threshold.
716+
717+
Unless you really know what you're doing, you should just *not* use this `Number.EPSILON` threshold approach at all.
718+
719+
| TIP: |
720+
| :--- |
721+
| If you'd like to read more details and solid advice on this topic, I highly recommend reading this post. [^EpsilonBad] But if we can't use `Number.EPSILON` to avoid the perils of floating-point skew, what do we do? If you can avoid floating-point altogether by scaling all your numbers up so they're all whole number integers (or bigints) while performing math, do so. Only deal with decimal values when you have to output/represent a final value after all the math is done. If that's not possible/practical, use an arbitrary precision decimal emulation library and avoid `number` values entirely. Or do your math in another external programming environment that's not based on IEEE-754. |
701722

702723
### Numeric Comparison
703724

704725
Like strings, number values can be compared (for both equality and relational ordering) using the same operators.
705726

706-
Remember that no matter what form the number value takes when being specified as a literal (base-10, octal, hexadecimal, exponential, etc), the underlying value stored is what will be compared. Also keep in mind the floating point imprecision issues discussed in the previous section, as the comparisons will be sensitive to the exact binary contents, even if the difference between two numbers is much smaller than the `Number.EPSILON` threshold.
727+
Remember that no matter what form the number value takes when being specified as a literal (base-10, octal, hexadecimal, exponential, etc), the underlying value stored is what will be compared. Also keep in mind the floating point imprecision issues discussed in the previous section, as the comparisons will be sensitive to the exact binary contents.
707728

708729
#### Numeric Equality
709730

@@ -1046,3 +1067,5 @@ The story doesn't end here, though. Far from it! In the next chapter, we'll turn
10461067
[^StrictEquality]: "7.2.16 IsStrictlyEqual(x,y)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-isstrictlyequal ; Accessed August 2022
10471068

10481069
[^LooseEquality]: "7.2.15 IsLooselyEqual(x,y)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-islooselyequal ; Accessed August 2022
1070+
1071+
[^EpsilonBad]: "PLEASE don't follow the code recipe in the accepted answer", Stack Overflow; Daniel Scott; July 2019; https://stackoverflow.com/a/56967003/228852 ; Accessed August 2022

0 commit comments

Comments
 (0)