I Still Dont Know JS
Some days, I get complacent. So complacent, in fact, that I let myself slip into thinking I know Javascript. If this fact alone doesn't make you sigh, well, thanks for your understanding. Here was today's unexpected adventure.
What's wrong with the code?
/**
* Given a number, decrease it by 1, or stop it at the specified min.
* @param {number} value - The value to decrease
* @param {number} min - The limit which the value can reach. Defaults to Number.MIN_VALUE.
* @returns {number} - Either value -1 or min.
*/
function decrement(value, min = Number.MIN_VALUE) {
const nextValue = value - 1;
return Math.max(nextValue, min);
}
This code is not trying to be sneaking or misleading at all. It takes one of the existing value. If it finds that min
value is larger than nextValue
it returns min
. Otherwise it returns nextValue
. Here are some examples:
decrement(10, 5); // 9 OK
decrement(-100, -101) // -101 OK
decrement(-101, -101) // -101 OK
So far, so good. Don't forget to test that default argument:
decrement(10) // 9 OK
decrement(-100) // 5e-324 OK wait what?
decrement(-1) // 5e-324 :
decrement(0) // 5e-324 Oh no...
What can we gather from this? Well, first thing is that it seems like this oddball result is the same. That means we get the same branch on each run. What else can we tell?
decrement(0.9999) // 5e-324 Bad
decrement(1.9999) // 0.9999 OK
decrement(1.0001) // 0.00009999999999998899 OK
decrement(1) // 5e-324 Bad
We can see when our function maps to a number <= 0 that we get this funny result. Huh...
So what's happening? MDN, tell me about MAX_VALUE
!:
The
Number.MAX_VALUE
property represents the maximum numeric value representable in JavaScript....
The
MAX_VALUE
property has a value of approximately1.79E+308
.
OK, that sounds right. So naturally MIN_VALUE
is the opposite?
The
Number.MIN_VALUE
property represents the smallest positive numeric value representable in JavaScript....
The
MIN_VALUE
property is the number closest to0
, not the most negative number, that JavaScript can represent.MIN_VALUE
has a value of approximately5e-324
. Values smaller thanMIN_VALUE
(“underflow values”) are converted to0
.
It turns out that my misunderstanding came from not thinking hard enough about how floats are represented.
For example, Java has the same implementation of these constants.
Interestingly, C# chose to assign MIN_VALUE
to the truly smallest value you can represent with 64-bit floats and instead created the more by-the-spec constant epsilon to determine what the smallest possible granularity you could have is.
Not what I would have expected intuitively, though I see the reasoning behind it.
Anyway, the solution we landed on for our situation was to default the bound from Number.MIN_VALUE
to Number.MIN_SAFE_INTEGER
. From MDN one more time:
The
Number.MIN_SAFE_INTEGER
constant represents the minimum safe integer in JavaScript (-(2^53 - 1)
)....
The reasoning behind that number is that JavaScript uses double-precision floating-point format numbers as specified in IEEE 754 and can only safely represent numbers between
-(2^53 - 1)
and2^53 - 1
.
The resulting code
/**
* Given a number, decrease it by 1, or stop it at the specified min.
* @param {number} value - The value to decrease
* @param {number} min - The limit which the value can reach. Defaults to Number.MIN_VALUE.
* @returns {number} - Either value -1 or min.
*/
function decrement(value, min = Number.MIN_SAFE_INTEGER) {
const nextValue = value - 1;
return Math.max(nextValue, min);
}
decrement(10) // 9 OK
decrement(-100) // -101 OK
decrement(-1) // -2 OK
decrement(0) // -1 Woohoo!
So, in conclusion, I still don't know JS, but I really don't know floats either. Woof.