Loose Equality, the Null Operator, and Other Oddities

 Here's a fun bit of code to run:

console.log('0.5' == 0.5)
//evaluates to true
console.log('0.5' === 0.5)
//evaluates to false

The first equality statement is what's known in JavaScript as "abstract equality comparison", which people usually refer to as "loose equality."  Loose equality compares objects only after doing a type conversion - so even though '0.5' is a string and 0.5 is a number, JavaScript sort of jams them together and says "sure, good enough."  It's weird, but convenient sometimes when dealing with data that's really (in an abstract sense) numbers but could come through the pipes as either a number or a string.

People have written surprisingly long algorithms to precisely define what abstract equality and strict equality (aka, normal equality) mean in JavaScript and other languages that adhere to the ECMAScript standards.  But for our regular usage, things that are to people the same thing are loosely equal and things that are actually equal are strictly equal with only a couple of exceptions to remember:

1. As mentioned above, numbers-as-strings and numbers are loosely equal.

2. Falsy values mess everything up.

Falsy values are values that JavaScript treats as false when "encountered in a boolean context", aka you've put them in an equality statement. There are eight falsy values in JavaScript: false (of course), 0, -0, 0n (i.e., zero as a BigInt), "", undefined, NaN (Not a Number), and the coolest-named one: null.

With the notable exception of NaN, all of them are loosely equal to false (0 == false evaluates to true), strictly not equal to false (0 === false evaluates to false) and occasionally loosely equal to each other:

0 == -0 == 0n == ""

null == undefined

Meanwhile, NaN is not equal to anything, ever, even itself, even loosely, even strictly. Yes, NaN === NaN evaluates to false.  This is often confusing, but it helps to think about the reason why: NaN doesn't necessarily mean "not a number", it's just a numeric data type that isn't defined or can't be represented.  We can't say NaN is even loosely equal to itself because it's possibly just a stand-in for something else, and we don't know if one something is equal to another, so we default to false.

Bonus:

What is -0?

-0 is exactly what it sounds like; it's negative zero.  Is negative zero different than regular/positive zero? No, it's not.  But in JavaScript, for unclear reasons, you can divide by zero. 1/0 === Infinity in JavaScript.  And if there's a positive infinity, surely there's a negative infinity?

Yes. In fact, 1/-0 === -Infinity.  So that's why you need a negative zero - in all other respects, 0 is -0 is 0 in JavaScript.

Bonus bonus:

Here's a classic programming interview question: what does the following line of JS code print to the terminal?

console.log(0.1 + 0.2 === 0.3)

If you've realized that this sounds like a set-up and answered "false", then ding-ding-ding you win!  0.1 + 0.2 === 0.3 does in fact evaluate to false in JavaScript due to floating point precision.  Essentially, the problem is that decimals don't usually convert easily to base 2 - you can't convert 1/10 to a fraction with a base of a power of 2 or even the sum of several fractions with different 2-power bases because the prime factors of 10 are 2 and 5.

There's a couple different ways around this problem, but JavaScript and other floating point languages essentially pretend the number doesn't exist by rounding decimals off to numbers that can be represented in a binary system that are close enough.  Well, close enough most of the time.  Don't do number theory research with JavaScript, kids.

Sometimes, though, the floating point precision lines up nicely and your decimal addition equality statements will evaluate to true though.  Here's a nifty little bit of code you can use to see how this works:

let decimals = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
let answers = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2]
let x = -1
for (let i=0; i < 10; i++) {
z = x
for (let j=0; j < 10; j++) {
z++
console.log(`${decimals[i]} + ${decimals[j]} === ${answers[z]}`, decimals[i] + decimals[j] === answers[z])
}
x++
}

Bonus bonus bonus:

If you remember, 0 is a falsy value.  So when you're constructing a hash in a situation like below:

for (let i=0; i< array.length; i++) {
if (!hash[array[i]]) {
hash[array[i]] = 1
} else {
hash[array[i]]++
}
}

For the love of God, make sure that your array[i] won't ever evaluate to 0 or that you at least have some exception handling.

Bonus bonus bonus bonus:

Although they are not listed as official falsy values, empty arrays loosely evaluate to false. Fyi.

Comments

Popular posts from this blog

The Sorting Hat

Kadane's Algorithm