JavaScript 中的高斯/银行家舍入

问题描述 投票:0回答:11

我一直在 C# 中使用

Math.Round(myNumber, MidpointRounding.ToEven)
进行服务器端舍入,但是,用户需要“实时”知道服务器端操作的结果是什么,这意味着(避免 Ajax 请求)创建一个 JavaScript 方法来复制 C# 使用的
MidpointRounding.ToEven
方法。

MidpointRounding.ToEven 是 Gaussian/banker's rounding,这是 here.

描述的一种非常常见的会计系统舍入方法

有人有这方面的经验吗?我在网上找到了示例,但它们没有四舍五入到 given 小数位数...

javascript rounding
11个回答
114
投票
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 实现。


18
投票

这是不寻常的 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);
}

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

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

3
投票

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

JavaScript 的 Math.round 有一个不寻常的属性,它会将中途情况四舍五入到正无穷大,无论它们是正数还是负数。因此,例如 2.5 将四舍五入为 3.0,但 -2.5 将四舍五入为 -2.0。 来源

这是错误的,因此我们必须在应用银行家四舍五入之前对负数进行四舍五入

.5
,相应地。

此外,就像

Math.round
一样,我想四舍五入到下一个整数并强制精度为0。我只希望
Math.round
在正数和负数中使用正确和固定的“四舍五入到偶数”方法。它需要像其他编程语言一样四舍五入,例如 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 是一个生成固定比例舍入函数的函数。我使用美分而不是美元进行货币操作,以避免引入 FPE。 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
投票

这个还利用了这样一个事实,即

Math.round
对带小数点
+Infinity
的值四舍五入到
.5
。负数和正数四舍五入到最接近的偶数。

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

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};
}

想法是,由于

e
m
places
是整数,所以当结果在舍入断点上时,
m / (2 * 10 ** (e - places))
将是精确的。


-1
投票

对于希望能够更好地阅读代码的人来说,这里有一个似乎可行的替代实现。

function bankersRound(n, decimalPlaces) {
  // Create our multiplier for floating point precision issues.
  const multiplier = Math.pow(10, decimalPlaces);
  // Multiple by decimal places to avoid rounding issues w/ floats
  const num = n * multiplier;
  // Use standard rounding
  const rounded = Math.round(num);
  // Only odd numbers should be rounded
  const shouldUseBankersRound = rounded % 2 !== 0;
  // Subtract one to ensure the rounded number is even
  const bankersRound = shouldUseBankersRound ? rounded - 1 : rounded;
  // Return to original precision
  return bankersRound / multiplier;
}

console.log(
  bankersRound(1.5255, 2),
  bankersRound(1.53543, 2),
  bankersRound(1.54543, 2),
  bankersRound(1.54543, 3),
  bankersRound(1.53529, 4),
  bankersRound(1.53529, 2),
  bankersRound(4.5, 0),
  bankersRound(5.5, 0),
  bankersRound(0.045, 2),
  bankersRound(0.055, 2)
);

© www.soinside.com 2019 - 2024. All rights reserved.