【问题标题】:Perl rounding error againPerl再次舍入错误
【发布时间】:2014-08-21 00:11:11
【问题描述】:

显示问题的示例:

  • 编号为 105;
  • 除以 1000(结果 0.105)
  • 小数点后两位应该是:0.11

现在,几个脚本 - 基于对另一个问题的回答:

这主要是推荐和大多数赞成的解决方案是使用printf

use 5.014;
use warnings;
my $i = 105;
printf "%.2f\n", $i/1000;   #prints 0.10

但打印出错误的结果。在评论中 https://stackoverflow.com/a/1838885@Sinan Unur 说(6 次点赞评论):

也可以将 sprintf("%.3f", $value) 用于数学目的。

但是,它“有时”不起作用......就像上面一样。

另一个推荐的解决方案Math::BigFloat

use 5.014;
use warnings;
use Math::BigFloat;
my $i = 105;

Math::BigFloat->precision(-2);
my $r = Math::BigFloat->new($i/1000);

say "$r";   #0.10 ;(

结果也是错误的。另一位推荐了bignum

use 5.014;
use warnings;
use bignum ( p => -2 );
my $i = 105;
my $r = $i/1000;
say "$r";   #0.10 ;(

又错了。 ;(

现在开始工作了:

use 5.014;
use warnings;
use Math::Round;
my $i = 105;
say nearest(0.01, $i/1000); #GREAT prints 0.11 :)

结果很好0.11,不过在这里评论https://stackoverflow.com/a/571740 抱怨它。

最后是“我自己”的另一个推荐功能:

use 5.014;
use warnings;
my $i = 105;
my $f = $i/1000;

say myround($f,2);  # 0.11

sub myround {
    my($float, $prec) = @_;
    my $f = $float * (10**$prec);
    my $r = int($f + $f/abs($f*2));
    return $r/(10**$prec);
}

也打印了0.11,但无法证明它的正确性。

我读到的参考资料:

我明白这是所有语言的共同问题,但毕竟请 以上阅读 - 我仍然有这个问题:

perl 中将浮点数舍入为 N 的 防错 方法是什么 小数位 - 以数学上正确的方式,例如什么将四舍五入结果 像 105/1000 一样正确到 N 位小数,没有“惊喜”...

【问题讨论】:

  • 就我个人而言,我总是依赖像 Pg/Oracle 这样的数据库来进行这种类型的计算。
  • 我应该指出,在您的 Math::Bigfloat 解决方案中,您将一个公共浮点数传递给了 new(),这意味着解决您的问题为时已晚。您可能应该从(除法前)整数中制作一个 Bigfloat,以便让 Bigfloat 的除法方法为您提供可以使用的东西。
  • +1 表示 tjd 的评论。我会认为(很高兴被证明是错误的)如果您正确使用 Math::Bigfloat,它将是防错的
  • 除了存储可以用 n/2^m 表示的数字,或者对那些内部状态始终为可以用 n/2^m 表示,以及结果。否则,错误是存储格式固有的。

标签: perl


【解决方案1】:

当数字恰好是 0.105 时,您会期待特定的行为,但浮点错误意味着您不能期望数字与您认为的完全一样。

105/1000 是二进制的周期数,就像 1/3 是十进制的周期一样。

105/1000
       ____________________
= 0.00011010111000010100011 (bin)

~ 0.00011010111000010100011110101110000101000111101011100001 (bin)

= 0.10499999999999999611421941381195210851728916168212890625

0.1049999... 小于 0.105,因此四舍五入为 0.10。

但即使你有 0.105,它仍然会四舍五入到 0.10,因为 sprintf 会舍入一半到偶数。更好的测试是 155/1000

155/1000
       ____________________
= 0.00100111101011100001010 (bin)

~ 0.0010011110101110000101000111101011100001010001111010111 (bin)

= 0.1549999999999999988897769753748434595763683319091796875

0.155 应该舍入到 0.16,但由于浮点错误,它会舍入到 0.15。

$ perl -E'$_ = 155; say sprintf("%.2f", $_/1000);'
0.15

$ perl -E'$_ = 155; say sprintf("%.0f", $_/10)/100;'
0.16

第二个有效,因为 5/10 不是周期性的,这就是解决方案。正如 Sinan Unur 所说,您可以使用sprintf 纠正错误。但是如果你不想丢失你的工作,你必须四舍五入到一个整数。

$ perl -E'
   $_ = 155/1000;

   $_ *= 1000;                # Move decimal point past significant.
   $_ = sprintf("%.0f", $_);  # Fix floating-point error.
   $_ /= 10;                  # 5/10 is not periodic
   $_ = sprintf("%.0f", $_);  # Do our rounding.
   $_ /= 100;                 # Restore decimal point.

   say;
'
0.16

这将修复舍入错误,允许 sprintf 正确舍入一半到偶数。

0.105  =>  0.10
0.115  =>  0.12
0.125  =>  0.12
0.135  =>  0.14
0.145  =>  0.14
0.155  =>  0.16
0.165  =>  0.16

如果您想改为四舍五入,则需要使用 sprintf 以外的其他值来进行最终四舍五入。或者您可以在除以 10 之前添加s/5\z/6/;


但这很复杂。

答案的第一句话是关键。当数字恰好是 0.105 时,您会期待特定的行为,但浮点错误意味着您不能期望数字与您认为的完全相同。解决方案是引入容差。这就是使用 sprintf 进行舍入的作用,但它是一个生硬的工具。

use strict;
use warnings;
use feature qw( say );

use POSIX qw( ceil floor );

sub round_half_up {
   my ($num, $places, $tol) = @_;

   my $mul = 1; $mul *= 10 for 1..$places;
   my $sign = $num >= 0 ? +1 : -1;

   my $scaled = $num * $sign * $mul;
   my $frac = $scaled - int($scaled);

   if ($sign >= 0) {
      if ($frac < 0.5-$tol) {
         return floor($scaled) / $mul;
      } else {
         return ceil($scaled) / $mul;
      }
   } else {
      if ($frac < 0.5+$tol) {
         return -floor($scaled) / $mul;
      } else {
         return -ceil($scaled) / $mul;
      }
   }
}

say sprintf '%5.2f', round_half_up( 0.10510000, 2, 0.00001);  #  0.11
say sprintf '%5.2f', round_half_up( 0.10500001, 2, 0.00001);  #  0.11  Within tol
say sprintf '%5.2f', round_half_up( 0.10500000, 2, 0.00001);  #  0.11  Within tol
say sprintf '%5.2f', round_half_up( 0.10499999, 2, 0.00001);  #  0.11  Within tol
say sprintf '%5.2f', round_half_up( 0.10410000, 2, 0.00001);  #  0.10
say sprintf '%5.2f', round_half_up(-0.10410000, 2, 0.00001);  # -0.10
say sprintf '%5.2f', round_half_up(-0.10499999, 2, 0.00001);  # -0.10  Within tol
say sprintf '%5.2f', round_half_up(-0.10500000, 2, 0.00001);  # -0.10  Within tol
say sprintf '%5.2f', round_half_up(-0.10500001, 2, 0.00001);  # -0.10  Within tol
say sprintf '%5.2f', round_half_up(-0.10510000, 2, 0.00001);  # -0.11

可能有一些现有的解决方案可以按照相同的方式工作。

【讨论】:

  • 哈! ;) 非常详细的答案。谢谢你。所以,在结果中:a.)不能依赖printf/sprintf,因为它在小数点后没有做正确的四舍五入b.) 每次都应该使用上述round_half_up 函数来获得正确的结果。
  • @kobame 我认为您的 a. 不正确。 printf/sprintf 会用你给它的东西做正确的事。不了解二进制浮点数的限制可能会导致程序员高估他们提供给printf/sprintf 的内容的准确性
  • @tjd, "printf/sprintf 会用你的付出做正确的事。", 你在说什么?他特别要求替换printf "%.2f\n", $i/1000,因为它不能满足他的要求。
  • @ikegami,我想我支持你的观点,即printf 在交给0.10499999999999999611421941381195210851728916168212890625 时会按预期运行。我错了吗?
  • @tjd 抱歉,printf "%.2f", 155/1000 的普通人(或称他们为普通人/普通人/愚蠢的人)真的想要获得0.16 而不是0.15。对不起。请理解并接受这样一个事实,不是每个在 perl 中编写一些简单脚本的人都是计算机专家,他们详细了解浮点数是如何存储的。 Ikegami 的回答很好,因为解释了很多事情,但这并不能改变事实 = 不能依赖 printf (因为有时会给出错误的结果)。我的问题上的链接有一个很好的比较,这不仅仅是 perl 的问题。 ;)
