59

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
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Apoorva Chikara
  • 8,277
  • 3
  • 20
  • 35
  • You mean, "it returns `-0`", right? – Pointy Dec 22 '21 at 14:10
  • Yes in the browser(-0), but when you run the code in the question it is `0`. – Apoorva Chikara Dec 22 '21 at 14:11
  • According to the language specification, -0 is not less than +0. What browser are you using? – Pointy Dec 22 '21 at 14:13
  • 4
    Interesting, can reproduce on Chrome. Also `Math.min(0, -0)` and `Math.min(-0, 0)` both return `-0`, so `Math.min` does differentiate those – Jonas Wilms Dec 22 '21 at 14:14
  • @Pointy I am using brave, even on chrome it shows me the same results. – Apoorva Chikara Dec 22 '21 at 14:15
  • 21
    _"When I run this code in stackoverflow execution context, it returns 0."_ - and if you check the browser console at the same time, there you will see `-0`. Stackverflows's "own" console inside these snippets behaves a bit different, than the real one. If you log `arr` as well, that gives `[0, 0, 0]` in the SO console, and `[0, 0, -0]` in the native browser console. – CBroe Dec 22 '21 at 14:15
  • I wanted to understand why -0 < 0, what is the logic behind it? I understand different JS env might behave differently. If we take chrome V8, how does it make -0 < 0 in the specification? – Apoorva Chikara Dec 22 '21 at 14:17
  • 1
    It's not. `-0 < 0`: false. `-0 > 0`: false. `-0 === 0`: true – Daniel Beck Dec 22 '21 at 14:18
  • @RandyCasburn yes it's pretty clear (a somewhat rare thing in the JavaScript spec), but that's not very explanatory. I don't doubt that some smart person had a good reason to make it work that way, but the rationale is not obvious to me. – Pointy Dec 22 '21 at 14:22
  • @Pointy without an explicit step for dealing with output in the case of [-0, 0] the return would be inconsistent (based on order passed?) – pilchard Dec 22 '21 at 14:26
  • @pilchard well if `-0 === +0` is true (noting that `0` *is* `+0`), I don't see how a result of `0` would be inconsistent in terms of general JavaScript behavior. Again, however, I'm not on the ECMAScript committee and I don't pretend to get the nuances behind things like this; I simply feel that it's surprising. Of course, the `-0` return value will behave "as normal" in subsequent comparisons etc. – Pointy Dec 22 '21 at 14:31
  • If we do -0 === 0 or -0 === +0 everything comes out to be true, the how something can be lesser. – Apoorva Chikara Dec 22 '21 at 14:33
  • @ApoorvaChikara well, I don't know why `Math.min()` was specified to work that way, but it was. The good news is that the `-0` return value should work just like `0` in almost everything else you do. – Pointy Dec 22 '21 at 14:34
  • 5
    there are other exceptions, `Object.is(-0, +0);` -> `false` and `1/0 === Infinity` -> `true` while `1/-0 === -Infinity` -> `true`. – pilchard Dec 22 '21 at 14:35
  • You know it's probably like that so that it's possible to *detect* `-0`, even though it's generally not really necessary to worry about it. It's kind-of like detecting neutrinos. – Pointy Dec 22 '21 at 14:35
  • 1
    That was my point about returning consistently @Pointy – pilchard Dec 22 '21 at 14:36
  • 4
    @Pointy the answer might also be in "IEEE 754 2019, §5.10", which defines both a comparison operation and a totalOrder ... Unfortunately this specification is behind a paywall – Jonas Wilms Dec 22 '21 at 14:51
  • @JonasWilms yes, it's *not* surprising that `-0` is less than `0` *conceptually*, because of the way IEE754 is designed for relational comparisons. I guess JavaScript (and most other languages, I guess, though I don't know for sure) makes the pragmatic choice of keeping the extreme nuisance of `-0` out of most ordinary computations, while still making it possible to find it in the rare cases that's necessary. – Pointy Dec 22 '21 at 14:54
  • Recommended reading: [Signed zero](https://en.wikipedia.org/wiki/Signed_zero) on Wikipedia, [Are +0 and -0 the same in JavaScript?](https://stackoverflow.com/q/7223359/1048572) on SO, and [JavaScript’s two zeros](https://2ality.com/2012/03/signedzero.html) on 2ality. – Bergi Dec 22 '21 at 22:44
  • 5
    @JonasWilms [Here you go](https://sci-hub.st/10.1109/IEEESTD.2019.8766229). Page 69, Section 9.6, "-0 compares less than +0". – 93Iq2Gg2cZtLMO Dec 23 '21 at 11:40
  • 2
    IMHO I would expect `1/Math.sign(Math.min(x, y))` to always be the same thing as `1/Math.sign(Math.min(y, x))`, which wouldn't be the case if `Math.min` returned, say, the first argument... then one might say: Why does `Math.sign` return `-0` for `-0`? In the end `+0` and `-0` **are** different because you can distinguish them, not only by the string representation, but also using `1/+0` or `1/-0` to get plus or negative infinity... – Bakuriu Dec 23 '21 at 15:12
  • `Math.min(1, 1)` is `1`. So with your logic, does it mean that `1 < 1`? – Eric Duminil Dec 24 '21 at 16:06
  • @bakuriu well said, except for the "not only by the string representation", which for JavaScript's ToString is indistinguishable (as one can see in the Snippet console, which performs a toString conversion) – Jonas Wilms Dec 28 '21 at 22:20
  • @93lq2Gg thanks, interesting finding, though unfortunately I would not include a link to such a third party mirror in one of my answers ... – Jonas Wilms Dec 28 '21 at 22:23
  • Very close question: https://stackoverflow.com/questions/7223359/are-0-and-0-the-same – Nikita Skrebets Jan 05 '22 at 23:42

4 Answers4

60

-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.

Holt
  • 36,600
  • 7
  • 92
  • 139
  • 7
    I agree, but this is somewhat odd, that `Math.min()` would have semantics different from those of the `<` operator. I would say that that definitely violates the principle of least surprise. – Pointy Dec 22 '21 at 14:18
  • @Pointy I do agree, I'm not saying it's not odd, I'm just saying that the observed behavior comes from the specification of `Math.min`, not from the specification of ordering between -0 and +0. – Holt Dec 22 '21 at 14:19
  • With the exception of [Math.min()](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.min) - check out the note. – Randy Casburn Dec 22 '21 at 14:20
  • 15
    it doesn't seem that odd to me, 0 is the only value whose - and + values are equal, so < is inadequate to return a consistent result. – pilchard Dec 22 '21 at 14:38
  • 3
    @pilchard That's a good point, it's probably better to ensure that `Math.min(x, y)` always equal `Math.min(y, x)` so that you do not have to bother about order of arguments. – Holt Dec 22 '21 at 14:42
  • 7
    As for why it's always been implemented like this and got specified in ES1: because [Java has the same behaviour](https://stackoverflow.com/q/67414007/1048572), and the `Math` object is basically taken from Java. – Bergi Dec 22 '21 at 22:49
  • 4
    @Pointy: If you had a min function defined only as `a – Peter Cordes Dec 23 '21 at 01:34
  • @PeterCordes yes, thank you for that observation. This has been a fascinating question, and while I've never been in a situation where I've had to worry about anything like `-0`, I understand now why it's necessary to have some mechanism to deal with that detail. – Pointy Dec 23 '21 at 02:53
  • @Bergi I guess that should be the actual answer to the question: ["Because JS is closer to Java than one might think"](https://stackoverflow.com/questions/245062/whats-the-difference-between-javascript-and-java) ... – Jonas Wilms Dec 23 '21 at 11:02
  • It's not just about `Math.min(x, y) === Math.min(y, x)` but also `Math.min(x, y) != Math.max(x, y)` where `x <> y` – Ben Dec 23 '21 at 16:17
  • 2
    But `-0 === 0`, so `Math.min(-0, 0) === Math.min(0, -0)` would hold even if we didn't have this exception. What this does do is guarantee that for example `1 / Math.min(-0, 0) === 1 / Math.min(0, -0)`. I don't know if that is the rationale, though. – Jasmijn Dec 23 '21 at 17:28
12

This is a specialty of Math.min, as specified:

21.3.2.25 Math.min ( ...args )

[...]

  1. 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.

  1. 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.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 8
    Wait, what? JS ToString is lossy and hides the sign of zero?? smh... – R.. GitHub STOP HELPING ICE Dec 22 '21 at 22:46
  • @R..GitHubSTOPHELPINGICE [yup](https://262.ecma-international.org/12.0/#sec-numeric-types-number-tostring), though usually such a string would be shown to a human, which (most likely) is unaware of the existence of a "negative zero". The information would then be lost anyways ;) – Jonas Wilms Dec 23 '21 at 10:48
  • @R..GitHubSTOPHELPINGICE How would you distinguish between -0 and 0 in code then, if the string values are equal? – leo848 Dec 28 '21 at 21:54
  • 2
    @leo848 `Object.is(-0, theValue)` ... Most other comparisons (including `===`) treat them as equal. – Jonas Wilms Dec 28 '21 at 22:11
9

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.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • You can use `Object.is` instead of `==` to compare with signed zero and `NaN`. – Bergi Dec 23 '21 at 07:59
  • @Bergi: thanks. I assume `Object.is` considers two NaNs with different payloads not-equal, like C `memcmp` would? (i.e. different mantissas, and/or different sign bits; IEEE 754 spends 2^53-1 encodings each on +-NaN for `double`, instead of doing gradual overflow or anything like that, only gradual underflow.) – Peter Cordes Dec 23 '21 at 08:03
  • 1
    No, in JS there is only a single `NaN` value without a payload, or [as the spec puts it](https://262.ecma-international.org/12.0/#sec-ecmascript-language-types-number-type): "*to ECMAScript code, all NaN values are indistinguishable from each other.*" – Bergi Dec 23 '21 at 08:08
  • ... as in general _there is no memory in ECMAScript_ ... The specification lives in a world far away from computers and circuits ... – Jonas Wilms Dec 23 '21 at 11:06
  • @JonasWilms: Not sure what your point is. The mantissa field of a floating-point number exists (and is the NaN payload if the exponent field is all 1s), whether you're using a memory-safe language like JS or not. If JS was going to let you get at it, obviously the API wouldn't be anything like C `memcpy`, that's just how an old, simple, non-memory-safe language like C does things. e.g. I could imagine an API like `Math.getmant(x)` to return the mantissa field as an integer-valued JS number. Or now that JS BigInt exists, some equivalent to C++20 `std::bit_cast( x )` could exist. – Peter Cordes Dec 23 '21 at 11:32
  • @JonasWilms: Of course it's a valid choice to just say that JS numbers don't have (accessible) NaN payloads, and all NaNs are indistinguishable. That's a different way to fully solve the problem, and probably a better choice for JS. (NaN payloads and signalling vs. quiet NaNs get extremely little use in real life even in systems where they're usable.) – Peter Cordes Dec 23 '21 at 11:33
  • 1
    @PeterCordes my point was that reading the specification is easier and more intuitive when not thinking about actual memory represetations or runtime implementations. In fact [there is also no mantissa](https://262.ecma-international.org/12.0/#sec-ecmascript-language-types-number-type) (well, there is _m_, to describe how numbers are normalized) and how numbers are stored is totally implementation specific. – Jonas Wilms Dec 23 '21 at 13:56
  • @JonasWilms: Thanks for the link. If I had taken the time to check the specification at all (for a language I barely use), I wouldn't have had to guess based on the fact that it uses IEEE754 doubles for numbers and has signed zero that it would also have NaN payloads. – Peter Cordes Dec 23 '21 at 14:01
0

The spec is curiously contradictory. The < comparison rule explicitly says that -0 is not less than +0. However, the spec for Math.min() says the opposite: if the current (while iterating through the arguments) value is -0, and the smallest value so far is +0, then the smallest value should be set to -0.

I would like for somebody to activate the T.J. Crowder signal for this one.

edit — it was suggested in some comments that a possible reason for the behavior is to make it possible to detect a -0 value, even though for almost all purposes in normal expressions the -0 is treated as being plain 0.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 8
    While pedantic in the extreme, I would argue that it is not *contradictory*. If a = Math.min(a, b), it is *not* true in general that a < b. It *is* true that a <= b. Furthermore it is perhaps desirable that Math.min(a,b) = Math.min(b,a). In that case, Math.min() must choose an ordering for distinct objects a, b for which a == b. This ordering need not implicate the less than operator. – President James K. Polk Dec 22 '21 at 14:37
  • @PresidentJamesK.Polk yes Mr President I just added a note to the answer. The ability to find a `-0` is probably valuable in some cases, and the semantics of the relational operators makes that difficult if these other mechanisms didn't provide that ability. (Of course, checking that `Math.min()` has returned `-0` is itself a conundrum, but clearly it can be done at least.) – Pointy Dec 22 '21 at 14:40
  • (copying my comment here for future readers) if you had a min function defined only as `a – Peter Cordes Dec 23 '21 at 03:02
  • If you want a simple operation that works the way you describe, only updating your current_min if a `<` comparison is true, use `current_min = x < current_min ? x : current_min`. That will ignore all NaN, and also ignore `-0` for current_min >= 0. And 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. Being commutative wrt. +-0 seems like a sensible decision, especially since we already want it to be NaN propagating and thus not equivalent to `a – Peter Cordes Dec 23 '21 at 03:06
  • (Correction to my earlier; [C `fmin`](https://en.cppreference.com/w/c/numeric/math/fmin) and `fmax` *avoid* progating NaN, always returning the non-NaN if there is one.) – Peter Cordes Dec 23 '21 at 03:19
  • @PeterCordes again, thank you for all that helpful information, but I will also add that in over 20 years of writing *lots* of JavaScript software I've never had to even think about `-0` once. I'm sure there are some avenues of application software effort that do require it, of course. – Pointy Dec 23 '21 at 03:26
  • Ended up turning those comments into an answer. And yeah, most people never have to think about signed zero. Languages like JS that want to fully define behaviour everywhere do need to consider that case, though. – Peter Cordes Dec 23 '21 at 03:59
  • The fact that `Math.min` returns −0 in preference to +0 is not an assertion that −0 is less than +0 and hence does not contradict the result of `<`. For example, if we had a format that could represent 5 as `5` or `05` or `005` and `Math.min` were specified to return (in the absence of any mathematically lower value in the input) the one with the fewest leading zeros, this would simply be a preference about representation, not an assertion that `5` is less than `005`. – Eric Postpischil Dec 23 '21 at 12:03
  • @EricPostpischil I don't mean to make a pointless argument, but one "feature" of IEEE 754 representation is that relational comparisons can be made between values using ordinary *integer* comparison operations. In that sense, `-0` is in fact "less than" `0`, because it looks like a negative integer value, while `0` looks like `0`. – Pointy Dec 23 '21 at 12:34
  • @Pointy: IEEE-754 formats cannot be compared by using integer compares. In binary formats, the positive floating-point numbers are in ascending order by ascending integer interpretation, but the negative numbers are in descending order by ascending two’s complement integer interpretation. To use an integer compare, you need to make an adjustment. Whether that adjustment uses a one’s complement or two’s complement would select whether −0 and +0 are distinguished or not. For the decimal formats, there are multiple representations of numbers, and integer compares are generally useless. – Eric Postpischil Dec 23 '21 at 13:43
  • Yes if both values are negative then the sense of `<` or `>` must be reversed, that is true. – Pointy Dec 23 '21 at 13:45
  • @Pointy: In any case, none of the semantics specified in IEEE-754 are contingent upon the integer representations; it is irrelevant to the purposes for which −0 and +0 are distinguished. – Eric Postpischil Dec 23 '21 at 13:45