Excel如何成功地舍入浮点数,即使它们不精确?

时间:2011-08-03 17:43:47

标签: c++ excel math floating-point rounding

例如,this blog表示0.005不完全是0.005,但舍入该数字会产生正确的结果。

我在C ++中尝试了各种舍入,但在将数字舍入到某些小数位时失败了。例如,Round(x,y)将x舍入为y的倍数。所以回合(37.785,0.01)应该给你37.79而不是37.78。

我正在重新打开这个问题,请求社区寻求帮助。问题在于浮点数的不精确性(37,785表示为37.78499999999)。

问题是Excel如何解决这个问题?

round() for float in C++中的解决方案对于上述问题不正确。

12 个答案:

答案 0 :(得分:20)

"回合(37.785,0.01)应该给你37.79而不是37.78。"

首先,没有达成共识37.79而不是37.78是"对"这里回答?断路器总是有点难度。虽然在平局的情况下总是四舍五入是一种广泛使用的方法,但它肯定不是唯一的方法。

其次,这并非打破平局。 IEEE binary64浮点格式的数值为37.784999999999997(大约)。除了人类输入值37.785之外,还有很多方法可以获得值37.784999999999997,并且碰巧将其转换为该浮点表示。在大多数情况下,正确答案是37.78而不是37.79。

<强>附录
请考虑以下Excel公式:

=ROUND(37785/1000,2)
=ROUND(19810222/2^19+21474836/2^47,2)

两个单元格将显示相同的值,37.79。关于37785/1000是否应该以37位置位或37.79计算两位数的准确性存在合理的争论。如何处理这些角落案件有点武断,并没有达成共识的答案。微软内部甚至没有达成共识的答案:&#34; 由于历史原因,Round()函数并未在不同的Microsoft产品中以一致的方式实现。&#34; (http://support.microsoft.com/kb/196652)鉴于无限精密机器,微软的VBA将在37.785到37.78(银行家轮)下,而Excel将产生37.79(对称算术轮)。

对后一个公式的四舍五入没有任何争论。它严格小于37.785,所以它应该是37.78,而不是37.79。然而Excel完善了它。为什么呢?

原因与计算机中实数的表示方式有关。与许多其他公司一样,Microsoft使用IEEE 64位浮点格式。当以这种格式表示时,数字37785/1000遭受精确损失。 19810222/2 ^ 19 + 21474836/2 ^ 47不会发生这种精度损失;这是一个&#34;确切的数字&#34;。

我有意构造了那个确切的数字,以获得与不精确的37785/1000相同的浮点表示。 Excel将这个精确值向上舍入而不是向下舍入是确定Excel ROUND()函数如何工作的关键:它是对称算术舍入的变体。它基于与角点情况的浮点表示的比较而舍入。

C ++中的算法:

#include <cmath> // std::floor

// Compute 10 to some positive integral power.
// Dealing with overflow (exponent > 308) is an exercise left to the reader.
double pow10 (unsigned int exponent) { 
   double result = 1.0;
   double base = 10.0;
   while (exponent > 0) {
      if ((exponent & 1) != 0) result *= base;
      exponent >>= 1;
      base *= base;
   }
   return result;
}   

// Round the same way Excel does.
// Dealing with nonsense such as nplaces=400 is an exercise left to the reader.
double excel_round (double x, int nplaces) {
   bool is_neg = false;

   // Excel uses symmetric arithmetic round: Round away from zero.
   // The algorithm will be easier if we only deal with positive numbers.
   if (x < 0.0) {
      is_neg = true;
      x = -x; 
   }

   // Construct the nearest rounded values and the nasty corner case.
   // Note: We really do not want an optimizing compiler to put the corner
   // case in an extended double precision register. Hence the volatile.
   double round_down, round_up;
   volatile double corner_case;
   if (nplaces < 0) {
      double scale = pow10 (-nplaces);
      round_down  = std::floor (x * scale);
      corner_case = (round_down + 0.5) / scale;
      round_up    = (round_down + 1.0) / scale;
      round_down /= scale;
   }
   else {
      double scale = pow10 (nplaces);
      round_down  = std::floor (x / scale);
      corner_case = (round_down + 0.5) * scale;
      round_up    = (round_down + 1.0) * scale;
      round_down *= scale;
   }

   // Round by comparing to the corner case.
   x = (x < corner_case) ? round_down : round_up;

   // Correct the sign if needed.
   if (is_neg) x = -x; 

   return x;
}   

答案 1 :(得分:4)

对于非常精确的任意精度和将浮点数舍入到一组固定的小数位,你应该看一下math library like GNU MPFR。虽然它是一个C库,但如果你想避免使用C,我发布的网页也会链接到几个不同的C ++绑定。