【解决方案2】:

在旧的整数数学编程时代,我们习惯假装使用小数位:

N = 345
DISPLAY N        # Displays 345
DISPLAY (1.2) N  # Displays 3.45

在尝试正确计算销售税时,我们学到了一个有价值的技巧:

my $amount = 1.344;
my $amount_rounded = sprintf "%.2f", $amount + .005;
my $amount2 = 1.345;
my $amount_rounded2 = sprintf "%.2f", $amount2 + .005;
say "$amount_rounted   $amount_rounded2";  # prints 1.34 and 1.35

通过添加 1/2 的精度,我可以正确显示舍入。当数字为 1.344 时,加上 0.005 即为 1.349,去掉最后一位数字显示倾角 1.344。当我对 1.345 做同样的事情时,添加 .005 使其成为 1.350,并删除最后一个数字将其显示为 1.35。

您可以使用将返回 四舍五入 金额的子例程来执行此操作。


有趣...

关于这个主题有一个PerlFAQ。它建议简单地使用printf 来获得正确的结果:

use strict;
use warnings;
use feature qw(say);

my $number = .105;
say "$number";
printf "%.2f\n", $number;   # Prints .10 which is incorrect
printf "%.2f\n", 3.1459;    # Prins 3.15 which is correct

对于 Pi,这有效,但不适用于 .105。然而:

