如何修复此Perl代码以便1.1 + 2.2 == 3.3?

时间:2010-10-12 15:29:22

标签: perl floating-point decimal

如何修复此代码以便1.1 + 2.2 == 3.3?这里发生了什么导致这种行为?我模糊地熟悉舍入问题和浮点数学,但我认为这仅适用于除法和乘法,并且在输出中可见。

[me@unixbox1:~/perltests]> cat testmathsimple.pl 
#!/usr/bin/perl

use strict;
use warnings;

check_math(1, 2, 3);
check_math(1.1, 2.2, 3.3);

sub check_math {
        my $one = shift;
        my $two = shift;
        my $three = shift;

        if ($one + $two == $three) {
                print "$one + $two == $three\n";
        } else {
                print "$one + $two != $three\n";
        }
}

[me@unixbox1:~/perltests]> perl testmathsimple.pl 
1 + 2 == 3
1.1 + 2.2 != 3.3

编辑:

到目前为止,大多数答案都是“它是一个浮点问题,呃”,并为它提供了解决方法。我已经怀疑是问题所在。我该如何演示?如何让Perl输出长形式的变量?将$ 1 + $ 2计算存储在临时变量中并打印它并不能证明问题。

编辑:

使用aschepler演示的sprintf技术,我现在能够“看到”问题。此外,根据mscha和rafl的建议,使用bignum可以解决比较不相等的问题。但是,sprintf输出仍然表示数字不正确。这对此解决方案留下了一点疑问。

bignum是解决这个问题的好方法吗?是否有任何可能的副作用,我们应该在将其整合到更大的现有程序时注意这些副作用?

8 个答案:

答案 0 :(得分:17)

What Every Computer Scientist Should Know About Floating-Point Arithmetic

这些都不是Perl特有的:有无数个实数,显然,所有这些都不能用有限数量的位来表示。

使用的具体“解决方案”取决于您的具体问题。你想跟踪货币金额吗?如果是这样,请使用bignum提供的任意精度数字(使用更多内存和更多CPU,获得更准确的结果)。你在做数值分析吗?然后,决定要使用的精度,并使用sprintf(如下所示)和eq进行比较。

您可以随时使用:

use strict; use warnings;

check_summation(1, $_) for [1, 2, 3], [1.1, 2.2, 3.3];

sub check_summation {
    my $precision = shift;
    my ($x, $y, $expected) = @{ $_[0] };
    my $result = $x + $y;

    for my $n ( $x, $y, $expected, $result) {
        $n = sprintf('%.*f', $precision, $n);
    }

    if ( $expected eq $result ) {
        printf "%s + %s = %s\n", $x, $y, $expected;
    }
    else {
        printf "%s + %s != %s\n", $x, $y, $expected;
    }
    return;
}

输出:

1.0 + 2.0 = 3.0
1.1 + 2.2 = 3.3

答案 1 :(得分:6)

"What Every Computer Scientist Should Know About Floating-Point Arithmetic"

基本上,Perl正在处理浮点数,而你可能期望它使用定点。处理这种情况的最简单方法是修改代码,以便在任何地方使用整数,除非在最终的显示例程中。例如,如果您正在处理美元货币,请将所有美元金额存储在便士中。 123美元和45美分成为“12345”。这样,在加法和减法运算期间就没有浮点模糊度。

如果这不是一个选项,请考虑Matt Kane's comment。找到一个好的epsilon值,并在需要比较值时使用它。

我冒昧地猜测大多数任务并不真正需要浮点数,但我强烈建议您仔细考虑它是否适合您的任务。< / p>

答案 2 :(得分:5)

修复浮点的快捷方法是使用bignum。只需添加一行

即可
use bignum;

到你的脚本的顶部。 显然,存在性能影响,因此这可能不适合您。

更加本地化的解决方案是明确使用Math::BigFloat,您需要更高的准确性。

答案 3 :(得分:4)

来自The Floating-Point Guide

  

为什么我的数字,如0.1 + 0.2加起来不是很好的一轮0.3,而且   相反,我得到一个奇怪的结果,如   0.30000000000000004吗

     

因为在内部,计算机使用a   格式(二进制浮点)   无法准确表示一个数字   像0.1,0.2或0.3一样。

     

编译代码时   解释,你的“0.1”已经存在   四舍五入到最接近的数字   格式,这导致一个小的   甚至在之前的舍入错误   计算发生了。

     

我该怎么做才能避免这个问题?

     

这取决于什么样的   你正在做的计算。

     
      
  • 如果你真的需要你的结果准确加起来,特别是当你   与钱合作:使用特殊的decimal datatype
  •   
  • 如果你只是不想看到所有这些额外的小数位:简单地说   将结果格式化为固定的   小数位数   显示它。
  •   
  • 如果您没有可用的十进制数据类型,则可以使用另一种方法   用整数,例如做钱   完全以美分计算。但   这是更多的工作,并有一些   缺点。
  •   

Youz还可以使用"fuzzy compare"来确定两个数字是否足够接近,假设它们使用精确数学是相同的。

答案 4 :(得分:4)

要查看浮点标量的精确值,请为sprintf提供一个很大的精度:

print sprintf("%.60f", 1.1), $/;
print sprintf("%.60f", 2.2), $/;
print sprintf("%.60f", 3.3), $/;

我明白了:

1.100000000000000088817841970012523233890533447265625000000000
2.200000000000000177635683940025046467781066894531250000000000
3.299999999999999822364316059974953532218933105468750000000000

不幸的是,C99的%转换似乎不起作用。 perlvar提到了一个过时的变量$#,它改变了打印数字的默认格式,但如果我给它一个%f,它就会中断,而%g拒绝打印“非重要”数字。

答案 5 :(得分:2)

abs($three - ($one + $two)) < $some_Very_small_number

答案 6 :(得分:1)

使用sprintf将变量转换为格式化字符串,然后比较生成的字符串。

# equal( $x, $y, $d );
# compare the equality of $x and $y with precision of $d digits below the decimal point.
sub equal {
    my ($x, $y, $d) = @_;
    return sprintf("%.${d}g", $x) eq sprintf("%.${d}g", $y);   
}

出现这种问题是因为你的分数没有完美的定点表示(0.1,0.2等)。因此,值1.12.2实际上分别存储为1.10000000000000...12.2000000....1(我不确定它是稍微大一点还是略小一些。在我的示例中我认为它们变得略大一些)。当您将它们一起添加时,它会变为3.300000000...3,大于3.3,并转换为3.300000...1

答案 7 :(得分:1)

Number::Fraction允许你使用有理数(分数)而不是小数,类似这样(':constants'被导入以自动将字符串像'11 / 10'转换为Number :: Fraction对象):< / p>

use strict;
use warnings;
use Number::Fraction ':constants';

check_math(1, 2, 3);
check_math('11/10', '22/10', '33/10');

sub check_math {
        my $one = shift;
        my $two = shift;
        my $three = shift;

        if ($one + $two == $three) {
                print "$one + $two == $three\n";
        } else {
                print "$one + $two != $three\n";
        }
}

打印:

1 + 2 == 3
11/10 + 11/5 == 33/10