close
close

The Big and Small JavaScript Numbers – Fluffy and Flakey

For all its warts, JavaScript has some novelties that few other languages ​​have. In this post I want to talk about JavaScript numbers, which recognizes both normal math 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, meaning this is the case perfect And imperfect representations of integers, as well as normal double-type floating-point values. Furthermore, this is supplemented with a few special values ​​that represent infinities and impossible numbers. Numbers don’t flow in JavaScript: they become positive or negative infinity. Dividing by zero does not throw an exception: it returns infinity (the limit rule). Nonsense math does not produce nonsensical answers: it returns ‘no number’.

Show me!

Although I encourage you to read the MDN article about it NumberI’d like to offer a few patterns that I’ve found useful when working in JavaScript and that I don’t often get to use in other languages. In many cases we can turn questionable code (code with conditional branching) into code that simply always works, regardless of the input. Here is a list of some of those patterns. Do you have others on hand?

Safe division

Finally, if we divide by zero, we get Infinity as a result. Often we see something like this written to prevent that:

const quotient = dividend / ( divisor + 0.0001 )

This is often good enough, but there are some pathological cases that should make us consider this code as a bug. What happens if the divisor is small in comparison? 0.0001? As long as it’s large enough, the little extra won’t have much impact on the outcome, but if divisor === 0.0001 then this value will make our result half as large as it should be. Solution? We can descend further and further until we are ‘sufficiently safe’. Maybe we can 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 is the number that cannot be arrived at by addition or subtraction, multiplication or division. Good, 1e-16 isn’t small enough for us, but luckily JavaScript can cover us.

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

Do you 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 is the smallest difference in values ​​that can be represented by language. If that value made a significant difference in our calculations, we would have to solve other major problems in our code.

Clamps

We often want to limit the numbers within certain limits.

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

This is a noisy and volatile approach. It introduces unnecessary branches (in our eyes, not the computer’s). Fortunately, mathematics has a solution for us.

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

In one line of code we expressed something that will always conform, but we didn’t have that if in it. This means fewer lines of code to maintain and fewer opportunities to accidentally break the logic of something 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 that is valid for all inputs and also specifies our intent: restrict a within certain limits (de if does not provide such context, apart from reading the code within it).

Clamp selections

We often index in lists with a separate index variable. Recently I encountered the need to grab the last item on the list if no index was provided but to grab the indexed item if it was.

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

It gets the job done, but it’s also a bit ugly and noisy. Can we no longer make unalterable and pure statements?

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

There, again not so bad – one line. Oh wait, do 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 default because it will always be larger than any index we pass and will be limited to the last element in the array.

Fun facts

All divided by Infinity is 0. In this example, we count how many groups we need to split a list into n-sized pieces. This is safe with negative numbers, zero and higher (we assume we 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). To use Infinity as a default value, trivial operations can therefore take the larger or smaller of a given number or the specified number if provided.

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

Like most languages, we can know whether a number is negative or positive Math.sign(). This is a relative of Math.abs(). We can use it to specify the 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 associative, commutativeAnd idempotentmeaning they can be run any number of times in any order and lead to the same result. Honestly; it doesn’t matter how we reach the destination – all paths take us to the same place.

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

How unnecessary! If the value has not changed, execute it Math.max() again it can’t hurt. It returns the same value.

return Math.max( a, runningMax )

Note that we can choose to post a where it makes more sense to read the code because of the commutativity. It could be for runningMax or after. You may notice 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 appears again. This list decrease will iterate through the list and compare the previous value (starting from the smallest possible number) with the next item in the list. It will propagate until the end, when the number with the highest value is returned (same goes for min()).

Making impossible situations impossible

There are many more patterns like this that will be valuable in our code and can prevent them from accidentally entering an invalid state. ifassignments are easily broken because they disconnect the logic of a value from its definition or binding. In most cases where we think we need conditional assignment, we can use higher order functions to get around that, as in clamp().

Often we don’t have to resort to a clever code snippet, nor to a garbled nesting of branches to get the desired value. A pause to think about what we’re doing and some basic math can usually clarify the logic in ways we probably didn’t expect. JavaScript’s number system also helps us here in the way it handles things like EPSILON And Infinity.

Let’s keep an eye on these cases where the conditionals, the loops, and the ternaries provide opportunities for bugs (by us or by future changes to our code). At that point, perhaps a single expression can return our valid desired output regardless of what comes in (assuming the types are correct: numbers in, numbers out).