在Perl中如何进行十进制运算?

9

我正在开发一个用Perl编写的会计脚本,想知道在进行小数算术运算时应该采取“正确”的方式。例如,我想确保这样的比较能够正确地工作:

"0.1" + "0.1" + "0.1" == "0.3"
"258.9" * "2000" == "517800"
...

在Python中,我会使用Decimal类型来处理值,但是在Perl中应该怎么做呢?


3
Math::BigRat和Math::Decimal可以解决这个问题。 - ikegami
1
从您的小数和整数中删除引号。 - Missaka Wijekoon
5
@Missaka Wijekoon,这些引用语并不会造成伤害。你没有理解重点。请尝试运行perl -E'say 0.1 + 0.1 + 0.1 == 0.3 ? "equal" : "not equal"' - ikegami
5
另一种非常古老的策略是对最小单位进行整数运算。 相应地,使用10美分取代0.1美元。 - tjd
4
好的。我们都使用字符串输入。但你仍然应该知道哪些项目是“货币”,并将其转换为最小的适当单位。对于美元货币,使用“美分”,对于美国股票交易,使用“八分之一美元”等。在处理“汇率”和“百分比”时,你应该非常清楚你的“舍入”算法,不要只是“抱有最好的希望”。 - tjd
显示剩余6条评论
3个回答

8

注意:现有的Math::Currency已经失效。

使用Math::BigFloat将数字表示为任意精度对象。

use Math::BigFloat;

print Math::BigFloat->new(0.1) +
      Math::BigFloat->new(0.1) +
      Math::BigFloat->new(0.1) == Math::BigFloat->new(0.3);

您可以使用bignum自动完成此操作...
use bignum;

print 0.1 + 0.1 + 0.1 == 0.3;

但是!这种魔法只适用于数字。如果你试图把字符串加在一起,它不会起作用,魔法来得太晚了。你必须明确地强制它们成为数字。要将字符串转换为数字,可以像这样将0添加到字符串中:$a += 0。或者你可以通过以0 +开头来强制执行一个大数方程式,它会沿着行级联下去。

use bignum;

$a = "0.1";
$b = "0.1";
$c = "0.1";
$d = "0.3";

# False
print $a + $b + $c == $d;

# True
print 0 + $a + $b + $c == $d;

需要注意两点。

首先,这一切都会带来巨大的性能成本。不仅是进行任意精度数学计算,而且还包括所有方法和重载魔法。请进行基准测试以确定是否可以接受。幸运的是,bignum 仅升级其范围内的数字,而不是整个程序。在 bignum 的范围之外使用这些数字也是安全的,对它们进行的任何数学计算也将被升级。

其次,Decimal 将保留有效数字。Math::BigFloat 不会。


Math::BigFloat 对我来说似乎可以工作,尽管它仍然是浮点算术。我很惊讶没有核心模块来执行十进制算术。 - Eugene Yarmash
2
@engene y,Math::BigRat可以进行无损算术运算,并且自5.8.0版本以来一直存在于核心中。 - ikegami
1
@eugeney 它在底层使用整数完成。0.01 变成了带有指数 21。每个任意精度库都必须有其精度的限制,否则您很容易耗尽内存。例如,1/3。Python 的 Decimal 限制为 30 位。Math::BigFloat 可以达到 40 位。Math::BigFloat 讨论了这个问题。如果您需要更高的精度,请使用 Math::BigRat。它将分数存储为分数。use bigrat; print 1/3 将输出 1/3 - Schwern

0
这是如何使用Math::Decimal进行除法的示例:
use 5.024003; # use `say'
use Math::Decimal qw( 
                     dec_mul_pow10 
                     dec_neg 
                     dec_rndiv 
                    ); 
my $num_1 = '3'; 
my $num_2 = '7'; 
my $precision = '3'; 

# You want to get three digits after the decimal, 
# so multiply dividend by 1000, divide to get an 
# integer quotient, and then divide that quotient 
# by 1000. 
my $dividend_up_some = dec_mul_pow10( $num_1, $precision ); 
# Rounding by `NEAR_EVN' is "bankers' rounding." 
my $quotient_up_some = 
          dec_rndiv( 'NEAR_EVN', $dividend_up_some, 
                     $num_2 ); 
# Move it back down to get your desired precision after 
# the decimal. 
my $quotient = 
      dec_mul_pow10( $quotient_up_some, 
                     dec_neg( $precision ) ); 
say "$num_1 / $num_2 = $quotient";

运行这个程序,以下是输出结果:
3 / 7 = 0.429

将$precision更改为'10',这是输出结果:

3 / 7 = 0.4285714286

-4
我知道的最好方法是使用绝对差小于公差进行测试。例如: perl -e '$x = 0.1 + 0.1 + 0.1; $y = 0.3; $q = abs($x - $y) < 0.0001 ? "相等" : "不相等"; print $q . "\n";'

4
在会计场景中使用浮点数是一个非常糟糕的想法,其中最主要的问题之一是这些舍入误差的累积。 - tjd
你可以从POSIX.pm获取实际的epsilon。use POSIX qw(DBL_EPSILON); - Schwern

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接