A ball of twine with a hole in it

Very often calculations in JavaScript produce results that don’t quite fall within the ranges we want, or generate numbers that need to be “cleaned up” before use. Exactly what happens to those numbers - rounding up or down, set within a range, or being “clipped” to a certain number of decimal places - depends on what you want to use them for.

Why Round Numbers?

One of the curious aspects of JavaScript is that it doesn’t actually store integers: instead, it thinks of numbers as floating point binaries. This, combined with the fact that many fractions can’t be expressed in a finite number of decimal places, means that JavaScript can create results like the following (using the console):

0.1 * 0.2;
> 0.020000000000000004

For practical purposes, this imprecision won’t matter in the vast majority of cases - we’re talking about an error of 2 quintillionths - but it is a little frustrating. It can also introduce slightly odd-looking results when dealing with numbers that represent values of currencies, percentages, or file sizes. To fix these representations, we need to round the results, or set decimal precision.

Rounding numbers has many other practical applications: if a user is manipulating a range element, for example, we may want to round the associated value to the nearest whole number, rather than dealing with a decimal.

Rounding Decimal Numbers

To clip a decimal number, use the toFixed() or toPrecision methods. Both take a single argument that determines, respectively, how many significant digits (i.e. the total number of digits used in the number) or decimal places (the number of digitals after the decimal point) to include in the result:

  • If no argument is specified for toFixed(), the default is 0, i.e. no decimal places; the argument has a maximum value of 20.
  • if no argument is specified for toPrecision, the number is unchanged.
var randNum = 6.25;
randNum.toFixed();
> "6"

Math.PI.toPrecision(1);
> "3"

var randNum = 87.335;
randNum.toFixed(2);
> "87.33"

var randNum = 87.337;
randNum.toPrecision(3);
> "87.3"

toFixed() and toPrecision() are also useful methods to pad out a whole number to include decimal places, which can be especially handy when dealing with numbers that represent currencies:

var wholeNum = 1
var dollarsCents = wholeNum.toFixed(2);
console.log(dollarsCents);
> "1.00"

Avoiding Decimal Rounding Errors

In some cases, both toFixed and toPrecision will round values of 5 down rather than up:

var numTest = 1.005;
numTest.toFixed(2);
> 1;

The result of the calculation above should be 1.01, not 1. If avoiding this error is important, I’d recommend a solution suggested by Jack L Moore that uses exponential numbers for the calculation:

function round(value, decimals) {
    return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
}

To use:

round(1.005,2);
> 1.01

Epsilon Rounding

An alternative method of rounding decimals was introduced with ES6 (aka JavaScript 2015). “Machine epsilon” provides a reasonable margin of error when comparing two floating point numbers. Without rounding, comparisons can yield results like the following:

0.1 + 0.2 === 0.3
> false

Math.EPSILON can be used in a function to produce correct comparisons:

function epsEqu(x, y) {
    return Math.abs(x - y) < Number.EPSILON * Math.max(Math.abs(x), Math.abs(y));
}

The function takes two arguments: one carrying the calculation, the other the (rounded) expected result. It returns the comparison of the two:

epsEqu(0.1 + 0.2, 0.3)
> true

Truncating Decimal Numbers

All the techniques shown so far do some kind of rounding to decimal numbers. To truncate a positive number to two decimal places, multiply it by 100, truncate it, then divide the result by 100:

function truncated(num) {
    return Math.trunc(num * 100) / 100;
}

truncated(3.1416)
> 3.14

If you want something with a bit more adaptability, you can take advantage of the bitwise double tilde operator:

function truncated(num, decimalPlaces) {    
    var numPowerConverter = Math.pow(10, decimalPlaces); 
    return ~~(num * numPowerConverter)/numPowerConverter;
}

To use:

var randInt = 35.874993;
truncated(randInt,3);
> 35.874

I’ll have more to say about bitwise operations in a future article.

Rounding To The Nearest Number

To round a decimal number up or down to the nearest whole number, depending on which is closest, use Math.round():

Math.round(4.3)
> 4

Math.round(4.5)
> 5

Note that “half values” like .5 round up.

Rounding Down To The Nearest Whole Number

If you always want to round down, use Math.floor:

Math.floor(42.23);
> 42

Math.floor(36.93);
> 36

Note that this “down” rounding direction is true for all numbers, including negatives. Think of a skyscraper with an infinite number of floors, including basement levels (representing the negative numbers). If you’re in an elevator between basement levels 2 and 3 (represented by a value of -2.5), Math.floor will take you to -3:

Math.floor(-2.5);
> -3

If want to avoid this behaviour, use Math.trunc, supported in all modern browsers (except IE/Edge):

Math.trunc(-41.43);
> -41

MDN also provides a three-line polyfill to gain Math.trunc support in older browsers and IE/Edge.

Rounding Up To The Nearest Whole Number

Alternatively, if you always want to round up, use Math.ceil. Again, think of that infinite elevator: Math.ceil will always go “upwards”, regardless of whether the number is negative or not:

Math.ceil(42.23);
> 43

Math.ceil(36.93);
> 37
Math.ceil(-36.93); -36

Rounding Up/Down To The Nearest Multiple of a Number

If we want to round to the nearest multiple of 5, the easiest way is to create a function that divides the number by 5, rounds it, then multiplies it by the same amount:

function roundTo5(num) {
    return Math.round(num/5)*5;
}

To use it:

roundTo5(11);
> 10

If you were rounding to multiples of different numbers, we could make this function more general by passing the function both the initial number and the multiple:

function roundToMultiple(num, multiple) {
    return Math.round(num/multiple)*multiple;
}

To use it, include both the number and the multiple in the call to the function:


var initialNumber = 11;
var multiple = 10;
roundToMultiple(initialNumber, multiple);
> 10;

To exclusively round up or round down; substitute ceil or floor for round in the function.

Clamping Number To a Range

There are plenty of times when you may receive a value x that needs to be within the bounds of a range. For example, you may need a value from 1 to 100, but receive a value of 123. To fix this, we can use min (which will always return the smallest of a set of numbers) and max (the largest member of any set of numbers). Using the 1 to 100 example:

var lowBound = 1;
var highBound = 100;
var numInput = 123;
var clamped = Math.max(lowBound, Math.min(numInput, highBound));
console.log(clamped);
> 100;

Again, this could be turned into a function, or possibly an extension of the Number class, a variation first suggested by Daniel X. Moore:

Number.prototype.clamp = function(min, max) {
  return Math.min(Math.max(this, min), max);
};

To use it:

(numInput).clamp(lowBound, highBound);

Gaussian Rounding

Gaussian rounding, also known as “bankers” rounding, convergent rounding, Dutch rounding, and odd–even rounding, is a method of rounding without statistical bias; regular rounding has a native upwards bias. Gaussian rounding avoids this by rounding to the nearest even number. The best solution I’m aware of is by Tim Down:

function gaussRound(num, decimalPlaces) {
    var d = decimalPlaces || 0,
    m = Math.pow(10, d),
    n = +(d ? num * m : num).toFixed(8),
    i = Math.floor(n), f = n - i,
    e = 1e-8,
    r = (f > 0.5 - e && f < 0.5 + e) ?
		((i % 2 == 0) ? i : i + 1) : Math.round(n);
    return d ? r / m : r;
}

Examples in use:

gaussRound(2.5)
> 2

gaussRound(3.5)
> 4

gaussRound(2.57,1)
> 2.6

Image by Philippe Put, used under a Creative Commons license

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.