JavaScript中的高斯/银行家舍入

82
我一直在使用C#中的Math.Round(myNumber, MidpointRounding.ToEven)进行服务器端舍入,但是用户需要'实时'了解服务器端操作的结果,这意味着(避免Ajax请求)需要创建一个JavaScript方法来复制C#使用的MidpointRounding.ToEven方法。
MidpointRounding.ToEven是高斯/银行家舍入,是会计系统中非常常见的舍入方法,可以在此处描述。
有没有人有相关经验?我已经在网上找到了示例,但它们不能将数字舍入到给定的小数位数...

好问题。这个脚本是你找到的例子之一吗?看起来可能适合,但我对这个主题不是专家 :-) - Andy E
很接近了!但不幸的是,它不能处理负数 - 我会对它进行一些更改并在这里发布...谢谢 :) - Jimbo
对于在2020年代阅读此文并使用Intl.NumberFormat的所有人,@Gen1-1在这个评论中提到的是正确的答案:roundingMode: 'halfEven'MDN)。 - undefined
11个回答

115
function evenRound(num, decimalPlaces) {
    var d = decimalPlaces || 0;
    var m = Math.pow(10, d);
    var n = +(d ? num * m : num).toFixed(8); // Avoid rounding errors
    var i = Math.floor(n), f = n - i;
    var e = 1e-8; // Allow for rounding errors in f
    var r = (f > 0.5 - e && f < 0.5 + e) ?
                ((i % 2 == 0) ? i : i + 1) : Math.round(n);
    return d ? r / m : r;
}

console.log( evenRound(1.5) ); // 2
console.log( evenRound(2.5) ); // 2
console.log( evenRound(1.535, 2) ); // 1.54
console.log( evenRound(1.525, 2) ); // 1.52

实时演示:http://jsfiddle.net/NbvBp/

如果要看更严谨的处理方式(我从未使用过),你可以尝试这个BigNumber 实现。


3
我想建议两个增加的内容。首先,我会检查 Math.pow(10, d) 表达式是否存在溢出/下溢问题(至少要检查这个)。如果发生错误且 decimalPlaces 为正数,则返回 num,否则重新引发异常。其次,为了弥补 IEEE 二进制转换误差,我会将 f == 0.5 改为类似于 f >= 0.499999999 && f <= 0.500000001 的东西 - 取决于您选择的“epsilon”(不确定 .toFixed(epsilon) 是否足够)。然后你就完成啦! - Marius
@Marius:说得好。我写这篇文章时对JS数字的了解很肤浅,现在也没多少进步,所以我会去查阅资料后再更新它。 - Tim Down
@TimDown 我发现题为“计算机科学家应该了解的浮点数算术知识”一文非常有价值。 - Marius
3
实际上,即使是 evenRound(0.545,2),也应该舍入为0.54而不是0.55,因此该函数返回正确的结果。如果左侧数字为偶数,则向下舍入,这在此例中是4。 - Alex Burdusel
2
在过去的十年中,你仍然用这个答案拯救人们,Tim。你真是传奇。 - Dwayne Charrington
显示剩余6条评论

20

这是一个不同寻常的stackoverflow,底部的回答比被接受的回答更好。 我只是整理了@xims的解决方案并让它更易读一些:

function bankersRound(n, d=2) {
    var x = n * Math.pow(10, d);
    var r = Math.round(x);
    var br = Math.abs(x) % 1 === 0.5 ? (r % 2 === 0 ? r : r-1) : r;
    return br / Math.pow(10, d);
}

1
这是一个比答案更好的不寻常的评论。它是相同的代码,只是压缩成一个“一行代码”(并重命名):**function round2(n,d=2){var n=n*Math.pow(10,d),o=Math.round(n);return(Math.abs(n)%1==.5?o%2==0?o:o-1:o)/Math.pow(10,d)}**(降低可读性是一个额外的奖励)。 - ashleedawg
4
@ashleedawg 纯粹的混淆并不能证明一个一行代码的价值。 - Martin Braun
1
你能详细说明为什么这比已接受的更好吗? - Charles Wood
1
@CharlesWood 它不依赖于返回字符串的 toFixed() 方法,并且总体上使用更少的操作。 - undefined
2
我选择这个答案作为四舍五入的选择。如果你想要一个小数,toFixed()单独使用似乎是有效的,但是当你想要将其四舍五入到没有小数时,它返回的是一个字符串。内置的Intl.NumberFormat有一个roundingMode选项,你可以将其设置为halfEven,这个选项可以工作,但是它也返回一个字符串,并且带有逗号格式化。 - undefined

