Why does Math.min() return -0 from [+0, 0, -0]
I know (-0 === 0) comes out to be true. I am curious to know why -0 < 0 happens?
When I run this code in stackoverflow execution context, it returns 0
.
const arr = [+0, 0, -0];
console.log(Math.min(...arr));
But when I run the same code in the browser console, it returns -0
. Why is that? I have tried to search it on google but didn't find anything useful. This question might not add value to someone practical example, I wanted to understand how does JS calculates it.
const arr = [+0, 0, -0];
console.log(Math.min(...arr)); // -0
Solution 1:
-0
is not less than 0
or +0
, both -0 < 0
and -0 < +0
returns False
, you're mixing the behavior of Math.min
with the comparison of -0
with 0
/+0
.
The specification of Math.min
is clear on this point:
b. If number is -0𝔽 and lowest is +0𝔽, set lowest to -0𝔽.
Without this exception, the behavior of Math.min
and Math.max
would depend on the order of arguments, which can be considered an odd behavior — you probably want Math.min(x, y)
to always equal Math.min(y, x)
— so that might be one possible justification.
Note: This exception was already present in the 1997 specification for Math.min(x, y)
, so that's not something that was added later on.
Solution 2:
This is a specialty of Math.min
, as specified:
21.3.2.25 Math.min ( ...args )
[...]
- For each element number of coerced, do
a. If number is NaN, return NaN.
b. If number is -0𝔽 and lowest is +0𝔽, set lowest to -0𝔽.
c. If number < lowest, set lowest to number.
- Return lowest.
Note that in most cases, +0 and -0 are treated equally, also in the ToString conversion, thus (-0).toString()
evaluates to "0"
. That you can observe the difference in the browser console is an implementation detail of the browser.
Solution 3:
The point of this answer is to explain why the language design choice of having Math.min
be fully commutative makes sense.
I am curious to know why -0 < 0 happens?
It doesn't really; <
is a separate operation from "minimum", and Math.min
isn't based solely on IEEE <
comparison like b<a ? b : a
.
That would be non-commutative wrt. NaN as well as signed-zero. (<
is false if either operand is NaN, so that would produce a
).
As far as principle of least surprise, it would be at least as surprising (if not moreso) if Math.min(-1,NaN)
was NaN
but Math.min(NaN, -1)
was -1
.
The JS language designers wanted Math.min
to be NaN-propagating, so basing it just on <
wasn't possible anyway. They chose to make it fully commutative including for signed zero, which seems like a sensible decision.
OTOH, most code doesn't care about signed zero, so this language design choice costs a bit of performance for everyone to cater to the rare cases where someone wants well-defined signed-zero semantics.
If you want a simple operation that ignores NaN in an array, iterate yourself with current_min = x < current_min ? x : current_min
. That will ignore all NaN, and also ignore -0
for current_min <= +0.0
(IEEE comparison). Or if current_min
starts out NaN, it will stay NaN. Many of those things are undesirable for a Math.min
function, so it doesn't work that way.
If you compare other languages, the C standard fmin
function is commutative wrt. NaN (returning the non-NaN if there is one, opposite of JS), but is not required to be commutative wrt. signed zero. Some C implementations choose to work like JS for +-0.0 for fmin
/ fmax
.
But C++ std::min
is defined purely in terms of a <
operation, so it does work that way. (It's intended to work generically, including on non-numeric types like strings; unlike std::fmin
it doesn't have any FP-specific rules.) See What is the instruction that gives branchless FP min and max on x86? re: x86's minps
instruction and C++ std::min
which are both non-commutative wrt. NaN and signed zero.
IEEE 754 <
doesn't give you a total order over distinct FP numbers. Math.min
does except for NaNs (e.g. if you built a sorting network with it and Math.max
.) Its order disagrees with Math.max
: they both return NaN if there is one, so a sorting network using min/max comparators would produce all NaNs if there were any in the input array.
Math.min
alone wouldn't be sufficient for sorting without something like ==
to see which arg it returned, but that breaks down for signed zero as well as NaN.