您可能还想在施乐帕洛阿尔托研究中心阅读David Goldberg撰写的题为"What every computer scientist should know about floating point arithmetic"的论文。这是一篇很好的文章,演示了基础过程,它允许浮点数在计算机中近似表示二进制数据中的所有内容,以及在基于FPU的浮点数学中如何舍入错误和其他问题。

答案 2 :(得分:3)

我不知道Excel是如何做到的,但是很好地打印浮点数是一个难题:http://www.serpentine.com/blog/2011/06/29/here-be-dragons-advances-in-problems-you-didnt-even-know-you-had/

答案 3 :(得分:2)

所以你的实际问题似乎是,如何获得正确的舍入浮点数 - &gt;字符串转换。通过谷歌搜索这些术语你会得到一堆文章,但如果你对某些东西感兴趣,大多数平台提供相当称职的sprintf()/ snprintf()实现。所以只需使用它们,如果发现错误,请向供应商提交报告。

答案 4 :(得分:2)

一个函数,它将浮点数作为参数并返回另一个浮点数,精确舍入到给定的十进制数字,不能写入,因为有许多数字具有有限的十进制表示,具有无限的二进制表示;其中一个最简单的例子是0.1。

要实现您想要的效果,您必须接受因舍入功能而使用的其他类型。如果你迫切需要打印数字,你可以使用字符串和格式化功能:问题就变成了如何获得你期望的格式。否则,如果您需要存储此编号以便对其执行精确计算,例如,如果您正在进行会计,则需要一个能够精确表示十进制数的库。在这种情况下,最常见的方法是使用缩放表示:值的整数和小数位数。将值除以10提升到比例,可以得到原始数字。

如果这些方法中的任何一种方法合适,我会尝试用实际建议扩展我的答案。

答案 5 :(得分:1)

Excel通过WORK来“正确”地舍入这样的数字。它们始于1985年,具有相当“正常”的浮点例程,并添加了一些缩放整数假浮点,并且从那以后它们一直在调整这些东西并添加特殊情况。应用程序DID曾经拥有大多数与其他人相同的“明显”错误,只是它很久以前就已经拥有了它们。当我在90年代初期为他们提供技术支持时,我自己提交了一对。

答案 6 :(得分:0)

正如mjfgates所说,Excel努力让这个“正确”。当你试图重新实现它时,要做的第一件事就是用“正确”来定义你的意思。明显的解决方案: - 实施理性算术 缓慢但可靠。 - 实施一堆启发式方法 快速但很难做到正确(想想“多年的错误报告”)。

这实际上取决于你的申请。

答案 7 :(得分:0)

正如基数为10的数字在转换为基数2时必须舍入时,可以将数字从基数2转换为基数10进行舍入。一旦数字具有基数为10的表示,则可以通过查看要舍入的数字右侧的数字以直接的方式再次舍入。

虽然上述断言没有任何问题,但还有一个更实用的解决方案。问题是二进制表示尝试尽可能接近十进制数,即使该二进制数小于十进制数。误差量在真值的[-0.5,0.5]最低有效位(LSB)内。为了舍入,你宁愿它在[0,1] LSB范围内,以便错误始终为正,但如果不改变浮点数学的所有规则,这是不可能的。

您可以做的一件事是将1 LSB加到该值,因此误差在真值的[0.5,1.5] LSB范围内。这总体上不太准确,但只有极少量;当值被舍入以表示为十进制数时,它更有可能被舍入到正确的十进制数,因为错误总是正数。

要在舍入前将值加1 LSB,请参阅this question的答案。例如,在Visual Studio C ++ 2010中,过程将是:

Round(_nextafter(37.785,37.785*1.1),0.01);

答案 8 :(得分:0)

你需要的是:

 double f = 22.0/7.0;
    cout.setf(ios::fixed, ios::floatfield);
    cout.precision(6); 
    cout<<f<<endl;  

如何实施(只是舍入最后一位数的概述) :

long getRoundedPrec(double d,   double precision = 9)
{
    precision = (int)precision;
    stringstream s;
    long l = (d - ((double)((int)d)))* pow(10.0,precision+1);
    int lastDigit = (l-((l/10)*10));
    if( lastDigit >= 5){
        l = l/10 +1;
    }
    return l;
}

答案 9 :(得分:0)

使用统计,数值...算法

可以通过多种方法优化浮点结果

最简单的可能是在精度范围内搜索重复的9或0 。如果有,可能那些9是还原剂,只需将它们围起来。但在许多情况下这可能不起作用

2.67899999 → 2.679
12.3499999 → 12.35
1.20000001 → 1.2

或者你可以包含一些精度以及浮点数。在每个步骤之后,根据操作数的精度调整精度。例如

1.113   → 3 decimal digits
6.15634 → 5 decimal digits

