Subject: | as_int() method can return incorrect value |
Since as_int() method uses multiplication to express the integer
part of the number, the CPU must translate this to a natural floating
point number and perform the operation. Unfortunately, this
introduces floating point approximation errors, leading to as_int
amounts that report *incorrectly* compared to their actual value as
stored in Math::Currency class.
I have attached a script that shows such error. This script reported
numerous as_int() floating point errors on 64-bit Perl on Solaris 9. (My
Perl configuration information is attached to this report).
I found that in order to solve this, I ended up having to greatly
increase the floating point accuracy of the mathematical result
depending on the current value of the the number in the class, and then
shift off the approximation errors. For example, as a functional patch
for USD currency (2-decimal accuracy), I modified my local
Math::Currency class to perform an inverse logarithmetic adjustment
(dependent on the size of the internal integer number) as follows:
sub as_int { #with extra step to force 1 extra (internal) level of
#fractional precision
my $self = shift;
my $prec_adj = int(7 + (4 / (log($MBI->_len( $self->{_m} )) || 1)));
$prec_adj = $prec_adj > 10 ? 10 : $prec_adj < 2 ? 2 : $prec_adj;
return "0" unless $MBI->_len( $self->{_m} );
my $num_str = Math::BigInt->new(
$self->as_float * 10**($self->format()->{FRAC_DIGITS} +
$self->{_prec_adj})
)->bstr;
return length($num_str) > 1
? substr($num_str,0, -1 * $self->{_prec_adj})
: $num_str;
}
The mathematical adjustment choice was based on a relatively rough
approximation of the relative level of floating point approximation
error for 64-bit double values, based on the size of the integer and
decimal part together. I then used truncation to remove the error-prone
decimal part section back to the original value decimal place accuracy.
Using brute-force testing, I have verified that this gives 100% accurate
amount results up to $100,000,000.00 (2-decimal place values, 64-bit
doubles). It's highly likely that this same method would work fine for
3-4 decimal place values.
I recommend additional research be done determine the best level of
floating point accuracy required for portability with 32 and 64 bit
double floats, but for the time being, I strongly recommend integration
of a general solution like the one I presented be done to guarantee that
as_int() method returns 100% accurate values.
-Eric Rybski (rybskej@yahoo.com)
Subject: | math_currency_as_int_fp_err.pl |
#!/usr/bin/perl
use sigtrap qw(die untrapped normal-signals stack-trace error-signals);
use Math::Currency;
my $start = shift @ARGV || 0;
my $dollar_range = shift @ARGV || 10000000;
my ($iter, $curr_err_cnt, $curr2_err_cnt) = (0, 0, 0);
my $time = time();
warn "Test range: $start..$dollar_range";
for my $dollar ($start..$dollar_range) {
for my $cent (0..99) {
my $amt = "$dollar.".($cent < 10 ? "0$cent" : $cent);
my $int = ($dollar ? $dollar : "")
.($dollar ? $cent < 10 ? "0$cent" : $cent : $cent);
my $curr = new Math::Currency $amt;
my $curr_int = $curr->as_int;
if ($curr_int ne $int) {
warn " Math::Currency $curr(".$curr_int.") ne $int\n";
$curr_err_cnt++;
}
warn "processed $amt so far...\n" if $dollar % 1000 == 0 && $cent % 100 == 0;
$iter++;
}
}
END {
my $dr = new Math::Currency $dollar_range;
print "Done! (took ".(time() - $time)." seconds)\n";
print "From \$0 to $dr ($iter iterations)...\n";
warn "\tFound $curr_err_cnt Math::Currency fp errors\n";
}
Subject: | perl5_sol9_config.txt |
Summary of my perl5 (revision 5 version 8 subversion 7) configuration:
Platform:
osname=solaris, osvers=2.9, archname=sun4-solaris-64
uname='sunos usadev01 5.9 generic_117171-17 sun4u sparc sunw,sun-fire-v210 '
config_args='-Dcc=gcc -Doptimize=-O2 -Duse_large_files -Duse64bitall -Dusemorebits -Accflags=-mcpu=v9 -m64 -I/usr/local/include/v9 -I/usr/local/include -Aldflags=-mcpu=v9 -m64 -L/usr/lib/sparcv9 -L/usr/local/lib/sparcv9 -L/usr/local/BerkeleyDB64/lib -L/usr/lib -L/usr/local/lib -R/usr/lib/sparcv9 -R/usr/local/lib/sparcv9 -R/usr/local/BerkeleyDB64/lib -Alddlflags=-mcpu=v9 -m64 -L/usr/lib/sparcv9 -L/usr/local/lib/sparcv9 -L/usr/local/BerkeleyDB64/lib -L/usr/lib -L/usr/local/lib -R/usr/lib/sparcv9 -R/usr/local/lib/sparcv9 -R/usr/local/BerkeleyDB64/lib'
hint=recommended, useposix=true, d_sigaction=define
usethreads=undef use5005threads=undef useithreads=undef usemultiplicity=undef
useperlio=define d_sfio=undef uselargefiles=define usesocks=undef
use64bitint=define use64bitall=define uselongdouble=undef
usemymalloc=n, bincompat5005=undef
Compiler:
cc='gcc', ccflags ='-mcpu=v9 -m64 -Wa,-xarch=v9 -mcpu=v9 -m64 -I/usr/local/include/v9 -I/usr/local/include -fno-strict-aliasing -pipe -I/usr/local/include -I/usr/local/BerkeleyDB64/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64',
optimize='-O2',
cppflags='-mcpu=v9 -m64 -Wa,-xarch=v9 -mcpu=v9 -m64 -I/usr/local/include/v9 -I/usr/local/include -fno-strict-aliasing -pipe -I/usr/local/include -I/usr/local/BerkeleyDB64/include'
ccversion='', gccversion='3.4.2', gccosandvers='solaris2.9'
intsize=4, longsize=8, ptrsize=8, doublesize=8, byteorder=87654321
d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
ivtype='long', ivsize=8, nvtype='double', nvsize=8, Off_t='off_t', lseeksize=8
alignbytes=8, prototype=define
Linker and Libraries:
ld='gcc', ldflags =' -m64 -mcpu=v9 -m64 -L/usr/lib/sparcv9 -L/usr/local/lib/sparcv9 -L/usr/local/BerkeleyDB64/lib -L/usr/lib -L/usr/local/lib -R/usr/lib/sparcv9 -R/usr/local/lib/sparcv9 -R/usr/local/BerkeleyDB64/lib '
libpth=/usr/lib/sparcv9 /usr/local/lib/sparcv9 /usr/local/BerkeleyDB64/lib
libs=-lsocket -lnsl -ldb -ldl -lm -lc -lpthread
perllibs=-lsocket -lnsl -ldl -lm -lc -lpthread
libc=/usr/lib/sparcv9/libc.so, so=so, useshrplib=true, libperl=libperl.so
gnulibc_version=''
Dynamic Linking:
dlsrc=dl_dlopen.xs, dlext=so, d_dlsymun=undef, ccdlflags=' -R /usr/local/lib/perl5/5.8.7/sun4-solaris-64/CORE'
cccdlflags='-fPIC', lddlflags=' -G -m64 -mcpu=v9 -m64 -L/usr/lib/sparcv9 -L/usr/local/lib/sparcv9 -L/usr/local/BerkeleyDB64/lib -L/usr/lib -L/usr/local/lib -R/usr/lib/sparcv9 -R/usr/local/lib/sparcv9 -R/usr/local/BerkeleyDB64/lib'
Characteristics of this binary (from libperl):
Compile-time options: USE_64_BIT_INT USE_64_BIT_ALL USE_LARGE_FILES
Built under solaris
Compiled at Sep 16 2005 17:16:32
@INC:
/usr/local/lib/perl5/5.8.7/sun4-solaris-64
/usr/local/lib/perl5/5.8.7
/usr/local/lib/perl5/site_perl/5.8.7/sun4-solaris-64
/usr/local/lib/perl5/site_perl/5.8.7
/usr/local/lib/perl5/site_perl
.