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.