由于这两个数字都在双精度16-17位精度范围内,因此它们的总和将精确到它们中的较大者,即5位数。类似地,3 + 5&lt; 16,所以他们的产品将精确到8位十进制数

1.113 + 6.15634 = 7.26934    → 5 decimal digits
1.113 * 6.15634 = 6.85200642 → 8 decimal digits

但是4.1341677841 * 2.251457145只会获得双精度,因为实际结果的十进制数超过了双精度

另一种有效的算法是 Grisu ,但我没有机会尝试。

  

2010年,Florian Loitsch在PLDI中发表了一篇精彩的论文,"Printing floating-point numbers quickly and accurately with integers",代表了该领域20年来最大的一步:他主要想出如何使用机器整数来执行准确的渲染!为什么我说“大多数”?因为尽管Loitsch的“Grisu3”算法速度非常快,但它却放弃了约0.5%的数字,在这种情况下你必须回归到Dragon4或衍生物

http://www.serpentine.com/blog/2011/06/29/here-be-dragons-advances-in-problems-you-didnt-even-know-you-had/

事实上,我认为Excel必须结合许多不同的方法来实现所有

的最佳结果
  

值达到零时的示例

     

在Excel 95或更早版本中,在新工作簿中输入以下内容:

     

A1: =1.333+1.225-1.333-1.225

     

右键单击单元格A1,然后单击“设置单元格格式”。在数字选项卡上,单击类别下的科学。将小数位数设置为15.

     

Excel 95不显示0,而是显示-2.22044604925031E-16

     

然而,Excel 97引入了一种尝试纠正此问题的优化。如果加法或减法操作导致值为零或非常接近于零,则 Excel 97及更高版本将补偿由于将操作数转换为二进制文件而导致的任何错误。。在Excel 97及更高版本中执行的上述示例在科学计数法中正确显示0或0.000000000000000E + 00。

http://support.microsoft.com/kb/78113

答案 10 :(得分:0)

我相信以下C#代码会对数字进行舍入,因为它们在Excel中被舍入。要在C ++中完全复制行为,您可能需要使用特殊的十进制类型。

用简单的英语,双精度数转换为小数,然后四舍五入到十五位有效数字(不要与十五位小数混淆)。结果将第二次舍入到指定的小数位数。

这可能看起来很奇怪,但您必须了解的是Excel 总是显示的数字四舍五入到15位有效数字。如果ROUND()函数没有使用该显示值作为起点,而是使用内部双重表示,则会出现ROUND(A1,N)似乎与A1中的实际值不对应的情况。这对于非技术用户来说会非常混乱。

最接近37.785的双精度精确十进制值为37.784999999999996589394868351519107818603515625。 (任何双精度都可以用有限的十进制小数精确表示,因为四分之一,八分之一,十六分之一等等都有有限的十进制扩展。)如果这个数字直接四舍五入到小数点后两位,就没有任何关系休息,结果将是37.78。如果你首先得到15位有效数字,你将获得37.7850000000000。如果这进一步四舍五入到小数点后两位,那么你得到37.79,所以毕竟没有真正的神秘感。

    // Convert to a floating decimal point number, round to fifteen 
    // significant digits, and then round to the number of places
    // indicated.
    static decimal SmartRoundDouble(double input, int places)
    {
        int numLeadingDigits = (int)Math.Log10(Math.Abs(input)) + 1;

        decimal inputDec = GetAccurateDecimal(input);

        inputDec = MoveDecimalPointRight(inputDec, -numLeadingDigits);

        decimal round1 = Math.Round(inputDec, 15);

        round1 = MoveDecimalPointRight(round1, numLeadingDigits);

        decimal round2 = Math.Round(round1, places, MidpointRounding.AwayFromZero);

        return round2;
    }

    static decimal MoveDecimalPointRight(decimal d, int n)
    {
        if (n > 0)
            for (int i = 0; i < n; i++)
                d *= 10.0m;
        else
            for (int i = 0; i > n; i--)
                d /= 10.0m;

        return d;
    }

    // The constructor for decimal that accepts a double does
    // some rounding by default. This gets a more exact number.
    static decimal GetAccurateDecimal(double r)
    {
        string accurateStr = r.ToString("G17", CultureInfo.InvariantCulture);
        return Decimal.Parse(accurateStr, CultureInfo.InvariantCulture);
    }

答案 11 :(得分:-1)

大多数小数部分无法以二进制形式准确表示。

double x = 0.0;
for (int i = 1; i <= 10; i++)
{
  x += 0.1;
}
// x should now be 1.0, right?
//
// it isn't. Test it and see.

一种解决方案是使用BCD。它太老了。但是,它也是经过验证的。我们每天都会使用很多其他古老的想法(例如使用0代表什么......)。

另一种技术使用输入/输出缩放。这样做的好处是几乎所有数学都是整数数学。