Gaussian/banker's rounding in JavaScript

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

Toeven 是 Gaussian/四舍五入给你描述的会计系统的一种非常常见的舍入方法。

有人有这方面的经验吗?我在网上找到的例子,但他们没有四舍五入到一个 given小数位数..。

14213 次浏览
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/

For what looks like a more rigorous treatment of this (I've never used it), you could try this 大数 implementation.

The accepted answer does round to a given number of places. In the process it calls toFixed which converts the number to a string. Since this is expensive, I offer the solution below. It rounds a number ending in 0.5 to the nearest even number. It does not handle rounding to an arbitrary number of places.

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

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

这是不寻常的堆栈溢出,其中底部的答案比接受的更好。只是清理了一下@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);
}

严格地说,所有这些实现都应该处理整数为负数的情况。

这是一个边缘情况,但仍然明智的做法是不允许它(或者非常清楚这意味着什么,例如 -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);
  • 如果您不喜欢调用 fix ()的开销
  • 希望能够提供一个任意的规模
  • 不想引入浮点错误
  • 希望有可读的、可重用的代码

roundHalfToEven is a function that generates a fixed scale rounding function. I do my currency operations on cents, rather than dollars, to avoid introducing FPEs. The unshift param exists to avoid the overhead of unshifting and shifting again for those operations.

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

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

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

JavaScript 的 Math.round 有一个不同寻常的特性,它将半途而废的情况转向正无穷大,不管它们是正的还是负的。例如,2.5将四舍五入到3.0,而 -2.5将四舍五入到 -2.0。 来源

This is wrong, so we have to round down on negatives .5 before applying the bankers rounding, accordantly.

此外,正如 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以及使用 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

成功了!