3

Although I've seen several questions about rounding, I haven't found an answer to this. Is there any way in C# / .Net to do midpoint rounding up? That is to say, if a decimal is halfway between 2 integers, I want to always round UP. As far as I know, this is a common rounding method, so I'm surprised that it's not listed in the standard Math.Round options.

87.3 -> 87
87.8 -> 88
87.5 -> 88
-87.3 -> -87
-87.8 -> -88
-87.5 -> -87

The closest I could find was MidpointRounding.AwayFromZero, but this handles negatives incorrectly, as they will round down, not up.

Mark Dickinson
  • 29,088
  • 9
  • 83
  • 120
GendoIkari
  • 11,734
  • 6
  • 62
  • 104

3 Answers3

8

How about:

Math.Floor(value + 0.5)

EDIT: The code above is correct in idea, but has some corner case bugs in the real world.

A strict solution would be:

static double Round(double val)
{
    // 0.49999999999999994 + 0.5 makes 1.
    if (val == 0.49999999999999994)
        return 0;

    // 4503599627370497.0 + 0.5 makes 4503599627370498.0.
    if (val <= -4503599627370496.0 || 4503599627370496.0 <= val)
        return val;

    return Math.Floor(val + 0.5);
}

What are those magic numbers?

0.49999999999999994 represents the greatest double value less than 0.5, that is:
1.1111111111 1111111111 1111111111 1111111111 1111111111 112 * 2-2
and adding 0.5 makes exactly:
1.1111111111 1111111111 1111111111 1111111111 1111111111 1112 * 2-1

Since the last 1 bit exceeds the double-precision, the FPU rounds it to nearest even[1]:
10.0000000000 0000000000 0000000000 0000000000 0000000000 002 * 2-1
then normalizes to:
1.0000000000 0000000000 0000000000 0000000000 0000000000 002 * 20
that is exactly 1.0.
Of course Round(0.49999999999999994) must be 0, so we simply return 0 in that special case.

The other magic number 4503599627370496.0 is 252.
Odd numbers greater than that and less than 253 plus 0.5 rounds up to nearest even as well, for example 4503599627370497.0 + 0.5 makes:
1.0000000000 0000000000 0000000000 0000000000 0000000000 0112 * 252
and rounds to:
1.0000000000 0000000000 0000000000 0000000000 0000000000 102 * 252
that is 4503599627370498.0.

So are the negative of those: -4503599627370497.0 + 0.5 rounds to -4503599627370496.0.

Actually we have no longer need to round such big numbers because they already don't have decimal part. All we need to do is return the given value itself without any operations in that situation.

See also

Notes

[1] CLI spec states:

The rounding mode defined in IEC 60559:1989 shall be set by the CLI to “round to the nearest number,” and neither the CIL nor the class library provide a mechanism for modifying this setting.

where "round to the nearest number" means "round to the nearest, ties to even", according to IEC 60559:1989(IEEE 754-1985).

In addition, neither the CIL nor the class library provide a mechanism for modifying this setting indeed, but we can change the rounding mode using CRT function _controlfp_s on Windows, at least on my environment: Win7 SP1 64bit, Intel Core i7-2600, .NET 4.0.
My experimental code below worked fine for me:

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int _controlfp_s(IntPtr currentControl, uint newControl, uint mask);

static double RoundWithFPC(double val)
{
    // Set the round mode as "Round towards negative infinity."
    _controlfp_s(IntPtr.Zero, 0x100 /* _RC_DOWN */, 0x300 /* _MCW_RC */);
    double rounded = Math.Floor(val + 0.5);
    // Restore the round mode.
    _controlfp_s(IntPtr.Zero, 0x000 /* _RC_NEAR */, 0x300 /* _MCW_RC */);
    return rounded;
}

except that it returns -0 instead of 0 when the argument is -0.5.

So how about the decimal type? It works always fine, doesn't it?

No. As for decimal type, a correct method would be:

static decimal Round(decimal val)
{
    // 7922816251426433759354395035m + 0.5m makes 7922816251426433759354395036m.
    if (val <= -7922816251426433759354395034m || 7922816251426433759354395034m <= val)
        return val;

    return Math.Floor(val + 0.5m);
}

where 7922816251426433759354395034m is nearly decimal.MaxValue / 10.
A decimal value whose absolute is greater than that cannot have decimal part, thus it + 0.5 rounds to even.

Community
  • 1
  • 1
Ripple
  • 1,257
  • 1
  • 9
  • 15
  • You'll need to cast the result of `Math.Floor()` to an `int`, but this is correct, and much simpler. – krillgar Aug 07 '14 at 15:40
  • 1
    Beware of corner cases: `0.49999999999999994` will round up to `1.0` with this method, and `5000000000000001.0` will result in `5000000000000002.0`. – Mark Dickinson Aug 08 '14 at 07:18
  • @MarkDickinson Thank you for pointing it out. That's right. I've verified that 0.49999999999999994 + 0.5 makes exactly 1, which surprised and interested me. I can't explain why. I might have to create a new question for that after some researching. – Ripple Aug 08 '14 at 14:30
  • @Ripple: I think there's already a question somewhere; let me see if I can find it. IIRC, there's an infamous Java round bug (now fixed) that had the same cause. It's essentially just a result of floating-point rounding: the result of that value plus 0.5 isn't exactly representable, and ends up being rounded up to 1.0. The other corner case is also a result of rounding an inexact sum. – Mark Dickinson Aug 08 '14 at 17:50
  • @MarkDickinson Thanks again! I'm gonna dig into that. – Ripple Aug 09 '14 at 02:37
  • @MarkDickinson Took so long, but now edited my answer. Please check it out if you had time. – Ripple Aug 23 '14 at 10:26
  • Thank you for upvoting and giving me a chance to acquire new knowledge. – Ripple Aug 23 '14 at 15:03
1

EDIT: This works, but Ripple's answer is much better. I'll leave this here for posterity. ;)


public static int Round(double value)
{
    if ((value > 0) || (int)(value*2)==(value*2))
        return (int)(value + 0.5);
    else
        return (int)(value - 0.5);
}

This returns the correct values for all the examples in the OP.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
0

This is what I came up with before I saw these answers, though Ripple's answer is much cleaner:

if (value - Math.Floor(value) == .5m)
{
    value = Math.Ceiling(value);
}
else
{
    value= Math.Round(value);
}

Even so, it is annoying and surprising that the Math class doesn't have this built in as an option for MidpointRounding.

GendoIkari
  • 11,734
  • 6
  • 62
  • 104