10

这是@soegaard提出的一个很好的解决方案。这里有一个小变化,可以使它支持小数点:

bankers_round(n:number, d:number=0) {
    var x = n * Math.pow(10, d);
    var r = Math.round(x);
    var br = (((((x>0)?x:(-x))%1)===0.5)?(((0===(r%2)))?r:(r-1)):r);
    return br / Math.pow(10, d);
}

在此期间,以下是一些测试:

console.log(" 1.5 -> 2 : ", bankers_round(1.5) );
console.log(" 2.5 -> 2 : ", bankers_round(2.5) );
console.log(" 1.535 -> 1.54 : ", bankers_round(1.535, 2) );
console.log(" 1.525 -> 1.52 : ", bankers_round(1.525, 2) );

console.log(" 0.5 -> 0 : ", bankers_round(0.5) );
console.log(" 1.5 -> 2 : ", bankers_round(1.5) );
console.log(" 0.4 -> 0 : ", bankers_round(0.4) );
console.log(" 0.6 -> 1 : ", bankers_round(0.6) );
console.log(" 1.4 -> 1 : ", bankers_round(1.4) );
console.log(" 1.6 -> 2 : ", bankers_round(1.6) );

console.log(" 23.5 -> 24 : ", bankers_round(23.5) );
console.log(" 24.5 -> 24 : ", bankers_round(24.5) );
console.log(" -23.5 -> -24 : ", bankers_round(-23.5) );
console.log(" -24.5 -> -24 : ", bankers_round(-24.5) );

注意:其中一些约定仅适用于ES6(例如:默认参数值)。 - Spade

7

接受的答案会四舍五入到指定的位数。在此过程中,它会调用toFixed将数字转换为字符串。由于这很昂贵,因此我提供以下解决方案。它将以0.5结尾的数字四舍五入为最近的偶数。它不处理任意位数的四舍五入。

function even_p(n){
  return (0===(n%2));
};

function bankers_round(x){
    var r = Math.round(x);
    return (((((x>0)?x:(-x))%1)===0.5)?((even_p(r))?r:(r-1)):r);
};

您的函数不允许指定要保留的小数位数。 - Alex Burdusel
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - soegaard
这是一个很好的解决方案,谢谢!以下是如何使其适用于小数的方法。看起来我需要发布一个单独的答案来展示代码... - xims

3

我对其他答案不满意,它们要么代码太冗长或复杂,要么不能正确处理负数的四舍五入。对于负数,我们必须巧妙地修复JavaScript的一个奇怪行为:

JavaScript的Math.round有一个不同寻常的属性,即它将四舍五入中间值向正无穷方向舍入,而不管它们是正数还是负数。所以例如2.5将会四舍五入到3.0,但-2.5将会四舍五入到-2.0。 来源

这是错误的,因此我们必须在负数的 .5 上进行向下舍入,然后再应用银行家舍入规则。

另外,就像Math.round一样,我想将结果四舍五入到下一个整数并强制精度为0。我只想使用正确且固定的“四舍六入五成双”方法来进行正负舍入。它需要和其他编程语言如PHP (PHP_ROUND_HALF_EVEN) 或C#(MidpointRounding.ToEven) 一样进行舍入。

/**
 * Returns a supplied numeric expression rounded to the nearest integer while rounding halves to even.
 */
function roundMidpointToEven(x) {
  const n = x >= 0 ? 1 : -1 // n describes the adjustment on an odd rounding from midpoint
  const r = n * Math.round(n * x) // multiplying n will fix negative rounding
  return Math.abs(x) % 1 === 0.5 && r % 2 !== 0 ? r - n : r // we adjust by n if we deal with a half on an odd rounded number
}

// testing by rounding cents:
for(let i = -10; i <= 10; i++) {
  const val = i + .5
  console.log(val + " => " + roundMidpointToEven(val))
}

Math.round 和我们自定义的 roundMidpointToEven 函数都不会考虑精度,因为无论如何,使用分计算总是更好,以避免在任何计算中出现浮点问题。

然而,如果您不涉及分,您可以像对 Math.round 所做的那样,乘以和除以相应的小数位数因子:

const usd = 9.225;
const fact = Math.pow(10, 2) // A precision of 2, so 100 is the factor
console.log(roundMidpointToEven(usd * fact) / fact) // outputs 9.22 instead of 9.23

