ChatGPT解决这个技术问题 Extra ChatGPT

JavaScript 中的高斯/银行家四舍五入

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

MidpointRounding.ToEven 是 Gaussian/banker's rounding,这是一种非常常见的舍入方法,用于描述的会计系统 here

有人对这个有经验么?我在网上找到了例子,但它们没有四舍五入到给定的小数位数......

好问题。 this script 是您找到的示例之一吗?看起来它可能是合适的,但我不是这方面的专家:-)
很近!但不幸的是,它不适用于负数-我会对其进行一些更改并在此处发布...谢谢:)

T
Tim Down
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 实现。


我想建议另外两个补充。首先,我会检查 Math.pow(10, d) 表达式上的溢出/下溢(至少)。出现此错误并且当 decimalPlaces 为正时,返回 num,否则重新引发该异常。其次,为了补偿 IEEE 二进制转换错误,我会将 f == 0.5 更改为 f >= 0.499999999 && f <= 0.500000001 - 这取决于您选择的“epsilon”(不确定 .toFixed(epsilon) 是否足够)。那你就金了!
@Marius:好点。当我写这篇文章时,我对 JS 数字的了解很粗略,现在也好不了多少,所以我会阅读然后更新这个。
@TimDown 我发现题为“每个计算机科学家应该知道的关于浮点运算的知识”的文章非常宝贵。
实际上 evenRound(0.545,2) 应该舍入到 0.54,而不是 0.55,因此函数正确返回它。如果左边的数字是偶数,它的行为就像向下舍入一样,在这种情况下是 4。
近十年过去了,蒂姆,你仍然在用这个答案拯救人们。你传奇。
A
Adi Fairbank

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

...这是比答案更好的不寻常评论。它是相同的代码,只是压缩成 "one-liner" (并重命名):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 纯粹的混淆并不能证明单行是合理的。
你能详细说明为什么这比公认的更好吗?
x
xims

这是@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(例如:默认参数值)。
s
soegaard

接受的答案确实四舍五入到给定数量的位置。在此过程中,它调用 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);
};

您的函数不允许指定要保留的小数位数
真的。那是留给更红的:-)。阅读:我所需要的只是正确舍入为整数。
这是很好的解决方案,谢谢!这是使它适用于小数的方法。看起来我必须发布一个单独的答案来显示代码......
s
shiznit013
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 参数是为了避免这些操作的 unshift 和再次 shift 的开销。


M
Martin Braun

我对其他答案不满意。它们的代码要么过于冗长或复杂,要么无法正确舍入负数。对于负数,我们必须巧妙地修复 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) 等其他编程语言一样进行舍入。

/** * 返回提供的数字表达式,四舍五入到最接近的整数,同时四舍五入为偶数。 */ 函数 roundMidpointToEven(x) { const n = x >= 0 ? 1 : -1 // n 描述了对中点奇数舍入的调整 const r = n * Math.round(n * x) // 乘以 n 将修复负舍入 return Math.abs(x) % 1 === 0.5 && r % 2 !== 0 ? r - n : r // 如果我们处理奇数舍入的一半,我们调整 n } // 通过舍入美分进行测试: 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_ROUND_HALF_EVEN 的 PHP 以及使用 MidpointRounding.ToEven 的 C# 的相同输出:

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

成功!


D
Daniel C. Oderbolz

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

这是一个边缘情况,但仍然明智的做法是禁止它(或者非常清楚这意味着什么,例如 -2 舍入到最接近的数百)。


T
TylorS

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

function bankersRound(n, decimalPlaces) { // 为浮点精度问题创建乘数。常量乘数 = Math.pow(10, decimalPlaces); // 小数位乘以避免四舍五入问题 w/ floats const num = n * multiplier; // 使用标准舍入 const rounded = Math.round(num); // 只有奇数应该四舍五入 const shouldUseBankersRound = rounded % 2 !== 0; // 减一以确保四舍五入为偶数 const bankersRound = shouldUseBankersRound ?四舍五入 - 1:四舍五入; // 返回原始精度 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) );


此答案运行 bankersRounding(9.55, 2) 的行为不太正确,这不应导致 9.54
您应该添加一个新变量 const isMedian = Math.abs(num) % 1 === 0.5