将浮点数截断为前导N个十进制数

时间:2016-11-15 20:20:14

标签: c++ c performance floating-point ieee-754

哪种方法可以获得浮点数的最左边非零数字 n (数字> = 0.0)。

例如,

如果n = 1:

  • 0.014568 - > 0.01
  • 0.246456 - > 0.2

如果n = 2:

  • 0.014568 - > 0.014
  • 0.246456 - > 0.24

在@schil227评论之后: 目前我正在进行乘法和除法(10),以便在十进制数字段中包含 n 数字。

2 个答案:

答案 0 :(得分:4)

代码可以使用sprintf(buf, "%e",...)来完成大部分繁重工作。

其他直接代码可能会失败,有很多极端情况,sprintf()可能至少是良好的可靠参考解决方案。

此代码打印doubleDBL_DECIMAL_DIG个位置,以确保没有数字的舍入会产生影响。 然后它会根据n将各种数字归零。

请参阅@Mark Dickinson comment了解使用比DBL_DECIMAL_DIG更大的值的原因。也许是DBL_DECIMAL_DIG*2的顺序。如上所述,有许多极端情况。

#include <float.h>
#include <math.h>
#include <stdio.h>

double foo(double x, int n) {
  if (!isfinite(x)) {
    return x;
  }
  printf("%g\n", x);
  char buf[DBL_DECIMAL_DIG + 11];
  sprintf(buf, "%+.*e", DBL_DECIMAL_DIG, x);
  //puts(buf);
  assert(n >= 1 && n <= DBL_DECIMAL_DIG + 1);
  memset(buf + 2 + n, '0', DBL_DECIMAL_DIG - n + 1);
  //puts(buf);
  char *endptr;
  x = strtod(buf, &endptr);
  printf("%g\n", x);
  return x;
}

int main() {
 foo(0.014568, 1);
 foo(0.246456, 1);
 foo(0.014568, 2);
 foo(0.246456, 2);
 return 0;
}

输出

0.014568
0.01
0.246456
0.2
0.014568
0.014
0.246456
0.24

这个答案假设OP不想要一个舍入的答案。回复:0.246456 -> 0.24

答案 1 :(得分:2)

如果你想把结果作为一个字符串,你应该打印到一个具有额外精度的字符串,然后自己切断它。 (有关IEEE 64位double需要多少额外精度的详细信息,请参阅@ chux的答案,以避免从9s字符串向上舍入,因为您需要截断但是所有常用的字符串函数四舍五入到最近。)

如果你想要double结果,那么你确定你真的想要这个吗?在计算过程中早期舍入/截断通常只会恶化最终结果的准确性。当然,在floor / ceil,trunc和nearbyint的实际算法中有用,这只是trunc的缩放版本。

如果你只想要一个double,你可以获得相当不错的结果而不需要一个字符串。 使用ndigitsfloor(log10(fabs(x)))计算比例因子,然后将缩放值截断为整数,然后缩小

经过测试和工作(有和没有-ffast-math)。请参阅Godbolt compiler explorer上的asm。这可能会合理有效地运行,尤其是使用-ffast-math -msse4.1时(因此floor和trunc可以内联到roundsd)。

如果您关心速度,请考虑将pow()替换为利用指数为小整数的事实。在这种情况下,我不确定库pow()实现的速度有多快。 GNU C __builtin_powi(x, n) trades accuracy for speed, for integer exponents, doing a multiplication tree, which is less accurate than what pow() does

#include <float.h>
#include <math.h>
#include <stdio.h>

double truncate_n_digits(double x, int digits)
{
    if (x==0 || !isfinite(x))
        return x;   // good idea stolen from Chux's answer :)

    double l10 = log10(fabs(x));
    double scale = pow(10.,  floor(l10) + (1 - digits));  // floor rounds towards -Inf
    double scaled = x / scale;
    double scaletrunc = trunc(scaled);  // trunc rounds towards zero
    double truncated = scaletrunc * scale;

#if 1    // debugging code
    printf("%2d %24.14g =>\t%24.14g\t scale=%g, scaled=%.30g\n", digits, x, truncated, scale, scaled);
    // print with more accuracy to reveal the real behaviour
    printf("   %24.20g =>\t%24.20g\n", x, truncated);
#endif

    return truncated;
}