为了完全验证自定义的 roundMidpointToEven 函数,以下是使用 PHP 的官方 PHP_ROUND_HALF_EVEN 和使用 C# 的 MidpointRounding.ToEven 得到相同输出的代码:
for($i = -10; $i <= 10; $i++) {
    $val = $i + .5;
    echo $val . ' => ' . round($val, 0, PHP_ROUND_HALF_EVEN) . "<br />";
}

for(int i = -10; i <= 10; i++) 
{
    double val = i + .5;
    Console.WriteLine(val + " => " + Math.Round(val, MidpointRounding.ToEven));
}

两个代码片段的输出结果相同,与我们自定义的 roundMidpointToEven 测试调用相似。
-9.5 => -10
-8.5 => -8
-7.5 => -8
-6.5 => -6
-5.5 => -6
-4.5 => -4
-3.5 => -4
-2.5 => -2
-1.5 => -2
-0.5 => 0
0.5 => 0
1.5 => 2
2.5 => 2
3.5 => 4
4.5 => 4
5.5 => 6
6.5 => 6
7.5 => 8
8.5 => 8
9.5 => 10
10.5 => 10

成功!


2
const isEven = (value: number) => value % 2 === 0;
const isHalf = (value: number) => {
    const epsilon = 1e-8;
    const remainder = Math.abs(value) % 1;

    return remainder > .5 - epsilon && remainder < .5 + epsilon;
};

const roundHalfToEvenShifted = (value: number, factor: number) => {
    const shifted = value * factor;
    const rounded = Math.round(shifted);
    const modifier = value < 0 ? -1 : 1;

    return !isEven(rounded) && isHalf(shifted) ? rounded - modifier : rounded;
};

const roundHalfToEven = (digits: number, unshift: boolean) => {
    const factor = 10 ** digits;

    return unshift
        ? (value: number) => roundHalfToEvenShifted(value, factor) / factor
        : (value: number) => roundHalfToEvenShifted(value, factor);
};

const roundDollarsToCents = roundHalfToEven(2, false);
const roundCurrency = roundHalfToEven(2, true);
  • 如果您不喜欢调用 toFixed() 的开销
  • 想要能够提供任意精度
  • 不想引入浮点误差
  • 想要拥有可读性强、可重用的代码

roundHalfToEven 是一个生成固定精度舍入函数的函数。我在货币操作中使用美分而不是美元,以避免引入 FPEs。unshift 参数存在是为了避免这些操作的 unshifting 和 shifting 开销。


1

严格来说,所有这些实现都应该处理要舍入的负数位数的情况。

这是一个边缘情况,但仍然明智的做法是禁止它(或非常清楚地说明其含义,例如-2是舍入到最接近的百位数)。


0

这个解决方案比当前任何答案都更加优雅。它正确处理了负数四舍五入和负小数位数。

function bankersRound (value, nDec = 2) {
    let x = value * Math.pow(10, nDec);
    let r = Math.round(x);
    return (Math.abs(x) % 1 === .5 ? r - (r % 2) : r) / Math.pow(10, nDec);
}

0
在我的情况下,我已经有了要四舍五入的数字,它是一个整数和小数位数(例如1.23存储为{m: 123, e: 2})。(在处理货币时,这通常是一个很好的方法。)要对这样的数字进行四舍五入,可以执行以下操作:
function round({e, m}, places) {
    if (e < places) return {e, m};
    const frac = m / (2 * 10 ** (e - places));
    const rnd = Math.abs(frac % 1);
    const offs = Math.sign(frac) * ((rnd > 0.25) + (rnd >= .75));
    return {e: places, m: Math.trunc(frac) * 2 + offs};
}

这个想法是,由于emplaces都是整数,当结果处于舍入断点时,m / (2 * 10 ** (e - places))将是精确的。


0

这个也利用了Math.round在带有小数.5的值上向+Infinity四舍五入的事实。负数和正数都会四舍五入到最接近的偶数。

let roundGaussian = num => {
  const sign = Math.sign(num);
  num = Math.abs(num);
  if (Math.floor(num % 2) !== 0) return Math.round(num) * sign;
  else return Math.abs(Math.round(-num)) * sign;
}

let tests = [123.5, 234.5, -123.5, -234.5];

for (let n of tests) console.log(roundGaussian(n));
// 124
// 234
// -124
// -234

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