When numbers don't make sense

MiguelMJ - 2023-03-09

Introduction

Most of the time, basic arithmetic is pretty simple and intuitive. Even working with powers and roots isn’t that much of a headache. However, the IEEE standard for floating-point numbers includes special values that can mess things up: NaN (Not a Number), positive infinity and negative infinity. In fact, most algebraic properties of real numbers are no longer valid. The only purpose of this post is to show some cases I found interesting, where these values produce counterintuitive results.

Cover image by Armand Khoury from Unsplash

Standard behavior of NaN and Infinity

  • For all x: NaN > x is false.

  • For all x: NaN < x is false.

  • For all x: NaN == x is false.

  • For all x: NaN != x is true.

  • For all x except NaN and +Infinity: +Infinity > x is true.

  • For all x except Nan and -Infinity: -Infinity < x is true.

  • Infinity / Infinity = NaN

  • Infinity * 0 == NaN

  • Infinity - Infinity == NaN

  • Any other arithmetic operation between a number and Infinity results in Infinity (with a possible change of sign).

  • Any other arithmetic operation between a number (or Infinity) and NaN results in NaN.

Everything I will tell you from now is trivially deduced from understanding the list above; in other words, if you understand all its implications, I won’t tell you much new. Anyway, I think it is interesting to review some side effects that may escape from us when we don’t keep them in mind.

Broken properties

The following is a non-exhaustive list of properties that we use to expect while programming, but are no longer true due to the existence of NaN and Infinity. I will provide the statement of the property (which doesn’t hold) and then a piece of code in JavaScript and Python that implements a check for it, followed by a call that returns false.

Note: JavaScript produces NaN and Infinity values easily, not only because they are native values but because they result from regular operations (Math.sqrt(-1) or 1/0). In the case of Python, they are implemented in the math library (math.nan and math.inf) and operations that would produce them (math.sqrt(-1) or 1/0) will raise an exception. For this reason, assume the following header in each Python snippet.

from math import nan, inf

Disclaimer: The snippets of JavaScript will also fail with values like strings or objects. I’m not trying to make any point about errors in JavaScript. The point is about the NaN and Infinity values, which belong to numeric types in any language with full support for the IEEE standard.


Now, behold the fall of the tautologies.

Identity

Let’s start with the most obvious and painful.

function identity(x){
    return x == x
}
identity(NaN)
def identity(x):
    return x == x

identity(nan)

Some of the following are direct results of this.

A list can’t contain elements that aren’t equal to any element

Sorry for the apparent redundancy in the statement: check the examples to see what I mean.

function checkIncludesEqual(arr, x){
    if(arr.includes(x)){
        return arr.some(e => e == x)
    }else{
        return true
    }
}

checkIncludesEqual([1, 2, NaN], NaN)
def checkIncludesEqual(arr, x):
    if x in arr:
        return any(x == e for e in arr)
    else:
        return True

checkIncludesEqual([1, 2, nan], nan)

The max value of a list is greater than or equal to the rest

function checkMaxIsGE(arr){
    let maxVal = Math.max(...arr)
    return arr.every(x => maxVal >= x)
}
checkMaxIsGE([-1, 0, 1, NaN])
def checkMaxIsGE(arr):
    maxVal = max(arr)
    return all(maxVal >= x for x in arr)

checkMaxIsGE([-1, 0, 1, nan])

The min value of a list is lesser than or equal to the rest

function checkMinIsLE(arr){
    let minVal = Math.min(...arr)
    return arr.every(x <= minVal >= x)
}
checkMinIsLE([-1, 0, 1, NaN])
def checkMinIsLE(arr):
    minVal = min(arr)
    return all(minVal <= x for x in arr)

checkMinIsLE([-1, 0, 1, nan])

Dividend equals divisor times quotient

This is the first we learn about division. D / d = Q implies D = Q * d. Well… no more. This example can only be shown in JavaScript because Python prevents division by zero.

function checkDdQ(dividend, divisor){
    quotient = dividend / divisor
    return dividend == quotient * divisor
}
checkDdQ(1, 0)
checkDdQ(1, Infinity)

The square of a value is either greater than or equal to itself or less than one

function checkSquareIsGE(x){
    return x*x >= x ||  x*x < 1
}
checkSquareIsGE(NaN)
def checkSquareIsGE(x):
    return x*x >= x || x*x < 1

checkSquareIsGE(nan)
checkSquareIsGE(inf)

“Less than or equal to” is equivalent to “Not greater than”

I find this particularly disturbing, not sure why.

function checkLEQisNGT(x){
    return (x <= 10) == !(x > 10)
}
checkLEQisNGT(NaN)
def checkLEQisNGT(x):
    return x <= 10 and not x > 10

checkLEQisNGT(nan)

A value divided by itself equals one

function checkBetweenSelfIsOne(x){
    return x/x == 1
}
checkBetweenSelfIsOne(NaN)
checkBetweenSelfIsOne(Infinity)
def checkBetweenSelfIsOne(x):
    return x/x == 1

checkBetweenSelfIsOne(nan)
checkBetweenSelfIsOne(inf)

A value minus itself equals zero

function checkMinusSelfIsZero(x){
    return x-x == 0
}
checkMinusSelfIsZero(NaN)
checkMinusSelfIsZero(Infinity)
def checkMinusSelfIsZero(x):
    return x-x == 0

checkMinusSelfIsZero(nan)
checkMinusSelfIsZero(inf)

A value times zero equals zero

function checkTimesZero(x):
    return x * 0 == 0

checkTimesZero(NaN)
checkTimesZero(Infinity)
def checkTimesZero(x):
    return x * 0 == 0

checkTimesZero(nan)
checkTimesZero(inf)

These last two have an interesting side effect too. One would expect that 1 / (x-x) and 1 / (x * 0) to result in Infinity. However, when x is Infinity, they result in NaN. Don’t you feel dizzy yet?

Bonus fun fact!

Hey, while working on this article I found something interesting about square roots in Python. As I stated earlier, Python raises an exception if you try to compute the square root of a negative number. But Python implements complex numbers natively so if you use the power operator (**) instead, it returns a valid result!

>>> from math import sqrt
>>> sqrt(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> (-1) ** 0.5
(6.123233995736766e-17+1j)

The perfect result would have been (0+1j) but due to floating-point precision issues, instead of 0 you have a very, very small number. I hope you found it interesting too!

Conclusion

Semantically sound code is much less prone to errors and reduces the need to be constantly catching exceptions. I hope these examples have served as a reminder that we cannot neglect special numeric values or take for granted many seemingly intuitive properties that, if overlooked, can cause apparently correct code to exhibit undesired behavior.