即使Excel不精确,Excel如何成功地轮询浮动数字?

例如,

这个博客说0.005不是完全是0.005,而是四舍五入得出正确的结果。

我在我的C ++中尝试了各种Round,并且我没有将舍入数字舍去到某些小数位。 例如,Round(x,y)将x舍入为y的倍数。 所以轮(37.785,0.01)应该给你37.79而不是37.78。

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

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

在这一轮的解决scheme()在C + +的浮动是不正确的上述问题。

“(37.785,0.01)轮会给你37.79而不是37.78”。

首先,没有共识,37.79而不是37.78这里是“正确”的答案? 领带破坏者总是有点艰难。 虽然在平局中总是四舍五入是一个广泛使用的方法,但它当然不是唯一的方法。

其次,这不是一个打破平局的局面。 IEEE二进制64位浮点格式的数值大约为37.784999999999997(大约)。 有很多方法可以得到37.784999999999997的值,除了人类键入的值为37.785,碰巧有转换为浮点表示。 在大多数情况下,正确答案是37.78而不是37.79。

附录
考虑下面的Excel公式:

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

两个单元格将显示相同的值,37.79。 关于37785/1000是否应该以37.78或37.79进行两点精度的合理争论。 如何处理这些angular落案件有点武断,没有一致的答案。 微软内部甚至没有达成一致的答案:“ 由于历史原因,Round()函数不能以一致的方式在不同的Microsoft产品中实现 ”( http://support.microsoft.com/kb/196652 )无限精密的机器,微软的VBA将会达到37.785到37.78(银行家轮),而Excel将会产生37.79(对称算术轮)。

对于后一个公式的四舍五入没有任何争议。 严格低于37.785,应该是37.78,而不是37.79。 然而,Excel将其整合起来。 为什么?

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

我故意构build的确切数字与不精确的37785/1000具有相同的浮点表示。 Excel将ROUND()函数的工作方式作为确定Excel精确值的关键,是对称算术舍入的变体。 它基于与angular落案例的浮点表示进行比较来回合。

C ++中的algorithm:

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

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

您也可以在施乐帕洛阿尔托研究中心阅读大卫•戈德堡(David Goldberg)的题为“每位计算机科学家应该了解浮点运算的知识”的文章。 这是一篇很好的文章,展示了基本过程,它允许在计算机中对浮点数进行近似计算,以表示二进制数据中的所有内容,以及基于FPU的浮点math中四舍五入错误和其他问题如何蔓延。

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

所以你的实际问题似乎是,如何正确地舍入浮点 – >string转换。 通过search这些条款,你会得到一堆文章,但是如果你对某些东西感兴趣,大多数平台都提供了合理的sprintf()/ snprintf()的实现。 所以只要使用它们,如果发现错误,请向供应商提交报告。

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

为了达到你想要的效果,你必须接受使用不同的types作为舍入函数的结果。 如果你的直接需要是打印数字,你可以使用一个string和一个格式化函数:问题变成如何获得准确的格式你期望。 否则,如果您需要存储此号码以便对其进行精确计算,例如如果您正在进行会计核算,那么您需要一个能精确表示十进制数字的库。 在这种情况下,最常用的方法是使用缩放表示法:该值与小数位数的整数。 将数值除以10就可以得到原始数字。

如果这些方法中的任何一个都适合,我会试着用实际的build议扩大我的答案。

Excel通过执行“正确”操作,将这样的数字变成这样。 它们始于1985年,有一套相当“正常”的浮点例程,并增加了一些缩放整数假浮点,并且一直在调整这些东西并添加特殊情况。 应用程序DID曾经拥有大部分与其他人相同的“显而易见的”错误,只是它们在很久以前才有。 当我在90年代初期为他们提供技术支持时,我自己提交了一对夫妇。

正如mjfgates所说,Excel很难做到这一点“正确”。 当您尝试重新实现时,首先要做的是定义“正确”的含义。 明显的解决scheme: – 实施合理的algorithm慢,但可靠。 – 实施一堆启发式快速但棘手的问题(想想“多年的错误报告”)。

这真的取决于你的应用程序。

你需要的是这样的:

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

正如基数为10的数字在转换为基数2时必须舍入一样,可能会在数字从基数2转换为基数10时舍入数字。 一旦这个数字有一个以10为底的表示,就可以通过查看你想要的那个数字的右边的数字来以直接的方式再次四舍五入。

虽然上述说法没有错,但还是有更实际的解决办法。 问题是二进制表示试图尽可能接近十进制数,即使该二进制小于十进制。 错误的数量在真值的[-0.5,0.5]最低有效位(LSB)之内。 为了四舍五入的目的,你宁愿它在[0,1] LSB范围内,这样错误总是正面的,但是如果不改变所有的浮点math规则,这是不可能的。

你可以做的一件事就是增加1 LSB的值,所以错误是在真值的[0.5,1.5] LSB之内。 整体而言,这是不太准确的,但只是非常小的数量; 当该值被四舍五入以表示为十进制数时,更可能被四舍五入为正确的十进制数,因为该错误总是正的。

在四舍五入之前将1 LSB添加到该值,请参阅此问题的答案。 例如在Visual Studio C ++ 2010中,过程将是:

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

有许多方法可以使用统计,数值…algorithm来优化浮点结果

最简单的方法是在精度范围内search重复的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 <16,所以他们的产品精确到8位十进制数

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

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

另一个有效的algorithm是Grisu,但我没有机会尝试。

2010年,Florian Loitsch在PLDI上发表了一篇精彩的文章“用浮点数快速准确地打印浮点数” ,这代表了20年来在这个领域最大的一步:他主要想出了如何使用机器整数进行精确的渲染! 为什么我说“主要”? 因为尽pipeLoitsch的“Grisu3”algorithm速度很快,但它放弃了大约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或更早版本中,将以下内容input到新的工作簿中:

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

我相信下面的C#代码在Excel中四舍五入取整。 要正确复制C ++中的行为,您可能需要使用特殊的十进制types。

用简单的英文,双精度数转换成小数,然后四舍五入到十五个有效数字(不要与小数点后15位混淆)。 结果再次舍入到指定的小数位数。

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

最接近37.785的双精度十进制值为37.784999999999996589394868351519107818603515625。 (任何double都可以精确地表示为有限十进制小数,因为四分之一,八分之一,十六分之一等都有有限的十进制扩展。)如果这个数字直接四舍五入到小数点后两位,那么就不会有打破,结果将是37.78。 如果你先到15位有效数字,你会得到3778.55亿。 如果这进一步舍入到小数点后两位,那么你得到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); } 

大多数小数不能用二进制精确表示。

 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. 

一个解决scheme是使用BCD。 这是旧的。 但是,这也是真实的。 我们还有很多其他我们每天都在使用的旧想法(比如用0表示什么都没有)。

另一种技术是在input/输出时使用缩放。 这有几乎所有的math是整数math的优势。