在Javascript中向远离零的方向舍入

8
我们正在使用Handsontable在Javascript中构建一个表格,其中包含货币金额。我们让用户可以选择在金额上显示两位小数或不显示小数位(这是客户的要求)。然后我们会发现如下情况:
Column A     Column B      Column C = A + B
-------------------------------------------
-273.50       273.50       0                 Two decimals
-273          274          0                 No decimals

经过一番调查,我们发现Javascript中基本的四舍五入函数Math.round() 的运作方式如下

如果小数部分恰好为0.5,则将参数向+∞的方向舍入到下一个整数。请注意,这与许多其他语言的round()函数不同,后者通常将此情况向远离零的下一个整数舍入(在负数且小数部分恰好为0.5的情况下会产生不同的结果)。

由于我们正在处理货币金额,我们不关心第二个小数点之后发生了什么,所以我们选择在表格中的任何负值上添加-0.0000001。 因此,当使用两个或没有小数渲染值时,我们现在获得正确的结果,例如Math.round(-273.5000001) = -274,而Math.round(-273.4900001)仍然是-273。
尽管如此,我们仍希望找到更好的解决办法。那么,最佳、最优雅的方法是什么(不需要修改原始数值)?请注意,我们不直接调用Math.round(x),而是告诉Handsontable使用给定数量的小数位数格式化值。

var rounded = (val < 0) ? Math.round(val - 0.5) : Math.round(val+0.5); - Taha Paksu
4
在处理货币时,实际上应该只使用最小面值的整数值。小数点只用于显示。这将避免许多与该领域的计算有关的问题。 - Sirko
@Sirko 这是一个有趣的观点,但是如果我们有例如 -27350 欧分(以最小面额保存),当我要在欧元面额中显示没有小数位时,我仍然会显示 -273 作为值,因为我必须先将该值除以100。 - Charlie
[33, 2.3, 53.34].map(x=>x.toFixed(2)).join(", ") == "33.00, 2.30, 53.34" thus all you need is .toFixed(2) - dandavis
1
@CarlosAlejo 我并没有建议这会解决具体的问题。这只是一个简单的提示,可能在处理货币时可以节省一些麻烦,因为浮点数问题通常比其他计算更重要。 - Sirko
3个回答

1

对于零和正数,Math.round()的工作方式符合预期。对于负数,需要先转换为正数,然后再乘回去:

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

对于这些输入,似乎工作正常:

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

我认为负数总是会舍入为另一个负数或零,所以这个乘法顺序是正确的。


1

以下是实现所需行为的一些变化方式,可以使用或不使用 Math.round()。这些函数的证明已经被验证。

哪个版本更适合您,由您决定。

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)]))
));


-3

正如@dandavis在评论中指出的那样,处理这个问题的最佳方法是使用.toFixed(2)而不是Math.round()

Math.round(),如您所指出的,会将一半四舍五入。

.toFixed()会将一半向远离零的方向舍入。我们在其中指定2以表示进行舍入时要使用的小数位数。


4
我认为这是误导性的。 .toFixed()不会将一半四舍五入。它会截断小数位。当你本来可能期望得到1.24时,(1.237).toFixed(2); // "1.23" - Jeremy Lawson
2
它绝对不是0,但它与C#的行为不同。 在JS中剪切后剩余部分的绝对值必须>5,而在C#中必须>= 5。 decimal.Round(1.255m, 2, MidpointRounding.AwayFromZero) // C# = 1.26 (1.255).toFixed(2) // JS = 1.25 - Stevie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接