The big and Small of JavaScript numbers

For all its warts, JavaScript has some novelties few other languages have. In this post I want to share about JavaScript numbers, which both acknowledge normal mathematics and the fact that they are represented by an imperfect data format.

Summary

JavaScript numbers are all internally represented by a double-precision floating-point value, which means that there are perfect and imperfect representations of whole numbers as well as normal double-type floating-point values. Additionally, this is augmented by a few special values representing infinities and impossible numbers. Numbers do not overflow in JavaScript: they become positive or negative infinity. Division-by-zero does not throw an exception: it returns infinity (the limit rule). Nonsense math does not yield nonsense answers: it returns “not a number.”

Show me!

While I encourage you to read the MDN article on Number, I want to provide a few patterns I have found useful when working in JavaScript that I can’t often use in other languages. In many cases we can turn iffy code (code with conditional branching) into code that simply always works regardless of the input. Here is a list of a few of those patterns. Do you have others up your sleeve?

Safe Division

If we end up dividing by zero we get Infinity as the result. Often we’ll see something like this written to prevent that:

const quotient = dividend / ( divisor + 0.0001 )

This is often good enough but there are pathological cases that should make us consider this code a bug. What happens if the divisor is small compared to that 0.0001? As long as it’s sufficiently large the little extra doesn’t affect the outcome much, but if divisor === 0.0001 then this value will cause our result to be half as large as it should be. Solution? We could descend further and further until we’re “sufficiently safe.” Maybe we could put it in a named constant for reuse.

const EPSILON = 1e-16
const quotient = dividend / ( divisor + EPSILON )

Why EPSILON? Because mathematicians agreed that ε is the smallest unit number. It’s the number which cannot be reached by adding or subtracting, multiplying or dividing. Well, 1e-16 isn’t small enough for us, but luckily, JavaScript has us covered.

const quotient = dividend / ( divisor + Number.EPSILON )

See what just happened? It’s already defined for us, and the value is a bit technical but perfect: “the difference between one and the smallest value greater than one that can be represented as a Number.” It’s the smallest difference in values representable by the language. If that value made a significant difference in our calculations then we would have other major problems in our code to take care of.

Clamping

We often want to limit numbers within certain bounds.

let a = 15
if ( a > 10 ) {
    a = 10
}

This is a noisy and mutable approach. It introduces needless branching (to our eyes, not the computer’s). Thankfully, math has a solution for us.

const a = Math.max( 10, 15 ) // a === 10

In one line of code we have expressed something that will always conform and we had no if in there. This means fewer lines of code to maintain and fewer opportunities to accidentally break apart the logic of what a should be. And a minimum?

const clamp = ( min, max, value ) => Math.min( max, Math.max( min, value ) )
const a = clamp( 3, 8, 15 ) // a === 8
const a = clamp( 3, 8, -9 ) // a === 3

Again, we have a single expression valid for all inputs which also indicates our intention: to constrain a within certain bounds (the if provides no such context apart from reading the code within).

Clamping selections

We often index into lists with a separate indexing variable. Recently I came across a need to grab the last item in the list if no index were provided but to grab the indexed item if it had been.

let item
if ( selectedIndex !== undefined ) {
    item = list[ selectedIndex ]
} else {
    item = list[ list.length - 1 ]
}

Get’s the job done, but it’s also a bit ugly and noisy. Couldn’t we make more immutable and clean declarations?

const index = clamp( 0, list.length - 1, selectedIndex )

There, not so bad again – one line. Oh wait, you see a bug? That’s right; what if selectedIndex is undefined? Here we can set a default. Let’s expand our code view…

const getItem = ( list, selectedIndex = Infinity ) => {
const index = clamp( 0, list.length - 1, selectedIndex )
    return list[ index ]
}

We have used Infinity as the default because it will always be bigger than any index we pass in and get clamped down to the last element in the array.

Fun Facts

Anything divided by Infinity is 0. In this example we’re counting how many groups we would need to split a list into n-sized chunks. This is safe with negative numbers, zero, and beyond (we’ll assume we will always need at least one group, even for an empty list).

const countGroups = ( list, groupSize = Infinity ) =>
    Math.max( 1, Math.ceil( list.length / groupSize ) )

Everything is smaller than Infinity (except other infinities). Using Infinity as a default value thus allows trivial operations to grab the larger or smaller of a given number or the provided number if it’s provided.

const getHardMax = ( softLimit = Infinity ) => Math.min( softLimit, 1000 )
const getHardMin = ( softLimit = -Infinity ) => Math.max( softLimit, 10 )

Like most languages, we can know if a number is negative or positive with Math.sign(). This is a relative of Math.abs(). We can use it to indicate direction or to enforce the sign at the end of an expression.

const delta = next - prev
const message = {
    [ -1 ]: `decreased by ${ Math.abs( delta ) }º`,
    [ 0 ]: 'stayed the same',
    [ 1 ]: `increased by ${ Math.abs( delta ) }º`,
}[ Math.sign( delta ) ]
return `The temperature ${ message }`.

The Math.max() and Math.min() operations are associativecommutative, and idempotent, meaning that they can be done in any order any number of times and come to the same result. Honestly; it doesn’t matter how we get to the destination – all the paths bring us to the same place.

if ( a > runningMax ) {
    return a
}
return runningMax

How redundant! If the value hasn’t changed, running it through Math.max() again won’t hurt. It will return the same value.

return Math.max( a, runningMax )

Notice that we can choose to put a wherever makes more sense reading the code because of the commutativity. It could be before runningMax or after. In fact, you may be noticing a pattern here for finding the largest value in an array…

const max = list => list.reduce( Math.max, -Infinity )
const min = list => list.reduce( Math.min, Infinity )

Our old friend Infinity is showing up again. This list reduction will iterate over the list and compare the previous value (starting at the smallest possible number) with the next item in the list. It will propagate forward to the end when the highest-valued number will be returned (same accordingly for min()).

Making impossible states impossible

There are many more patterns like this that will be valuable in our code which can prevent accidentally getting into invalid states. if-assignements can be easy to break since they decouple a value’s logic from its definition or binding. In most cases where we think we need conditional assignment, however, we can use higher-order functions to get around that, such as with clamp().

Oftentimes we neither have to resort to some clever snippet of code nor to a mangled nesting of branches in order to get a value we want. A pause to think about what we are doing and some basic math can usually clarify the logic in ways we probably didn’t expect. JavaScript’s number system helps us out here too because of how it supports things like EPSILON and Infinity.

Let’s keep an eye out for these cases where the conditionals and the loops and the ternaries open up opportunities for bugs (by us or by future modifications to our code). In those place, maybe a single expression can return our valid desired output no matter what comes in (assuming the types are right: numbers in, numbers out).

1 thought on “The big and Small of JavaScript numbers

  1. Well done and written in a nice way 🙂

Leave a Reply

%d bloggers like this:
search previous next tag category expand menu location phone mail time cart zoom edit close