Gaussian/banker's rounding in JavaScript

后端 未结 6 1629
天涯浪人
天涯浪人 2020-11-27 02:51

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

相关标签:
6条回答
  • 2020-11-27 03:14
    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.

    0 讨论(0)
  • 2020-11-27 03:17

    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);
    }
    
    0 讨论(0)
  • 2020-11-27 03:21
    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.

    0 讨论(0)
  • 2020-11-27 03:22

    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) );
    
    0 讨论(0)
  • 2020-11-27 03:27

    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);
    };
    
    0 讨论(0)
  • 2020-11-27 03:39

    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 讨论(0)
提交回复
热议问题