【问题标题】:Round up midpoint rounding option in c#c# 中舍入中点舍入选项
【发布时间】:2014-08-07 15:00:04
【问题描述】:

虽然我看到了几个关于舍入的问题,但我还没有找到答案。 C#/.Net 中有什么方法可以进行中点四舍五入吗?也就是说,如果一个小数在2个整数之间,我想总是向上取整。据我所知,这是一种常见的舍入方法,所以我很惊讶它没有在标准的 Math.Round 选项中列出。

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

我能找到的最接近的是 MidpointRounding.AwayFromZero,但这会错误地处理负数,因为它们会向下舍入,而不是向上舍入。

【问题讨论】:

    标签: c# .net math rounding


    【解决方案1】:

    怎么样:

    Math.Floor(value + 0.5)
    

    编辑:上面的代码在想法上是正确的,但在现实世界中有一些极端情况的错误。

    一个严格的解决方案是:

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

    那些神奇的数字是什么?

    0.49999999999999994代表小于0.5的最大double值,即:
    1.1111111111 1111111111 1111111111 1111111111 1111111111 112 * 2-2
    加上 0.5 正好:
    1.1111111111 1111111111 1111111111 1111111111 1111111111 1112 * 2-1

    由于最后 1 位超出双精度,FPU 将其四舍五入到最接近的偶数[1]
    10.0000000000 0000000000 0000000000 0000000000 0000000000 002 * 2-1
    然后归一化为:
    1.0000000000 0000000000 0000000000 0000000000 0000000000 002 * 20
    正好是 1.0。
    当然Round(0.49999999999999994) 必须为 0,所以在这种特殊情况下我们只需返回 0。

    另一个幻数 4503599627370496.0 是 252
    大于 253 的奇数加上 0.5 舍入到最接近的偶数,例如 4503599627370497.0 + 0.5 使得:
    1.0000000000 0000000000 0000000000 0000000000 0000000000 0112 * 252
    并四舍五入:
    1.0000000000 0000000000 0000000000 0000000000 0000000000 102 * 252
    即 4503599627370498.0。

    负数也是如此:-4503599627370497.0 + 0.5 轮到 -4503599627370496.0。

    实际上我们不再需要对这么大的数字进行四舍五入,因为它们已经没有小数部分了。在这种情况下,我们需要做的就是返回给定值本身,而不需要任何操作。

    另请参阅

    备注

    [1]CLI spec 状态:

    IEC 60559:1989 中定义的舍入模式应由 CLI 设置为“舍入到最接近的数字”,CIL 和类库均不提供修改此设置的机制。

    根据 IEC 60559:1989(IEEE 754-1985),其中“四舍五入”是指“四舍五入,与偶数相等”。

    此外,CIL 和类库都没有提供修改此设置的机制,但我们可以在 Windows 上使用 CRT 函数 _controlfp_s 更改舍入模式,至少在我的环境中: Win7 SP1 64bit, Intel Core i7-2600, .NET 4.0.
    下面的实验代码对我来说效果很好:

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

    除了当参数为 -0.5 时它返回 -0 而不是 0。

    那么小数类型呢?它总是可以正常工作,不是吗?

    没有。至于十进制类型,正确的方法是:

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

    其中 7922816251426433759354395034m 几乎是十进制。MaxValue / 10。
    绝对值大于该值的十进制值不能有小数部分,因此+0.5四舍五入为偶数。

    【讨论】:

    • 您需要将Math.Floor() 的结果转换为int,但这是正确的,而且要简单得多。
    • 当心极端情况:0.49999999999999994 使用此方法将四舍五入为1.05000000000000001.0 将导致5000000000000002.0
    • @MarkDickinson 感谢您指出这一点。这是正确的。我已经验证 0.49999999999999994 + 0.5 正好是 1,这让我感到惊讶和感兴趣。我无法解释为什么。经过一番研究,我可能不得不为此创建一个新问题。
    • @Ripple:我认为某处已经存在问题;让我看看我能不能找到它。 IIRC,有一个臭名昭著的 Java 圆形错误(现已修复)具有相同的原因。它本质上只是浮点舍入的结果:该值加上 0.5 的结果不能完全表示,最终被舍入到 1.0。另一种极端情况也是舍入不精确和的结果。
    • @MarkDickinson 再次感谢!我要深入研究。
    【解决方案2】:

    编辑:这可行,但 Ripple 的答案要好得多。我将把它留在这里以供后代使用。 ;)


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

    这将为 OP 中的所有示例返回正确的值。

    【讨论】:

    • (int)(value*2)==(value*2) 是做什么的?
    • @krillgar 它更正了负值末尾为 0.5 的特殊情况。没有它,就会给出错误的答案。所以对于-87.5,答案应该是-87。
    • 我猜你可以通过value % 0.5 == 0 做到这一点?
    • 相比之下,模块化是一项相当昂贵的操作
    • @SriramSakthivel 是的,这也可以(我只是想尽可能使用最简单的浮点运算)。
    【解决方案3】:

    这是我在看到这些答案之前想出的,尽管 Ripple 的答案更清晰:

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

    即便如此,Math 类没有内置此选项作为 MidpointRounding 的选项,这很烦人且令人惊讶。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-11-05
      • 2011-06-09
      • 2016-10-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多