Rounding away from zero in Javascript
Asked Answered
M

3

9

We are building a table in Javascript with Handsontable representing currency amounts. We give the user the possibility of render the amounts with two decimal places or no decimal places (it's a requirement from the client). And then we find things like this:

Column A     Column B      Column C = A + B
-------------------------------------------
-273.50       273.50       0                 Two decimals
-273          274          0                 No decimals

Investigating a little we came to find that the basic rounding function in Javascript, Math.round(), works like this:

If the fractional portion is exactly 0.5, the argument is rounded to the next integer in the direction of +∞. Note that this differs from many languages' round() functions, which often round this case to the next integer away from zero, instead (giving a different result in the case of negative numbers with a fractional part of exactly 0.5).

As we are dealing with currency amounts, we do not care about what happens after the second decimal place, so we chose to add -0.0000001 to any negative value in the table. Thus, when rendering the values with two or no decimals, now we get the proper results, as Math.round(-273.5000001) = -274, and Math.round(-273.4900001) is still -273.

Nonetheless, we would like to find a finer solution to this problem. So what is the best, most elegant way to achieve this (that does not require modifying the original numeric value)? Note that we do not directly call Math.round(x), we just tell Handsontable to format a value with a given number of decimal places.

Motheaten answered 7/4, 2017 at 8:16 Comment(5)
var rounded = (val < 0) ? Math.round(val - 0.5) : Math.round(val+0.5);Liter
Actually when dealing with currency you should just use integer values in the smallest denomination. Add the decimal point just for display. This will prevent a lot of problems with calculations in that area.Momentous
@Momentous that's a interesting point, but if we have, say, -27350 cents of euro (saved in the smallest denomination), when I am to display that with no decimal places in the euro denomination, I would still have -273 as the value displayed, as I would have to divide the value by 100 before.Motheaten
[33, 2.3, 53.34].map(x=>x.toFixed(2)).join(", ") == "33.00, 2.30, 53.34" thus all you need is .toFixed(2)Sinewy
@CarlosAlejo I didn't suggest this will solve the concret problem at hand. This was just a mere hint, which might save you some trouble when dealing with currencies, where floating point problems are usually more important than in other computations.Momentous
N
2

Just some variations on how you could implement the desired behaviour, with or without using Math.round(). And the proof that these functions work.

It's up to you which version speaks to you.

const round1 = v => v<0 ? Math.ceil(v - .5) : Math.floor(+v + .5);

const round2 = v => Math.trunc(+v + .5 * Math.sign(v));

const round3 = v => Math.sign(v) * Math.round(Math.abs(v));

const round4 = v => v<0 ? -Math.round(-v): Math.round(v);

const funcs = [Number, Math.round, round1, round2, round3, round4];

[
  -273.50, -273.49, -273.51, 
   273.50,  273.49,  273.51
].forEach(value => console.log(
  Object.fromEntries(funcs.map(fn => [fn.name, fn(value)]))
));
Nanji answered 11/9, 2022 at 1:27 Comment(0)
D
1

Math.round() works as wanted for zero and positive numbers. For negative numbers, convert to a positive and then multiply back:

/**
 * @arg {number} num
 * @return {number}
 */
function round(num) {
  return num < 0 ? -Math.round(-num) : Math.round(num);
}

This seems to work correctly for these inputs:

round(-3) = -3
round(-2.9) = -3
round(-2.8) = -3
round(-2.7) = -3
round(-2.6) = -3
round(-2.5) = -3
round(-2.4) = -2
round(-2.3) = -2
round(-2.2) = -2
round(-2.1) = -2
round(-2) = -2
round(-1.9) = -2
round(-1.8) = -2
round(-1.7) = -2
round(-1.6) = -2
round(-1.5) = -2
round(-1.4) = -1
round(-1.3) = -1
round(-1.2) = -1
round(-1.1) = -1
round(-1) = -1
round(-0.9) = -1
round(-0.8) = -1
round(-0.7) = -1
round(-0.6) = -1
round(-0.5) = -1
round(-0.4) = 0
round(-0.3) = 0
round(-0.2) = 0
round(-0.1) = 0
round(0) = 0
round(0.1) = 0
round(0.2) = 0
round(0.3) = 0
round(0.4) = 0
round(0.5) = 1
round(0.6) = 1
round(0.7) = 1
round(0.8) = 1
round(0.9) = 1
round(1) = 1
round(1.1) = 1
round(1.2) = 1
round(1.3) = 1
round(1.4) = 1
round(1.5) = 2
round(1.6) = 2
round(1.7) = 2
round(1.8) = 2
round(1.9) = 2
round(2) = 2
round(2.1) = 2
round(2.2) = 2
round(2.3) = 2
round(2.4) = 2
round(2.5) = 3
round(2.6) = 3
round(2.7) = 3
round(2.8) = 3
round(2.9) = 3
round(3) = 3

I think negatives will always round to another negative or zero so this multiplication order is correct.

Dobruja answered 28/3, 2022 at 13:1 Comment(0)
I
-3

As @dandavis noted in the comments, the best way to handle this is to use .toFixed(2) instead of Math.round().

Math.round(), as you noted, rounds half up.

.toFixed() will round half away from zero. We designate the 2 in there to indicate the amount of decimals to use when doing the rounding.

Isabel answered 16/5, 2017 at 18:5 Comment(2)
I think this is misleading. .toFixed() doesn't round half away from zero. It truncates decimal places. (1.237).toFixed(2); // "1.23" when you might otherwise expect 1.24Zoroastrianism
It's definitely away from 0 but it behaves differently than C#. Absolute of what remains after the cut must be >5 in JS while in C# it must be >= 5. decimal.Round(1.255m, 2, MidpointRounding.AwayFromZero) // C# = 1.26 (1.255).toFixed(2) // JS = 1.25Alodie

© 2022 - 2024 — McMap. All rights reserved.