【问题标题】:Truncating a double floating point at a certain number of digits在一定位数处截断双浮点
【发布时间】:2017-05-08 17:48:05
【问题描述】:

我编写了以下例程,它应该在第 n 位小数处截断 C++ 双精度。

double truncate(double number_val, int n)
{
    double factor = 1;
    double previous = std::trunc(number_val); // remove integer portion
    number_val -= previous;
    for (int i = 0; i < n; i++) {
        number_val *= 10;
        factor *= 10;
    }
    number_val = std::trunc(number_val);
    number_val /= factor;
    number_val += previous; // add back integer portion
    return number_val;
}

通常,这很好用……但我发现对于一些数字,尤其是那些似乎在 double 中没有精确表示的数字,存在问题。

例如,如果输入是 2.0029,并且我想在内部将其截断到第五位,则双精度似乎存储为介于 2.0028999999999999996 和 2.00289999999999999999 之间的某个值,并且在小数点后第五位截断它会得到 2.00289,就数字的存储方式而言,这可能是正确的,但对于最终用户来说,这看起来像是错误的答案。

如果我在小数点后四舍五入而不是截断,一切都会好起来的,当然,如果我给出一个双精度表示,其十进制表示的小数点后有 n 位以上,它也可以正常工作,但是怎么做我修改了这个截断例程,以便由于 double 类型及其十进制表示不精确导致的不准确不会影响最终用户看到的结果?

我想我可能需要某种舍入/截断混合来完成这项工作,但我不确定我会如何编写它。

编辑:感谢到目前为止的回复,但也许我应该澄清一下,这个值不一定会产生输出,但这个截断操作可以是许多不同的用户指定的浮点数操作链的一部分。在多个操作中累积在双精度内的误差是可以的,但是任何单个操作(例如截断或舍入)都不应产生与其实际理想值相差超过一半的 epsilon 的结果,其中 epsilon 是表示的最小量级通过当前指数的双精度。我目前正在尝试消化下面 iinspectable 提供的关于浮点运算的链接,看看它是否能帮助我弄清楚如何做到这一点。

编辑:嗯,链接给了我一个想法,这有点骇人听闻,但它应该可以工作,即在我开始用它做任何其他事情之前,在函数的顶部放置一个像 number_val += std::numeric_limits&lt;double&gt;::epsilon() 这样的行。不过,不知道有没有更好的方法。

编辑:我今天在公共汽车上时有一个想法,我还没有机会彻底测试,但它的工作原理是将原始数字四舍五入到 16 位有效十进制数字,然后将其截断:

double truncate(double number_val, int n)
{
    bool negative = false;
    if (number_val == 0) {
        return 0;
    } else if (number_val < 0) {
        number_val = -number_val;
        negative = true;
    } 
    int pre_digits = std::log10(number_val) + 1;
    if (pre_digits < 17) {
        int post_digits = 17 - pre_digits;
        double factor = std::pow(10, post_digits);
        number_val = std::round(number_val * factor) / factor;
        factor = std::pow(10, n);
        number_val = std::trunc(number_val * factor) / factor;
    } else {
        number_val = std::round(number_val);
    }
    if (negative) {
        number_val = -number_val;
    }
    return number_val;
}

由于双精度浮点数无论如何只能有大约 16 位精度,这可能适用于所有实际目的,但最多只有一位精度,否则双精度可能支持。

我想进一步指出,这个问题与上面建议的重复问题不同,因为 a) 这是使用 C++,而不是 Java...我没有 DecimalFormatter 便利类,并且 b) 我想要截断,而不是四舍五入,给定数字的数字(在 double 数据类型允许的精度范围内),以及 c) 正如我之前所说,这个函数的结果是 not 应该是一个可打印的字符串......它应该是这个函数的最终用户可能选择进一步操作的本机浮点数。由于双精度类型的不精度导致的多次操作累积错误是可以接受的,但任何单个操作都应该在双精度数据类型的精度限制范围内正确执行。

【问题讨论】:

  • What Every Computer Scientist Should Know About Floating-Point Arithmetic。有了这个,不要试图修改你的浮点值。如果您需要截断值,请在面向用户的界面中执行(例如,在格式化显示值或序列化为文本时)。
  • 您正在尝试的内容原则上是不可能的。请参阅here 了解原因以及迭代证明。
  • 我几乎可以肯定这是重复的。将浮点值截断到指定数量的 小数 位没有多大意义;例如1.23 不能用二进制浮点数精确表示。这种截断唯一有意义的情况是当您从 1.2345 这样的浮点值生成人类可读的字符串(如 "1.23")时。
  • 不可能。以你自己的例子为例,假设计算机看到输入2.0028999999999999996,它是2.0029 的不精确表示还是2.0028999999999999996 的精确表示,还是介于两者之间?计算机无法做到这一点。充其量您可以将浮点数截断为指定的 binary 数字。对于十进制数字,您不能这样做。
  • 除了 hack 之外,添加 epsilon() 对大于或等于 2.0 的值没有任何作用。您正在尝试解决无法解决的问题。如果您需要准确存储小数,则必须使用能够做到这一点的表示。 Binary-coded decimals 是您尝试解决的问题的一种常见解决方案。