测试用例:

int main() {
 truncate_n_digits(0.014568, 1);
 truncate_n_digits(0.246456, 1);
 truncate_n_digits(0.014568, 2);
 truncate_n_digits(-0.246456, 2);
 truncate_n_digits(1234567, 2);
 truncate_n_digits(99999999999, 6);
 truncate_n_digits(-99999999999, 6);
 truncate_n_digits(99999, 10);
 truncate_n_digits(-0.0000000001234567, 3);
 truncate_n_digits(1000, 6);
 truncate_n_digits(0.001, 6);
 truncate_n_digits(1e-312, 2);  // denormal, and not exactly representable: 9.999...e-313
 truncate_n_digits(nextafter(1e-312, INFINITY), 2);  // denormal, just above 1.00000e-312
 return 0;
}

每个结果显示两次:首先只有%.14g所以舍入给出了我们想要的字符串,然后再用%.20g来显示足够的位置来揭示浮点数学的真实性。大多数数字都不是完全可表示的,因此即使完美舍入也无法返回double 表示截断的十进制字符串。 (最大约为尾数大小的整数是完全可表示的,分母的幂是2的幂。)

 1                 0.014568 =>                      0.01         scale=0.01, scaled=1.45679999999999987281285029894
    0.014567999999999999353 =>   0.010000000000000000208
 1                 0.246456 =>                       0.2         scale=0.1, scaled=2.46456000000000008398615136684
      0.2464560000000000084 =>     0.2000000000000000111
 2                 0.014568 =>                     0.014         scale=0.001, scaled=14.5679999999999996163069226895
    0.014567999999999999353 =>   0.014000000000000000291
 2                -0.246456 =>                     -0.24         scale=0.01, scaled=-24.6456000000000017280399333686
     -0.2464560000000000084 =>   -0.23999999999999999112
 3               1234.56789 =>                      1230         scale=10, scaled=123.456789000000000555701262783
       1234.567890000000034 =>                      1230
 6               1234.56789 =>                   1234.56         scale=0.01, scaled=123456.789000000004307366907597
       1234.567890000000034 =>     1234.5599999999999454
 6              99999999999 =>               99999900000         scale=100000, scaled=999999.999990000040270388126373
                99999999999 =>               99999900000
 6             -99999999999 =>              -99999900000         scale=100000, scaled=-999999.999990000040270388126373
               -99999999999 =>              -99999900000
10                    99999 =>                     99999         scale=1e-05, scaled=9999900000
                      99999 =>     99999.000000000014552
 3            -1.234567e-10 =>                 -1.23e-10         scale=1e-12, scaled=-123.456699999999983674570103176
   -1.234566999999999879e-10 => -1.2299999999999998884e-10
 6                     1000 =>                      1000         scale=0.01, scaled=100000
                       1000 =>                      1000
 6                    0.001 =>                     0.001         scale=1e-08, scaled=100000
   0.0010000000000000000208 =>  0.0010000000000000000208
 2     9.9999999999847e-313 =>      9.9999999996388e-313         scale=1e-314, scaled=100.000000003458453079474566039
   9.9999999999846534143e-313 =>        9.9999999996388074622e-313
 2     1.0000000000034e-312 =>      9.0000000001196e-313         scale=1e-313, scaled=9.9999999999011865980946822674
   1.0000000000034059979e-312 =>        9.0000000001195857973e-31

由于您想要的结果通常不能完全表示(并且由于其他舍入错误),结果double有时会低于您想要的结果,因此以完全精度打印它可能会给出1.19999999而不是1.20000011。您可能希望使用nextafter(result, copysign(INFINITY, original))来获得比您想要的结果更高的结果。

当然,在某些情况下,这可能会让事情变得更糟。但是,由于我们将其截断为零,因此通常我们得到的结果恰好低于(大小)无法代表的确切值。