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;
};
4 Answers 4
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);
}
-
1\$\begingroup\$ Excellent. I am not sure if you noticed my separate answer where I fixed the bugs and more. But your answer is significantly faster than mine final one. I especially like how you've avoided
split()
which seems to slow things down. \$\endgroup\$mseifert– mseifert2017年03月27日 03:29:15 +00:00Commented Mar 27, 2017 at 3:29 -
\$\begingroup\$ @mseifert: Yes, I did notice it but I found your shifting code a bit harder to follow and I wanted to optimize it, as well. Also, as you have mentioned I tried to avoid string operations. Actually, I wanted to remove
sNumber
whatsoever, but replacingsNumber[periodIndex + precision + 1]
would introduce a lot of burden, making it counterproductive. \$\endgroup\$Przemek– Przemek2017年03月27日 11:25:49 +00:00Commented Mar 27, 2017 at 11:25
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
.
-
\$\begingroup\$ I noticed this as well with the first case and added
number = +number
to handle when a string with an 'e' was passed. The below answer will now handle cases with ane
in the string. For exampleconsole.log(round("1.2345678e+2", 2)); // ==> 123.46
andconsole.log(round(1.2345678e+2, 2)); // ==> 123.46
. \$\endgroup\$mseifert– mseifert2017年03月25日 20:27:03 +00:00Commented Mar 25, 2017 at 20:27 -
\$\begingroup\$ No, I mean 1.0e-20 and 1.0e20, which have an
e
in their natural string representation. \$\endgroup\$Roland Illig– Roland Illig2017年03月25日 20:30:02 +00:00Commented Mar 25, 2017 at 20:30 -
\$\begingroup\$ Thanks for pointing this out. I've fixed the answer to handle those cases. Now I understand why MDN did it the way they did. \$\endgroup\$mseifert– mseifert2017年03月25日 22:03:59 +00:00Commented Mar 25, 2017 at 22:03
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
-
\$\begingroup\$ Shouldn't
round(1.0e-20, 20)
still be1.0e-20
? If not,round(1.0e-1, 1)
would have to be0
then, also, just by analogy. \$\endgroup\$Roland Illig– Roland Illig2017年03月26日 00:46:24 +00:00Commented Mar 26, 2017 at 0:46 -
\$\begingroup\$ @RolandIllig Yes thanks. It is fixed now. I needed to convert the string
number[1]
to a numeric using+number[1]
\$\endgroup\$mseifert– mseifert2017年03月26日 03:57:34 +00:00Commented Mar 26, 2017 at 3:57
Use this function to round the given number
Number((6.688689).toFixed(1));
-
2\$\begingroup\$ Can you expand on why that function would be an improvement? \$\endgroup\$forsvarir– forsvarir2017年03月25日 09:08:45 +00:00Commented Mar 25, 2017 at 9:08
-
5\$\begingroup\$ This suggestion does not do the same thing as what the OP's code does. THis always rounds to an integer value, but the OP code rounds to a supplied precision. Also, assuming the
1
is a variable, this suggestion would not support negative precisions like-2
. \$\endgroup\$rolfl– rolfl2017年03月25日 11:05:55 +00:00Commented Mar 25, 2017 at 11:05
round
function as well. \$\endgroup\$(e.g. round(1254, -2)) => 1300 )
so I mentioned that as well. \$\endgroup\$