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

C++ Excel如何成功地将浮点数舍入,即使它们不精确?,c++,excel,math,floating-point,rounding,C++,Excel,Math,Floating Point,Rounding,例如,表示0.005不完全是0.005,但将该数字四舍五入会产生正确的结果 我尝试了C++中的所有舍入,当把数字舍入到小数点时,它失败了。例如,舍入(x,y)将x舍入为y的倍数。所以四舍五入(37.785,0.01)应该是37.79,而不是37.78 我重新提出这个问题是为了向社区寻求帮助。问题在于浮点数的不精确性(37785表示为37.784999999) 问题是Excel如何解决这个问题 上述问题的解决方案是不正确的。我不知道Excel是如何做到的,但很好地打印浮点数是一个困难的问题:Exc

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

我尝试了C++中的所有舍入,当把数字舍入到小数点时,它失败了。例如,舍入(x,y)将x舍入为y的倍数。所以四舍五入(37.785,0.01)应该是37.79,而不是37.78

我重新提出这个问题是为了向社区寻求帮助。问题在于浮点数的不精确性(37785表示为37.784999999)

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


上述问题的解决方案是不正确的。

我不知道Excel是如何做到的,但很好地打印浮点数是一个困难的问题:

Excel通过做功“正确”地舍入这样的数字。他们从1985年开始,使用了一组相当“正常”的浮点例程,并添加了一些缩放的整数伪浮点,从那时起他们一直在调整这些东西并添加特殊情况。该应用程序确实曾经有过大多数与其他人相同的“明显”错误,只是它在很久以前就有了这些错误。90年代初,我为他们提供技术支持时,我自己也提交了一份申请

正如mjfgates所说,Excel为实现“正确”目标付出了艰苦的努力。当你试图重新实现这一点时,首先要做的是定义你所说的“正确”是什么意思。显而易见的解决方案:

  • 实现有理运算

    缓慢但可靠

  • 实现一系列启发式算法

    快速但很难做到正确(想想“多年的bug报告”)


这实际上取决于您的应用程序。

所以您的实际问题似乎是,如何正确地进行浮点->字符串的四舍五入转换。通过谷歌搜索这些术语,你会得到一大堆文章,但如果你对一些有用的东西感兴趣,大多数平台都提供了相当有能力的sprintf()/snprintf()实现。因此,只要使用这些,如果发现错误,就向供应商提交报告。

正如base-10数字在转换为base-2时必须四舍五入一样,也可以在数字从base-2转换为base-10时四舍五入。一旦数字有了以10为基数的表示形式,就可以通过查看要舍入的数字右侧的数字,以直观的方式再次对其进行舍入

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

您可以做的一件事是将1 LSB添加到该值中,因此误差在真值的[0.5,1.5]LSB范围内。总的来说,这是不太准确的,但只是非常小的数量;如果将值四舍五入表示为十进制数,则更可能四舍五入为正确的十进制数,因为误差始终为正

要在舍入值之前向该值添加1 LSB,请参阅的答案。例如,VisualStudioC++ 2010中的程序是:

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

要获得非常精确的任意精度,并将浮点数四舍五入到一组固定的小数位数,您应该看一看。虽然它是一个C库,但是如果你想避免使用C.</P>,我所发布的网页也链接到几个不同的C++绑定。
你可能还想在施乐帕洛阿尔托研究中心读一篇题为大卫·戈德伯格的论文。这是一篇优秀的文章,展示了允许在一台用二进制数据表示一切的计算机中对浮点数进行近似计算的基本过程,以及在基于FPU的浮点数学中舍入误差和其他问题是如何逐渐增加的。

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

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.
double x=0.0;

对于(inti=1;i您需要的是:

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

以浮点数为参数并返回另一个浮点数(精确舍入到给定数量的十进制数字)的函数无法写入,因为有许多具有有限十进制表示的数字具有无限二进制表示;最简单的示例之一是0.1

为了达到您想要的效果,您必须接受使用不同的类型作为取整函数的结果。如果您当前的需要是打印数字,则可以使用字符串和格式化函数:问题在于如何准确获得您期望的格式。否则,如果您需要存储此数字以执行精确的计算例如,如果你在做会计,你需要一个能够精确表示十进制数的库。在这种情况下,最常用的方法是使用比例表示法:一个整数代表数值和十进制数字的数量。将数值除以提升到比例的10得到原始数

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

“四舍五入(37.785,0.01)应该是37.79,而不是37.78。”

首先,没有共识认为37.79而不是37.78是“正确”的答案?平局打破者总是有点难。虽然平局时总是四舍五入是一种广泛使用的方法,但它肯定不是唯一的方法

其次,这不是一个平局的情况。IEEE二进制64浮点格式的数值是37.7849999997(大约)。除了人工输入37.785的值并将其转换为浮点表示之外,还有很多方法可以获得37.784999999997的值。在大多数情况下,
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;
}
=ROUND(37785/1000,2)
=ROUND(19810222/2^19+21474836/2^47,2)
#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;
}   
2.67899999 → 2.679
12.3499999 → 12.35
1.20000001 → 1.2
1.113   → 3 decimal digits
6.15634 → 5 decimal digits
1.113 + 6.15634 = 7.26934    → 5 decimal digits
1.113 * 6.15634 = 6.85200642 → 8 decimal digits
    // 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);
    }