DEV Community

EmNudge
EmNudge

Posted on

Identifying Negative Zero

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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] === "-";
}
Enter fullscreen mode Exit fullscreen mode

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 (9)

Collapse
 
savagepixie profile image
SavagePixie

You could also use Object.is(), which does make a difference between -0 and 0:

Object.is(0, -0) // -> false
Object.is(-0, -0) // -> true
Collapse
 
emnudge profile image
EmNudge

Yes! I completely forgot to include that one!
Object.is() seems to be a "fixed" === since it also works when comparing NaN, unlike ===.

Collapse
 
theodesp profile image
Theofanis Despoudis

MDN gives a polyfill code:

if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) { // Steps 1-5, 7-10
      // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // Step 6.a: NaN == NaN
      return x !== x && y !== y;
    }
  };
}
Collapse
 
rubberduck profile image
Christopher McClellan

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

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.

Collapse
 
emnudge profile image
EmNudge • Edited

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 and 0 are treated the same.

Collapse
 
rubberduck profile image
Christopher McClellan

That’s unfortunate.

Collapse
 
egeriis profile image
Ronni Egeriis Persson

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:

const isNeg = v => v < 0 || 1 / v === -Infinity;

And this function was used to position the element with some simple logic:

const x = `${isNeg(left) ? 'right' : 'left'} ${Math.abs(left)}rem`;
const y = `${isNeg(top) ? 'bottom' : 'top'} ${Math.abs(top)}rem`;
Collapse
 
kayahr profile image
Kayahr

You can create a very small sign function which works with -0 and 0:

sign = x => Math.sign(1 / x || x)
Enter fullscreen mode Exit fullscreen mode

For everything except Infinity Math.sign(1 / x) is sufficient. To support infinite values the || x is needed because 1 / Infinity is 0.

Collapse
 
kollinmurphy profile image
Kollin Murphy

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.