As usual, I like to delve into some of the weird or complicated bits of JS. The other week I was thinking about a way to possibly identify a negative zero.
Some of you may be taken aback. What is a negative zero? Can zero even be negative? It can according to the IEEE! This is a standard used for almost all languages in their number systems. Thusly, a lot of the "weirdness" in JS number systems is actually standard across other languages, too!
Although, how would we know if the input we were receiving was -0
or 0
? Does it matter? Not really, but it's a fun exercise.
Let's first create the rules. We have a function called isZeroNegative
which accepts a single parameter. We must return true
if the zero is negative and false
if it is a positive zero. We can write whatever code we want inside the function.
function isZeroNegative(zero) {
// some code
return false; // default return
}
Our Options
It seems pretty simple, but you will find it a bit challenging. You might first think to check if it's below 0
, as that's usually the definition of a negative number, but this won't do. The check will return false
even though it is negative. -0 < 0 === false
.
Math.sign()
might be your next guess. Its very purpose, after all, is to let us know whether a number is negative or not. If you pass a negative number, it will return -1
and 1
if it is positive. Unfortunately, it will return the same zero we passed in if the number is a zero, whether negative or positive. Math.sign(-0) === -0
.
The next option might be to just check if it's strictly equal to -0
. Easy enough! Unfortunately, not even strict equality is strict enough! -0 === 0
.
We're running out of options quickly. We need to run our -0
through a system that won't spit out a zero. It needs to give us back a number lower or higher than that or just a different data-type altogether.
What about stringification? We can call .toString()
on any number to get its string equivalent. We can then check for the negative sign. We can get the negative sign in -4
by doing (-4).toString()[0]
. Unfortunately, yet again, negative zero is one step ahead of us. Darn that negative zero!
Doing (-0).toString()
will simply result in "0"
. Negative zero stringifies to just zero.
Enough of the cat-and-mouse games. Let's review some actual answers. Try to think of one your own. Remember what we said 3 paragraphs back.
Actual Answers
There are a couple of methods. These might not even be an exhaustive list, so feel free to try out other ideas!
I first came across this blog post when researching this problem. It's from Allen Wirfs-Brock, a person currently on tc39, but it was was written in 2011, so there may be more methods made available recently. I will be taking the first 2 solutions from there.
The first solution we're going to explore is possibly the most performant as we don't change any data types. We work entirely within the number system.
Mathematics and the IEEE
We should first think about what kind of mathematical application can we involve -0
in to affect the result? Addition or subtraction? No, those would both act similarly to 0
. Multiplication or Division? Multiplication would give us a zero (either negative or positive) and so that leaves us right where we started. Division with the zero as the numerator brings the same problem as multiplication!
What if we involve -0
as the denominator? Any number aside for another zero on the numerator would result in -Infinity
! We can check for -Infinity
pretty easily!
We have to be sure it is only -0
on the denominator that can result in -Infinity
, however. Jeff Waldon provides -Math.pow(2, -1074)
as a counterexample. We can thusly add a strictly equals check to ensure we're dealing with a zero.
Via this solution, our winning code is now.
function isZeroNegative(zero) {
const isZero = zero === 0;
const isNegative = 1 / zero === -Infinity;
return isNegative && isZero;
}
Interestingly enough, we can now create a more full-proof Math.sign()
using the same sort of method. This one works the same as the old one, but now works well with zeros. As a side-effect, Math.sign('')
now returns 1
instead of 0
.
Math.sign = function(num) {
if (Number(num) !== Number(num)) return NaN;
if (num === -Infinity) return -1;
return 1 / num < 0 ? -1 : 1;
}
The Strictest Equality
Let's go back to a previous attempt at a solution. We found that strict equality (===
) wasn't strict enough. What kind of equality is more strict than that? What about constants? If the engine can identify -0
as being different than 0
, we may be able to use that somehow.
Unfortunately, const
is too strict. We can't reassign a constant variable in the same scope, no matter what. Even if we reassign it to what it already was. The mere presence of the =
operator is enough to trigger an error.
What we're trying to do is something like the following.
function isZeroNegative(zero) {
if (zero !== 0) return false;
const posZero = 0;
try {
posZero = num;
} catch(e) {
return true;
}
return false;
}
This will, unfortunately, trip even if we receive a positive zero. As mentioned before, the presence of =
is enough to set things off.
Are there any other constants in JS? Well there in fact are! Using Object.freeze
, we can make an object immutable.
It is important to note that mutating a property on an object that has been frozen will not throw an error. We need that to happen. To do so, we will use the more direct Object.defineProperty
.
function isZeroNegative(zero) {
if (zero !== 0) return false;
const posZero = Object.freeze({ val: 0 });
try {
Object.defineProperty(posZero, 'val', { value: num });
} catch(e) {
return true;
}
return false;
}
This will throw an error if the new value is anything other than 0
!
Modern String Conversion
Let's yet again approach a solution we dismissed earlier. While it is true that (-0).toString()
returns "0"
, there is a more modern stringifier - .toLocaleString()
. It's pretty powerful, but recently I came across a tweet regarding how calling it on Infinity
will return the symbol ("∞"), not the normal stringified version ("Infinity").
Calling .toLocaleString()
on -0
actually returns "-0"
!
Using this finding, we can modify the code as follows:
function isZeroNegative(zero) {
if (zero !== 0) return false;
return zero.toLocaleString()[0] === "-";
}
The last one is the quickest and simplest, but possibly not the most performant, which may be important in situations where finding a negative zero is important.
Conclusion
Why would one even need to find a negative zero? I can't think of one off hand. The reason isn't necessary.
This article was more so an investigation into thought processes. Solving problems is an extremely important skill as a software engineer. The way you investigate solutions can be more important than the solution itself.
Top comments (10)
You could also use
Object.is()
, which does make a difference between-0
and0
:Yes! I completely forgot to include that one!
Object.is() seems to be a "fixed" === since it also works when comparing NaN, unlike ===.
MDN gives a polyfill code:
A while back I had an interesting use case for
-0
. I was looking for a way to map x,y coordinates into absolute positioning of an element in CSS. And I wanted that behavior to be different for positive and negative numbers, to create an easy interface for the consumer of that component. So negative x would position the element from the right edge, positive x would position from the left edge. Vice versa for the y axis.I ended up writing a small function that would determine if the number provided is negative, including
-0
:And this function was used to position the element with some simple logic:
Javascript uses IEEE 754 floating point numbers where the most significant (left most) bit represents the sign of the number. Therefore, +0 is encoded something like
0000
while -0 is something like1000
.I don’t know JS very well, so I’m not sure you can access this raw, underlying representation, but in theory, you can bitwise and the input with an actual negative zero to efficiently check for equality.
I did some testing and unfortunately could not get them to produce different results with bitwise operators.
I have been looking at the spec for how it decides what to do and it seems that unless explicitly stated otherwise,
-0
and0
are treated the same.That’s unfortunate.
Be carefull with this one -
return zero.toLocaleString()[0] === "-"
.For some locales on certain OS minus character is going to be different (e.g. char code 8722 instead of 45), so this comparison will be false for -0. For example that's how it works for Swedish on Windows.
You can create a very small sign function which works with
-0
and0
:For everything except Infinity
Math.sign(1 / x)
is sufficient. To support infinite values the|| x
is needed because1 / Infinity
is0
.Thanks for sharing! Found this article because I was converting decimal coordinates to degrees/minutes/seconds coordinates. Pulling the degree out as the integer number before the decimal was causing the negative sign to be lost when it was converted back into a string.