82

I have been using Math.Round(myNumber, MidpointRounding.ToEven) in C# to do my server-side rounding, however, the user needs to know 'live' what the result of the server-side operation will be which means (avoiding an Ajax request) creating a JavaScript method to replicate the MidpointRounding.ToEven method used by C#.

MidpointRounding.ToEven is Gaussian/banker's rounding, a very common rounding method for accounting systems described here.

Does anyone have any experience with this? I have found examples online, but they do not round to a given number of decimal places...

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Jimbo
  • 22,379
  • 42
  • 117
  • 159
  • Good question. Was [this script](http://dansnetwork.com/2009/11/30/round-half-to-even-with-javascript/) one of the examples you found? It looks like it might be suitable but I'm no expert on the subject :-) – Andy E Jun 24 '10 at 10:26
  • Its close! But unforunately doesnt work with negative numbers - I'll do some changes to it and post here... Thanks :) – Jimbo Jun 27 '10 at 11:12

11 Answers11

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

Live demo: http://jsfiddle.net/NbvBp/

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

Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • 3
    I'd like to suggest two more additions. Firstly I would check for overflow/underflow on the `Math.pow(10, d)` expression (at least). On this error AND when decimalPlaces is positive, return num, else re-raise that exception. Secondly, to compensate for IEEE binary conversion errors, I would change `f == 0.5` to something like `f >= 0.499999999 && f <= 0.500000001` - depending on your choice of 'epsilon' (not sure if .toFixed(epsilon) is enough). Then you're golden! – Marius Oct 04 '13 at 15:32
  • @Marius: Good points. My knowledge of JS numbers was sketchy when I wrote this and not much better now, so I'll read up and then update this. – Tim Down Oct 04 '13 at 15:40
  • @TimDown I found the article entitled "What Every Computer Scientist Should Know About Floating-Point Arithmetic" to be invaluable. – Marius Oct 10 '13 at 15:38
  • unless I'm reading this wrong it falls over at [4 decimal places](http://jsfiddle.net/NbvBp/13/). I was expecting `outputRound(1.5255, 2);` to return `1.52` but it returned `1.53`? – Liam Dec 03 '14 at 17:03
  • [even wors](http://jsfiddle.net/NbvBp/14/)e I'd expect `outputRound(1.5254, 2)` to definitely return `1.52` but it doesn't: `1.53` – Liam Dec 03 '14 at 17:05
  • What should `outputRound(31475.005, 2)` return? The two options IMO are 31475 and 31475.01 which both end with an odd digit, so which one should be chosen? It's currently returning 31475 but I don't know if that's correct. I guess we should interpret it as 31475.00 which ends with an even digit... – Adriaan Koster Feb 04 '15 at 15:48
  • I think that evenRound(0.545,2) should be 0.55, but it gives 0.54 – NahuelGQ Dec 04 '15 at 21:54
  • 3
    Actually evenRound(0.545,2) should round to 0.54, not 0.55, therefore the function returns it correctly. It behaves as for round down if the digit to the left is even, which in this case is 4. – Alex Burdusel Dec 08 '15 at 20:11
  • I know its rounding to a fixed length with midpoint rounding away zero ... but not for this - >112.225 with 2 decimal places -> 112.22 , I guess it should be 112.23 – Jay Mar 21 '17 at 05:24
  • 2
    Almost ten years on and you're still saving people with this answer, Tim. You legend. – Dwayne Charrington Jul 01 '19 at 04:32
  • It works but it sure does look like 2010 JavaScript. We need a champion to refactor. ;) – Langdon Mar 30 '21 at 19:15
18

This is the unusual stackoverflow where the bottom answers are better than the accepted. Just cleaned up @xims solution and made a bit more legible:

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);
}
Adi Fairbank
  • 369
  • 3
  • 2
  • 1
    ...and this is the unusual comment that is better than the answer. It's the same code, only compressed into a *"one-liner"* (and renamed): **`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)}`** (the reduced legibility is a bonus). – ashleedawg Nov 10 '21 at 08:23
  • 4
    @ashleedawg Pure obfuscation does not justify a one-liner. – Martin Braun May 17 '22 at 00:52
  • 1
    Can you elaborate on why this is better than the accepted? – Charles Wood Jun 07 '22 at 02:54
10

That's a great solution from @soegaard. Here is a small change that makes it work for decimal points:

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

And while at it - here are some tests:

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
  • 1,570
  • 17
  • 22
7

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
  • 30,661
  • 4
  • 57
  • 106
  • your function does not allow specifying the number of decimals to keep – Alex Burdusel Dec 08 '15 at 19:57
  • True. That is left for the redder :-). Read: all I needed was correct rounding to an integer. – soegaard Dec 08 '15 at 20:00
  • That's great solution, thank you! Here is how you make it work for decimals. Looks like I'll have to post a separate answer to show the code... – xims Oct 12 '16 at 13:11
3

I was not happy with the other answers. They have either too verbose or complicated code or fail to round properly for negative numbers. For negative numbers we have to cleverly fix a weird behavior of JavaScript:

JavaScript's Math.round has the unusual property that it rounds halfway cases towards positive infinity, regardless of whether they're positive or negative. So for example 2.5 will round to 3.0, but -2.5 will round to -2.0. Source

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

Also, just as Math.round, I want to round to the next integer and enforce a precision of 0. I just want Math.round with the correct and fixed "round halves to even" method in positive and negative. It needs to round the same like in other programming languages such as PHP (PHP_ROUND_HALF_EVEN) or 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 as well as our custom roundMidpointToEven function won't care for precision, because it's always better to calculate with cents to avoid float-point issues on any calculations anyways.

However, if you don't deal with cents you can simply multiply and divide the appropriate factor for the number of decimal placeholders in the same way you would do it for 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

To fully validate the custom roundMidpointToEven function, here is the same output using PHP with its official PHP_ROUND_HALF_EVEN as well as C# using 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));
}

Both snippets return the same like the test call of our custom 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

Success!

Martin Braun
  • 10,906
  • 9
  • 64
  • 105
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);
  • If you do not like the overhead of calling toFixed()
  • Want to be able to supply an arbitrary scale
  • Don't want to introduce floating-point errors
  • Want to have readable, reusable code

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.

shiznit013
  • 21
  • 3
1

Stricly speaking, all of these implementations should handle the case of a negative number of digits to round to.

It is an edge case, but still it would be wise to disallow it (or be very clear about what that means, for example -2 is rounding to the nearest amount of hundreds).

0

This solution is slightly more elegant than any of the current answers. It handles rounding negative numbers and negative number of decimal places correct.

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);
}
Thomas Jensen
  • 41
  • 1
  • 4
0

This one also leverages the fact that Math.round rounds towards +Infinity on values with decimal .5. Negative and positive numbers all round to nearest even number.

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
BobRodes
  • 5,990
  • 2
  • 24
  • 26
0

In my situation, I already have the number I want to round as an integer and number of decimal places (eg 1.23 is stored as {m: 123, e: 2}. (This is generally a good approach when working with currencies.) To round a number like that, you can do

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

The idea is that since e, m, and places are whole numbers, m / (2 * 10 ** (e - places)) will be exact when the result is on a rounding breakpoint.

Dan
  • 12,409
  • 3
  • 50
  • 87
-1

For folks who want to be able to read the code a little better, here's an alternative implementation that seems to work.

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)
);
TylorS
  • 139
  • 7