如何四舍五入到最接近的偶数?

Posted

技术标签:

【中文标题】如何四舍五入到最接近的偶数?【英文标题】:How to round to nearest even integer? 【发布时间】:2019-02-09 10:37:37 【问题描述】:

我的最后一个目标总是四舍五入到最近的偶数

例如,我想要的数字 1122.5196 作为结果 1122。我试过这个选项:

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

最后,我想得到的总是最近的偶数。例如:

1122.51 --> 1122 1122.9 --> 1122(因为最接近的 int 是 1123 但它是奇数,而11221124更接近1123.0 --> 1124下一个偶数,下一个更高偶数)

我只使用正数

等等。

有一些方法可以做到这一点,或者我应该实现自己的方法?

【问题讨论】:

您希望输入 1123.0 的结果是什么? (Math.Round 总是会为此返回 1123.0...) double result = Math.Round(source / 2) * 2; 如果你只想截断浮点数,使用Math.Truncate(number) 他并没有试图截断任何东西。他试图将数字四舍五入到最接近的偶数 在1123.0 1124的情况下,下一个偶数值。 【参考方案1】:

试试这个(让我们使用Math.RoundMidpointRounding.AwayFromZero 以获得“下一个偶数值”但缩放 - 2 因子):

double source = 1123.0;

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

演示:

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

结果:

   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

【讨论】:

1123.0 -> 1124 的情况也(巧合地)对 双偶 可能性(4 除以 1124)进行了一轮。 1121.0 -> 11221121.0 -> 1120 的案例更好地显示了 MidpointRounding 决胜局。 @Jeppe Stig Nielsen:很好的测试示例! 1121.0 -> 1122非常感谢! 所有 C# 编译器是否都将 source / 2 优化为 source * 0.50.5 完全可以表示为二进制 double,因此它非常安全。 (FP mul 大约是现代 x86 硬件上 FP 除法的 1/3 延迟和 10 倍吞吐量:Floating point division vs floating point multiplication)【参考方案2】:

一个班轮:

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

Fiddle

解释:如果我们有一个偶数,浮点数后面有一些数字,我们只需要去掉这些数字。如果我们有一个奇数,我们需要做同样的事情,然后移动到下一个保证为偶数的整数。

附:感谢@DmitryBychenko 指出将 double 转换为 long 并不是最明智的主意。

【讨论】:

你会在(long)value 上失败:(long) 1e100 不会是正确的long1E+100 -> -9223372036854775808 double RoundToNearestEven(double value) => Math.Truncate(value) + Math.Truncate(value) % 2; 如果我们想截断让我们这样做(没有unwanted和破坏乐趣long @DmitryBychenko 请注意,只要double 的绝对值仍然很小,以至于double 实际上可以将偶数与其他数字区分开来,则从double 转换为long 不会损失幅度。从2**53 == 9.007199254740994E+15 及以上,每个 完全可表示的double 已经是一个偶数。 当然,将double 1E+100 转换为long 将丢失原始数字并导致错误结果仍然是完全正确的。 正确的实现,就像我们现在看到的那样,应该如果超过2**53,则返回相同的double 使用2.0d * Math.truncate(value / 2.0d) 可能会更快。如果 CSE 优化未触发,则只有一个 Math.truncate% for doubles 也比除法和乘法要多得多(我认为它必须计算 q - d * Math.truncate(q / d),因此需要额外的截断和减法)。总的来说,我认为你有两个额外的截断和一个额外的减法。 @ChaiT.Rex:如果您要谈论性能,请乘以 0.5 而不是除以 2。(编译器希望为您进行此优化,因为 1/2 是完全可表示的作为二进制浮点数,但在源代码中这样做会确保)。无论如何,Dmitry Bychenko 已经发布了这个答案,是的,它更快,因为 FP 余数很慢。【参考方案3】:

即使在使用时也得到结果 1123 的原因

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

是因为这正是您要求编译器执行的操作。使用小数四舍五入时,请务必记住 1123.0 是偶数。

即。 1122.51 舍入到偶数变成 1123.0(请注意,因为它是小数,所以它会一直保留小数位,因此这里的 .0 使它成为偶数)。

相反,我会编写一个函数来执行此操作,something 比如:

   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;
    

【讨论】:

MidpointRounding 仅指定当小数部分正好为 0.5 时会发生什么。如果不是 (0.5196),则适用正常的舍入规则。 为了提供更多背景信息,MidpointRounding.ToEven 在您不希望随机舍入误差随时间在一个方向上累积的金融应用程序中很有用。作为一个简单但人为的例子,考虑 1.005 美元 + 0.995 美元。 (以美分为单位在内部存储)使用 MidpointRounding.ToEven,您将获得 2 美元而不是 2.01 美元。 @Ryan:更常见于科学/统计应用,如模拟。但它也被称为“银行家四舍五入”。 (en.wikipedia.org/wiki/Rounding#Round_half_to_even)。无论如何,更重要的是,它是 IEEE 浮点的默认舍入模式,并且在 x86 上的硬件中得到有效支持。 (虽然截断也是如此,因为 x87 已被 SSE/SSE2 淘汰。)【参考方案4】:

这是我在 msdn 上找到的一个示例函数,它只会产生最接近的数字 似乎很适合你的情况,

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,261,82,26", 
                    "Argument", "Digits", "Result" );
  Console.WriteLine("0,261,82,26", 
                    "--------", "------", "------" );
  for (int ctr = 0; ctr < values.Length; ctr++)
    Console.WriteLine("0,261,82,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

“当舍入中点值时,舍入算法执行相等性测试。由于浮点格式的二进制表示和精度问题,该方法返回的值可能是意外的。”

【讨论】:

这不是 OP 要求的。来自问题:“例如,我想要的数字 1122.5196 作为结果 1122。”一个简单的Decimal.Round 到零小数位将给出1123,在这种情况下不是1122【参考方案5】:

@马克 您的 sn-p 与 Excel 计算不匹配。 在 Excel 中,尝试以下值:

210.61 -> 212

2.98 -> 4

-2,98 -> -4

带上你的代码:

210.61 -> 210

2.98 -> 2

-2,98 -> -4

我修改了代码,它现在可以工作了:

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

【讨论】:

以上是关于如何四舍五入到最接近的偶数?的主要内容,如果未能解决你的问题,请参考以下文章

Python 3.x 舍入行为

在 PySpark 中,如何将时间戳值四舍五入到最接近的分钟?

如何将双精度格式设置为四舍五入到最接近的美元的货币?

在R中将时间四舍五入到最接近的小时[重复]

四舍五入到最接近的 N 数

Pandas - 将时间戳四舍五入到最接近的秒数