Problem
JavaScript has some inaccurate rounding behavior so I’ve been looking for a reliable solution without success. There are many answers in this SO post but none cover all the edge cases as far as I can tell. I wrote the following which handles all the edge cases presented. Will it be reliable with edge cases I haven’t tested?
If this is a viable solution, any enhancements to make it more efficient would be appreciated. It’s not fast (time to run function 1000000 times: 778ms) but doesn’t seem to be terrible either. If there is a better solution, please post.
The edge cases that seemed to give the most problem were the first two:
console.log(round(1.005, 2)); // 1.01
console.log(round(1234.00000254495, 10)); //1234.000002545
console.log(round(1835.665, 2)); // 1835.67))
console.log(round(-1835.665, 2)); // -1835.67))
console.log(round(10.8034, 2)); // 10.8
console.log(round(1.275, 2)); // 1.28
console.log(round(1.27499, 2)); // 1.27
console.log(round(1.2345678e+2, 2)); // 123.46
console.log(round(1234.5678, -1)); // 1230
console.log(round(1235.5678, -1)); // 1240
console.log(round(1234.5678, -2)); // 1200
console.log(round(1254.5678, -2)); // 1300
console.log(round(1254, 2)); // 1254
console.log(round("123.45")); // 123
console.log(round("123.55")); // 124
function round(number, precision) {
precision = precision ? precision : 0;
var sNumber = "" + number;
var a = sNumber.split(".");
if (a.length == 1 || precision < 0) {
// from MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
var factor = Math.pow(10, precision);
var tempNumber = number * factor;
var roundedTempNumber = Math.round(tempNumber);
return roundedTempNumber / factor;
}
// use one decimal place beyond the precision
var factor = Math.pow(10, precision + 1);
// separate out decimals and trim or pad as necessary
var sDec = a[1].substr(0, precision + 1);
if (sDec.length < precision + 1) {
for (var i = 0; i < (precision - sDec.length); i++)
sDec = sDec.concat("0");
}
// put the number back together
var sNumber = a[0] + "." + sDec;
var number = parseFloat(sNumber);
// test the last digit
var last = sDec.substr(sDec.length - 1);
if (last >= 5) {
// round up by correcting float error
// UPDATED - for negative numbers will round away from 0
// e.g. round(-2.5, 0) == -3
number += 1/(factor) * (+number < 0 ? -1 : 1);
}
number = +number.toFixed(precision);
return number;
};
Solution
Here is my solution that I came up with by starting off from your own code, which as I was tracing the logic I realized was a bit bloated, through quite efficient. What’s more, when I was adding a few more test cases I discovered 2 bugs in it. I also did benchmark of your, @mseifert and mine code.
But first, few comments about your code:
- You have quite numerous unnecessary (in my opinion) variables,
- The loop and surrounding conditional statement is completely unnecessary as well,
- Variables’ names could be more descriptive, especially
a
‘s, sDec += '0'
is shorter and faster thansDec = sDec.concat("0")
,- Why to assign
+number.toFixed(precision)
to variable number, if in the very next line you justreturn number
? Go withreturn +number.toFixed(precision)
instead.
Test cases
Just one note: this is definitely not the elegant way of writing tests, but test cases themselves weren’t in the scope of this question, so I allowed myself to do it this way.
Two lines with comments next to them are those which revealed bugs in your original code to me.
console.clear();
console.log(round(1.0e-5, 5) === 0.00001);
console.log(round(1.0e-20, 20) === 1e-20);
console.log(round(1.0e20, 2) === 100000000000000000000);
console.log(round(1.005, 2) === 1.01);
console.log(round(1234.00000254495, 10) === 1234.000002545);
console.log(round(1835.665, 2) === 1835.67);
console.log(round(-1835.665, 2) === -1835.67);
console.log(round(1.27499, 2) === 1.27);
console.log(round(1.2345678e+2, 2) === 123.46);
console.log(round(1234.5678, -1) === 1230);
console.log(round(1234.5678, -2) === 1200);
console.log(round(1254.5678, -2) === 1300);
console.log(round(1254, 2) === 1254);
console.log(round(1254) === 1254);
console.log(round('1254') === 1254);
console.log(round('123.55') === 124);
console.log(round('123.55', 1) === 123.6);
console.log(round('123.55', '1') === 123.6);
console.log(round(123.55, '1') === 123.6);
console.log(round('-1835.665', 2) === -1835.67);
console.log(round('-1835.665', '2') === -1835.67); // Made me turn precision into +precision
console.log(round(-1835.665, '2') === -1835.67);
console.log(round('1.0e-5', 5) === 0.00001); // Made me add number = +number;
console.log(round('1.0e-5', '5') === 0.00001);
console.log(round(1.0e-5, '5') === 0.00001);
console.log(round('1.0e-20', 20) === 1e-20);
console.log(round('1.0e-20', '20') === 1e-20);
console.log(round(1.0e-20, '20') === 1e-20);
console.log(round('1.0e20', 2) === 100000000000000000000);
console.log(round('1.0e20', '2') === 100000000000000000000);
console.log(round(1.0e20, '2') === 100000000000000000000);
Benchmark
Notice that my code is not much faster than yours.
(made with JSBench.Me and using Chrome 57)
Complete code
You can change the only var
keyword to const
from ES6 if you want to use it, since the variables that this keyword applies to are indeed constant.
function round(number, precision) {
'use strict';
precision = precision ? +precision : 0;
var sNumber = number + '',
periodIndex = sNumber.indexOf('.'),
factor = Math.pow(10, precision);
if (periodIndex === -1 || precision < 0) {
return Math.round(number * factor) / factor;
}
number = +number;
// sNumber[periodIndex + precision + 1] is the last digit
if (sNumber[periodIndex + precision + 1] >= 5) {
// Correcting float error
// factor * 10 to use one decimal place beyond the precision
number += (number < 0 ? -1 : 1) / (factor * 10);
}
return +number.toFixed(precision);
}
You should automate your test cases by comparing the actual with the expected result and only logging it when these differ. This will tell you immediately whether everything worked (no logging at all).
Your test doesn’t cover numbers whose string representation contains an e
.
Here is a solution which is quick and handles all cases – negative number and negative precision. It was adapted from MDN – the main differences are
- I pass positive numbers to specify decimal places where it is the opposite on MDN.
- I took it out of the prototype and limited it to just the round function (removed ceil and floor)
- it is significantly faster
function round(number, precision) {
number = +number;
precision = precision ? +precision : 0;
if (precision == 0) {
return Math.round(number);
}
var sign = 1;
if (number < 0) {
sign = -1;
number = Math.abs(number);
}
// Shift
number = number.toString().split('e');
number = Math.round(+(number[0] + 'e' + (number[1] ? (+number[1] + precision) : precision)));
// Shift back
number = number.toString().split('e');
return +(number[0] + 'e' + (number[1] ? (+number[1] - precision) : -precision)) * sign;
}
console.log(round(1.0e-5, 5)); // 0.00001
console.log(round(1.0e-20, 20)); // 1e-20
console.log(round(1.0e20, 2)); // 100000000000000000000
console.log(round(1.005, 2)); // 1.01
console.log(round(1234.00000254495, 10)); //1234.000002545
console.log(round(1835.665, 2)); // 1835.67))
console.log(round(-1835.665, 2)); // -1835.67))
console.log(round(1.27499, 2)); // 1.27
console.log(round(1.2345678e+2, 2)); // 123.46
console.log(round(1234.5678, -1)); // 1230
console.log(round(1234.5678, -2)); // 1200
console.log(round(1254.5678, -2)); // 1300
console.log(round(1254, 2)); // 1254
console.log(round("123.55")); // 124
Use this function to round the given number
Number((6.688689).toFixed(1));