use strict;
use warnings;
use feature qw(say);

my $number = .1051;
say "$number";
printf "%.2f\n", $number;   # Prints .11 which is correct
printf "%.2f\n", 3.1459;    # Prints 3.15 which is correct

这看起来像是 Perl 在内部存储 .105 的方式的问题。可能像.10499999999 这样的东西会正确向下舍入。我还注意到 Perl 警告我不要使用 roundrounding 作为未来可能的保留字。

【讨论】:

  • 常见问题解答说您可以使用sprintf 来修复错误,但 OP 使用sprintf 进行舍入而不首先修复错误。
  • 我在常见问题解答中没有看到任何关于必须修复错误的内容。常见问题解答说使用printfsprintf 进行舍入。 Perl 将数字显示为.105,但随后sprintf 不会舍入显示的内容,而是.104999...
  • (那不是 pi. 3.14159...)
  • 我只是相信你的话。请允许我改述一下:您说常见问题解答说您可以使用 sprintf 修复错误,但 OP 使用 sprintf 进行舍入而不首先修复错误。
  • FAQ 中确切地说:要舍入到特定位数,sprintf() 或 printf() 通常是最简单的方法。 。因为(恕我直言)所有用户都希望获得 正确 结果,所以这是常见问题解答中的一个坏建议。唯一的解决方案是使用自己的舍入函数,正如常见问题解答后面所述:在这些情况下,不信任 Perl 使用的任何舍入系统可能是值得的,而是自己实现所需的舍入函数。 所以,(不幸的是)sprintf 的建议很糟糕(恕我直言)。
【解决方案3】:

您的自定义函数大部分应该按预期工作。以下是它的工作原理以及如何验证它的正确性:

sub myround {
    my($float, $prec) = @_;

    # Prevent div-by-zero later on
    if ($float == 0) { return 0; }

    # Moves the decimal $prec places to the right
    # Example: $float = 1.234, $prec = 2
    #   $f = $float * 10^2;
    #   $f = $float * 100;
    #   $f = 123.4;
    my $f = $float * (10**$prec);

    # Round 0.5 away from zero using $f/abs($f*2)
    #   if $f is positive, "$f/abs($f*2)" becomes  0.5
    #   if $f is negative, "$f/abs($f*2)" becomes -0.5
    #   if $f is zero, we have a problem (hence the earlier if statement)
    # In our example:
    #   $f = 123.4 + (123.4 / (123.4 * 2));
    #   $f = 123.4 + (0.5);
    #   $f = 123.9;
    # Then we truncate to integer:
    #   $r = int(123.9);
    #   $f = 123;
    my $r = int($f + $f/abs($f*2));

    # Lastly, we shift the deciaml back to where it should be:
    #   $r / 10^2
    #   $r / 100
    #   123 / 100
    #   return 1.23;
    return $r/(10**$prec);
}

但是,下面的$float = 0会报错,所以开头多了一个if语句。

上述函数的好处是可以四舍五入到位小数,允许你四舍五入到小数点的左边。例如,myround(123, -2) 将给出100

【讨论】:

    【解决方案4】:

    我会将use bignum 添加到您的原始代码示例中。

    use 5.014;
    use warnings;
    use bignum;
    
    my $i = 105;                # Parsed as Math::BigInt
    my $r = $i / 1000;          # Overloaded division produces Math::BigFloat
    say $r->ffround(-2, +inf);  # Avoid using printf and the resulting downgrade to common float.
    

    这解决了您在 use Math::BigFloat 示例中犯的错误,方法是立即将您的数字解析为对象,而不是等待您将舍入错误的结果传递给 Math::BigFloat-&gt;new

    【讨论】:

    • 我可能错过了你的回答,因为这个脚本也会为我打印0.10,而不是正确的0.11
    • @kobame,你是对的。我犯了允许 printf 将对象降级为普通浮点数的错误。固定。
    【解决方案5】:

    根据我的经验,Perl printf/sprintf 使用了错误的算法。我至少考虑了以下简单的例子得出了这个结论:

    # The same floating part for both numbers (*.8830 or *.8829) is expected in the rounded value, but it is different for some reason:
    printf("%.4f\n", "7.88295"); # gives 7.8830
    printf("%.4f\n", "8.88295"); # gives 8.8829
    

    整数部分在这个例子中应该没有任何影响,但它有。 我用 Perl 5.8.8 得到了这个结果。

    【讨论】:

      猜你喜欢
      • 2012-12-21
      • 1970-01-01
      • 1970-01-01
      • 2010-10-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多