47

My last goal is always to round to the nearest even integer.

For example, the number 1122.5196 I want as result 1122. I have tried this options:

Math.Round(1122.5196d, 0, MidpointRounding.ToEven);       // result 1123
Math.Round(1122.5196d, 0, MidpointRounding.AwayFromZero); // result 1123

At the end, what I would like to get it is always the nearest even integer. For example:

  • 1122.51 --> 1122
  • 1122.9 --> 1122 (because the nearest int is 1123 but it is odd, and 1122 is nearer than 1124)
  • 1123.0 --> 1124 (the next even value, the next higher even value)

I only work with positive numbers.

And so on.

There are some method that do that or I should to implement my own method?

Dmitry Bychenko
  • 149,892
  • 16
  • 136
  • 186
Álvaro García
  • 15,452
  • 25
  • 82
  • 152

5 Answers5

65

Try this (let's use Math.Round with MidpointRounding.AwayFromZero in order to obtain "next even value" but scaled - 2 factor):

double source = 1123.0;

// 1124.0
double result = Math.Round(source / 2, MidpointRounding.AwayFromZero) * 2;

Demo:

double[] tests = new double[] {
     1.0,
  1123.1,
  1123.0,
  1122.9,
  1122.1,
  1122.0,
  1121.5,
  1121.0,
};

string report = string.Join(Environment.NewLine, tests
  .Select(item => $"{item,6:F1} -> {Math.Round(item / 2, MidpointRounding.AwayFromZero) * 2}"));

Console.Write(report);

Outcome:

   1.0 -> 2     // In case of tie, next even value
1123.1 -> 1124
1123.0 -> 1124  // In case of tie, next even value
1122.9 -> 1122
1122.1 -> 1122
1122.0 -> 1122
1121.5 -> 1122
1121.0 -> 1122  // In case of tie, next even value
Dmitry Bychenko
  • 149,892
  • 16
  • 136
  • 186
  • 1
    The case `1123.0 -> 1124` is also (by coincidence) a round to the _doubly even_ possibility (4 divides 1124). The case `1121.0 -> 1122` versus `1121.0 -> 1120` shows the `MidpointRounding` tie-breaker better. – Jeppe Stig Nielsen Sep 04 '18 at 13:22
  • @Jeppe Stig Nielsen: Nice test exmple! `1121.0 -> 1122` thank you very much! – Dmitry Bychenko Sep 04 '18 at 13:26
  • 2
    Do all C# compilers optimize `source / 2` into `source * 0.5`? `0.5` is exactly representable as a binary `double` so it's perfectly safe. (FP mul is about 1/3 the latency and 10x the throughput of FP division on modern x86 hardware: [Floating point division vs floating point multiplication](https://stackoverflow.com/a/45899202)) – Peter Cordes Sep 05 '18 at 06:48
8

One liner:

double RoundToNearestEven(double value) =>
    Math.Truncate(value) + Math.Truncate(value) % 2;

Fiddle

Explanation: if we have an even number with some digits after floating point, we need to just get rid of those digits. If we have an odd number, we need to do the same and then move to the next integer that is guaranteed to be even.

P.S. Thanks to @DmitryBychenko for pointing out that casting double to long is not the brightest idea.

Dmitry Korolev
  • 665
  • 4
  • 18
  • It's ok unless you are given a value *beyond* `long.MaxValue`, e.g. `value = 1e100;` – Dmitry Bychenko Sep 04 '18 at 12:26
  • @DmitryBychenko T̶h̶e̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶ ̶a̶c̶c̶e̶p̶t̶s̶ ̶p̶a̶r̶a̶m̶e̶t̶e̶r̶ ̶o̶f̶ ̶t̶y̶p̶e̶ ̶d̶o̶u̶b̶l̶e̶ ̶t̶h̶a̶t̶ ̶m̶a̶y̶ ̶h̶a̶v̶e̶ ̶u̶p̶ ̶t̶o̶ ̶1̶6̶ ̶d̶i̶g̶i̶t̶s̶,̶ ̶a̶n̶d̶ ̶t̶h̶e̶n̶ ̶c̶o̶n̶v̶e̶r̶t̶s̶ ̶i̶t̶ ̶t̶o̶ ̶l̶o̶n̶g̶ ̶t̶h̶a̶t̶ ̶i̶s̶ ̶1̶9̶ ̶d̶i̶g̶i̶t̶s̶ ̶m̶a̶x̶,̶ ̶s̶o̶ ̶i̶t̶ ̶s̶h̶o̶u̶l̶d̶ ̶b̶e̶ ̶s̶a̶f̶e̶.̶ – Dmitry Korolev Sep 04 '18 at 12:35
  • 1
    you'll fail on `(long)value`: `(long) 1e100` will not be a correct `long`: `1E+100 -> -9223372036854775808` – Dmitry Bychenko Sep 04 '18 at 12:36
  • 1
    `double RoundToNearestEven(double value) => Math.Truncate(value) + Math.Truncate(value) % 2;` if we want to *truncate* let's do it (without *unwanted* and spoiling the fun `long`) – Dmitry Bychenko Sep 04 '18 at 12:47
  • 2
    @DmitryBychenko Note that as long as the absolute value of the `double` is still so small that a `double` can actually distinguish the even whole numbers from other numbers, the conversion from `double` to `long` will not lose magnitude. From `2**53 == 9.007199254740994E+15` and up, _every_ exactly representable `double` is already an even whole number. __Of course, it is still entirely correct that casting `double` `1E+100` to `long` will lose the original number and lead to a bad result.__ Correct implementations, like the one we see now, should just return the same `double` if over `2**53`. – Jeppe Stig Nielsen Sep 04 '18 at 13:12
  • 1
    It's likely to be faster to use `2.0d * Math.truncate(value / 2.0d)`. In case of the CSE optimization not firing, there's only one `Math.truncate`. `%` for `double`s is also much more work than a division and a multiplication (I think it has to compute `q - d * Math.truncate(q / d)`, so an extra truncation and subtraction). In total, I think you have an additional two truncations and an extra subtraction. – Chai T. Rex Sep 04 '18 at 20:13
  • 1
    @ChaiT.Rex: If you're going to talk about performance, multiply by `0.5` instead of dividing by 2. (The compiler hopefully does this optimization for you because `1/2` is exactly representable as a binary float, but doing it in the source would make sure). Anyway, Dmitry Bychenko already posted that as an answer, and yes it's much faster because FP remainder is slow. – Peter Cordes Sep 05 '18 at 06:46
3

The reason you are getting the result 1123 even when using

Math.Round(1122.5196d, 0, MidpointRounding.ToEven);

is because that's exactly what you have asked the compiler to do. When rounding to even with decimals, be sure to remember that 1123.0 is even.

ie. 1122.51 rounded to even becomes 1123.0 (note that as it is a decimal, it will always keep its decimal place and therefore the .0 here makes this an even number).

Instead, I would write a function to do this, something like:

   private int round_up_to_even(double number_to_round)
    {
        int converted_to_int = Convert.ToInt32(number_to_round);
        if (converted_to_int %2 == 0) { return converted_to_int; }
        double difference = (converted_to_int + 1) - number_to_round;
        if (difference <= 0.5) { return converted_to_int + 1; }
        return converted_to_int - 1;
    }
Mark
  • 672
  • 6
  • 18
  • 7
    MidpointRounding only specifies what happens, when decimal part is exactly 0.5. When it isn't (0.5196), normal rounding rules apply. – mareko Sep 04 '18 at 12:50
  • 1
    To give a little more context, MidpointRounding.ToEven is useful in financial applications where you don't want random rounding errors to accumulate in a single direction over time. As a simple but contrived example, consider $1.005 + $0.995. (stored internally in units of cents) With MidpointRounding.ToEven, you get $2 rather than $2.01. – Ryan Sep 05 '18 at 01:53
  • 1
    @Ryan: more usually in scientific / statistical applications like simulations. But it is also called "banker's rounding". (https://en.wikipedia.org/wiki/Rounding#Round_half_to_even). Anyway, more importantly, it's the default rounding mode for IEEE floating point, and supported efficiently in hardware on x86. (Although so is truncation since x87 was obsoleted by SSE/SSE2.) – Peter Cordes Sep 05 '18 at 06:55
0

Here is an example function i found on msdn,that will only produce even nearest numbers seems to fit your case well ,

using System;
class Example
{
public static void Main()
{
  // Define a set of Decimal values.
  decimal[] values = { 1.45m, 1.55m, 123.456789m, 123.456789m, 
                       123.456789m, -123.456m, 
                       new Decimal(1230000000, 0, 0, true, 7 ),
                       new Decimal(1230000000, 0, 0, true, 7 ), 
                       -9999999999.9999999999m, 
                       -9999999999.9999999999m };
  // Define a set of integers to for decimals argument.
  int[] decimals = { 1, 1, 4, 6, 8, 0, 3, 11, 9, 10};

  Console.WriteLine("{0,26}{1,8}{2,26}", 
                    "Argument", "Digits", "Result" );
  Console.WriteLine("{0,26}{1,8}{2,26}", 
                    "--------", "------", "------" );
  for (int ctr = 0; ctr < values.Length; ctr++)
    Console.WriteLine("{0,26}{1,8}{2,26}", 
                      values[ctr], decimals[ctr], 
                      Decimal.Round(values[ctr], decimals[ctr]));
  }
}

// The example displays the following output:
//                   Argument  Digits                    Result
//                   --------  ------                    ------
//                       1.45       1                       1.4
//                       1.55       1                       1.6
//                 123.456789       4                  123.4568
//                 123.456789       6                123.456789
//                 123.456789       8                123.456789
//                   -123.456       0                      -123
//               -123.0000000       3                  -123.000
 //               -123.0000000      11              -123.0000000
//     -9999999999.9999999999       9    -10000000000.000000000
 //     -9999999999.9999999999      10    -9999999999.9999999999

"When rounding midpoint values, the rounding algorithm performs an equality test. Because of problems of binary representation and precision in the floating-point format, the value returned by the method can be unexpected."

Wesam
  • 848
  • 3
  • 15
  • 27
  • This isn't what the OP asked for. From the question: "For example, the number 1122.5196 I want as result 1122." A simple `Decimal.Round` to zero decimal places will give `1123`, not `1122` for this case. – Mark Dickinson Sep 07 '18 at 14:44
0

@Mark your snippet don't match Excel calculations. In Excel, try the following values:

  • 210.61 -> 212

  • 2.98 -> 4

  • -2,98 -> -4

    with you code:

  • 210.61 -> 210

  • 2.98 -> 2

  • -2,98 -> -4

I modified the code and it's working now:

public static double round_up_to_even(double number_to_round)
{
    var converted_to_int = Convert.ToDouble(number_to_round);
    if (converted_to_int %2 == 0) return Math.Round(converted_to_int, 0);
    
    var difference = (converted_to_int + 1) - number_to_round;
    if (difference <= 0.5) return Math.Round(converted_to_int + 1, 0);

    var vOffset=converted_to_int < 0 ? -1 : 1;
    return Math.Round(converted_to_int + vOffset, 0);
}
Community
  • 1
  • 1