标签: c++ rounding truncation


【解决方案1】:

好的,如果我理解正确,你有一个浮点数,你想将它截断为 n 位:

10.099999
   ^^      n = 2

becomes

10.09
   ^^

但是您的函数正在将数字截断为近似接近的值:

10.08999999
   ^^

然后显示为10.08

您如何保留truncate 公式,它会尽可能地截断,并使用std::setprecisionstd::fixed 将截断的值四舍五入到所需的小数位数? (假设您使用的是std::cout 输出?)

#include <iostream>
#include <iomanip>

using std::cout;
using std::setprecision;
using std::fixed;
using std::endl;

int main() {
  double foo = 10.08995; // let's imagine this is the output of `truncate`

  cout << foo << endl;                             // displays 10.0899
  cout << setprecision(2) << fixed << foo << endl; // rounds to 10.09
}

我已经为此在wandbox 上设置了一个演示。

【讨论】:

    【解决方案2】:

    我已经调查过了。这很难,因为浮点表示会导致您不准确,然后由于小数而导致进一步的不准确。 0.1 不能用二进制浮点数精确表示。但是,您可以使用带有 %g 参数的内置函数 sprintf,该参数应该为您准确舍入。

     char out[64];
     double x = 0.11111111;
     int n = 3;
     double xrounded;
     sprintf(out, "%.*g", n, x);
     xrounded = strtod(out, 0);
    

    【讨论】:

    • 是的,但这只会 round 数字,我想在其中获取数字,因为它会在双浮点的精度限制内被截断。例如,如果我有 2.088 并且我想截断为 5 位,我应该返回的数字仍然应该是 1.2088 的近似值,而不是 1.20879
    • @markt1964:你还没有理解,base-2 和 base-10 数字系统的数字很少,两个系统都可以准确表示。所以也许,最接近十进制 1.2088 的浮点数(存储为 base-2)可以表示 is 1.20879...
    • 所以 sprintf 会将数字转换为最接近的十进制表示,strtod 将从十进制转换为最接近的二进制。 (如果您需要额外的精度,您可以使用尾随 0.5 秒)
    • 我意识到小数分数通常不能以 2 为底的浮点数准确表示,但是当我取十进制数 2.0029 时,它甚至没有 5 bsae 10 位小数点后第一个数字地方,这表示为2.0028999999999999996,当我们将其截断到小数点后5位时,我们得到2.00289,它本身不能以2为底准确表示,存储为大约2.0028900000000000002,与2.0028999999999999996有很大不同。
    【解决方案3】:

    获取双精度字符串

    如果您只是想打印输出,那么使用 stringstream 非常简单直接:

    #include <cmath>
    #include <iostream>
    #include <iomanip>
    #include <limits>
    #include <sstream>
    
    using namespace std;
    
    string truncateAsString(double n, int precision) {
        stringstream ss;
        double remainder = static_cast<double>((int)floor((n - floor(n)) * precision) % precision);
        ss << setprecision(numeric_limits<double> ::max_digits10 + __builtin_ctz(precision))<< floor(n);
        if (remainder)
            ss << "." << remainder;
        cout << ss.str() << endl;
        return ss.str();
    }
    
    int main(void) {
        double a = 9636346.59235;
        int precision = 1000; // as many digits as you add zeroes. 3 zeroes means precision of 3.
        string s = truncateAsString(a, precision);
        return 0;
    }
    

    得到精确值的除法浮点数

    也许你正在为你的浮点寻找真正的价值,你可以使用boost multiprecision library

    Boost.Multiprecision 库可用于要求精度超过标准内置类型(例如 float、double 和 long double)的计算。对于扩展精度计算,Boost.Multiprecision 提供了一个名为 cpp_dec_float 的模板数据类型。精度的小数位数在编译时通过模板参数固定。

    演示

    #include <boost/math/constants/constants.hpp>
    #include <boost/multiprecision/cpp_dec_float.hpp>
    #include <iostream>
    #include <limits>
    #include <cmath>
    #include <iomanip>
    
    using boost::multiprecision::cpp_dec_float_50;
    
    cpp_dec_float_50 truncate(cpp_dec_float_50 n, int precision) {
        cpp_dec_float_50 remainder = static_cast<cpp_dec_float_50>((int)floor((n - floor(n)) * precision) % precision) / static_cast<cpp_dec_float_50>(precision);
        return floor(n) + remainder;
    }
    
    int main(void) {
        int precision = 100000; // as many digits as you add zeroes. 5 zeroes means precision of 5.
        cpp_dec_float_50 n = 9636346.59235789;
        n = truncate(n, precision); // first part is remainder, floor(n) is int value truncated.
        cout << setprecision(numeric_limits<cpp_dec_float_50> ::max_digits10 + __builtin_ctz(precision)) << n << endl; // __builtin_ctz(precision) will equal the number of trailing 0, exactly the precision we need!
        return 0;
    }
    

    输出:

    9636346.59235
    

    注意:需要sudo apt-get install libboost-all-dev

    【讨论】:

      猜你喜欢
      • 2013-12-31
      • 2012-05-07
      • 1970-01-01
      • 2011-04-29
      • 2011-06-07
      • 1970-01-01
      • 2017-07-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多