I am just wondering if I sumbled upon an error in perl an error in my logic
or somehtign else see the below perl one liners
$ perl -e '$n = 0.945; $r = sprintf("%.2f", $n); print "$r\n";'
0.94
$ perl -e '$n = 0.9451; $r = sprintf("%.2f", $n); print "$r\n";'
0.95
$ perl -e '$n = 0.9450; $r = sprintf("%.2f", $n); print "$r\n";'
0.94
$ perl -e '$n = 0.94500; $r = sprintf("%.2f", $n); print "$r\n";'
0.94
$ perl -e '$n = 0.945001; $r = sprintf("%.2f", $n); print "$r\n";'
0.95
Now I expected perl to round the number: 0.945 to 0.95 and not to 0.94 like
it is doing. I have no idea why it is doing this but I sure don't like it.
The problem is I have to do some VAT calculations on thousands of invoices
and invoice elements. I found that in rare cases I'm of by 1 cent or even 3
cents if there are lots of elements. Going over the logic I ran into this
problem.
Cany anyone advise me how to deal with this as I need to find a way to round
correctly in all cases not just in cases where Perl decides it is a good
idea to do so. :-)
Regards,
Rob
The problem is likely caused by the fact that floating-point numbers in most
modern computers are represented by using binary digits (and binary fractions)
instead of decimal ones. So there may be rounding errors. As a result, it is
not advisable to use the regular floating-point numbers for dealing with
monetary values. Some of the options you may wish to consider instead are:
1. Use a rational library for computation such as Math::BigRat.
2. Use a binary-coded-decimal (BCD) system. Last time I investigated I
couldn't find one for Perl, though , and I've misplaced the link to the
downloadable software library at IBM for that. (Though Python has something
like that in pure-Python)
3. Work always in the smallest unit - cents or whatever.
Regards,
Shlomi Fish
> Regards,
>
> Rob
--
-----------------------------------------------------------------
Shlomi Fish http://www.shlomifish.org/
My Aphorisms - http://www.shlomifish.org/humour.html
God considered inflicting XSLT as the tenth plague of Egypt, but then
decided against it because he thought it would be too evil.
Please reply to list if it's a mailing list post - http://shlom.in/reply .
I eventually forgot to add the link to this article:
http://docs.sun.com/source/806-3568/ncg_goldberg.html
"What Every Computer Scientist Should Know About Floating-Point Arithmetic".
It's kinda long, but I mostly enjoyed it and found it enlightening.
Regards,
Shlomi Fish
Thank you all that replied, in the end I copied the rules I learned in
elementary school about how to round a number to two decimals. I'm sure it
is a horrible hack and can be done a lot faster, but with a country not
being able to process their invoices into the accounting system this was the
best way I could come up with that didn't in some cases result in a error.
I am not using any of the suggested modules because the production system is
outside of my control and getting a module installed can take days if not
longer due to processes procedures forms and so on. The country has only a
few days left till the end of the month by which time this should absolutely
be working.
This is the routine as it looks now (I would be happy to hear about errors
or improvements that you might see)
sub round {
my $number = shift;
if ( $number == 0 ) { return 0.00; }
if ( length( $number ) <= 4 ) { return $number; }
my $int = int( $number ); # Get the whole number
my $dec = $number - $int; # Get the decimals only
$dec = sprintf( "%.15f", $dec ) * 1000000000000000;
my @digits = split( //, $dec );
my @reverse_digits = reverse( @digits );
my $number_of_digits = $#reverse_digits - 1;
my $remember = 0;
my $tracker = 0;
for( my $x = 0; $x <= $number_of_digits; $x++ ) {
$reverse_digits[$x] = $reverse_digits[$x] + $remember;
my $result = $reverse_digits[$x] + 5;
if ( $result >= 10 ) {
$remember = 1;
} else {
$remember = 0;
}
$tracker = $x;
}
if ( $reverse_digits[$tracker] >= 10 ) {
$reverse_digits[$tracker] = $reverse_digits[$tracker] - 10;
$reverse_digits[$tracker + 1]++;
if ( $reverse_digits[$tracker + 1] == 10 ) {
$reverse_digits[$tracker + 1] = 0;
$int++;
}
}
my @reordered_digits = reverse(@reverse_digits);
splice( @reordered_digits, 2 );
my $decimal_amount = join( '', @reordered_digits );
return "$int.$decimal_amount";
}
Again, thanks for all the help and suggestions,
Rob
I tried it out. Is this supposed to be correct?
$ perl Perl_rounding_errors.pl
0.524 --> 0.52
0.5241 --> 0.52
0.5242 --> 0.52
0.5243 --> 0.52
0.5244 --> 0.52
0.5245 --> 0.53
0.5246 --> 0.53
0.5247 --> 0.53
0.5248 --> 0.53
0.5249 --> 0.53
John
--
The programmer is fighting against the two most
destructive forces in the universe: entropy and
human stupidity. -- Damian Conway
i must have not see the OP's reply yet so this is mostly for him.
>> This is the routine as it looks now (I would be happy to hear about errors
>> or improvements that you might see)
>> sub round {
>> my $number = shift;
>> if ( $number == 0 ) { return 0.00; }
that is no different than returning 0.
>> if ( length( $number ) <= 4 ) { return $number; }
>> my $int = int( $number ); # Get the whole number
>> my $dec = $number - $int; # Get the decimals only
>>
>> $dec = sprintf( "%.15f", $dec ) * 1000000000000000;
that will introduce more errors. you can just edit the text if you want
to mung the decimal point accurately. you are converting between numbers
and text back and forth for no reason.
>> my @digits = split( //, $dec );
>> my @reverse_digits = reverse( @digits );
>> my $number_of_digits = $#reverse_digits - 1;
this is insane code.
>> my $remember = 0;
>> my $tracker = 0;
>> for( my $x = 0; $x <= $number_of_digits; $x++ ) {
>> $reverse_digits[$x] = $reverse_digits[$x] + $remember;
>> my $result = $reverse_digits[$x] + 5;
>> if ( $result >= 10 ) {
>> $remember = 1;
>> } else {
>> $remember = 0;
>> }
>> $tracker = $x;
>> }
>> if ( $reverse_digits[$tracker] >= 10 ) {
>> $reverse_digits[$tracker] = $reverse_digits[$tracker] - 10;
>> $reverse_digits[$tracker + 1]++;
>> if ( $reverse_digits[$tracker + 1] == 10 ) {
>> $reverse_digits[$tracker + 1] = 0;
>> $int++;
>> }
>> }
>> my @reordered_digits = reverse(@reverse_digits);
>> splice( @reordered_digits, 2 );
>> my $decimal_amount = join( '', @reordered_digits );
>> return "$int.$decimal_amount";
i can't see this code making any sense or doing the 'right' thing. you
need to learn how to let perl work for you.
the classic way to round is to add .5 to the right decimal spot and then
truncate at the right spot. have you tried that? also perl does rounding
just fine. the problem is that you don't know what perl thinks your
numbers really are when you only print 2 decimal digits. print more (as
you do in that insane code) and you will see. finally as others have
stated, USE integers for money math. never use floats. then you will
never have this problem.
uri
--
Uri Guttman ------ u...@stemsystems.com -------- http://www.sysarch.com --
----- Perl Code Review , Architecture, Development, Training, Support ------
--------- Gourmet Hot Cocoa Mix ---- http://bestfriendscocoa.com ---------
Or use COBOL or PL/I ;-). Seriously, though, you need to be using fixed-point
arithmetic, which in Perl is only available as integer arithmetic. Doing it
in cents is not enough--it has to be carried out to the tenth of a cent. To round,
add 5 to the low-order digit, which represents a tenth of a cent, then round
off the resulting low-order digit. You do need to keep track of the scale so you can
put the decimal point in the right place when
you write out the values. Even that method could result in some inaccurate rounding
if the computer keeps integers in binary but you expect your rounding to be done
in base 10.
I did a quick program a while back to compute stock basis. My proof results were a few cents
off from those of the stock broker. I attribute that to the fact that I was using floating point
and the "standard" rounding algorithm, instead of converting to fixed-point integer
arithmetic. It didn't matter because I only needed the values for my tax returns, and a
few cents has no negligible effect on the bottom line.
Since you are using real accounting data, it really has to be accurate. For
that, you need fixed-point currency calculations and a way to round in base 10.
--Marilyn
> --
> To unsubscribe, e-mail: beginners-...@perl.org
> For additional commands, e-mail: beginne...@perl.org
> http://learn.perl.org/
>
>
> Hi John,
Weirdly enough this is how the Uniface system feeding me the numbers rounds
them, since this is the system used in 32 countries and accepted by all the
tax authorities I have to emulate that rounding of numbers. I would indeed
have done the more correct methods but unfortunately they do not match the
magic of Uniface.
As the users look in the Uniface system and compare the numbers with what I
feed into the accounting system I cannot do anything else but copy that
behaviour.
@Uri, returning 0 or 0.00 does make a small difference in that the receiving
program likes the 0.00 better then the plain 0 don't ask me why ask our
mainframe colleagues why. I wish I could let perl do the work for me but it
simply does not regardless of how I try and do it return the "right" numbers
when I compare them with the numbers in the Uniface system. I have a sample
set of a month worth of invoices for 1 country and the below code is the
only way in which I was able to emulate the behaviour of the Uniface system
in all cases. My feeling is that the Uniface system does the same kind of
weird trick.
I have tried pretty much all Math::*, POSIX and other rounding libraries I
could find on CPAN and they all ended up with a few cents different here and
there. Now I know and can see that those libraries make a lot more sense and
actually produce correct results in pretty much all situations but
unfortunately mine is not one of them. As for the sanity of using Uniface...
I just hope that when the Uniface version is updated from 7 to 9 or 10 this
rounding algorithm doesn't change, though I would not be surprised if it
does.
RC> @Uri, returning 0 or 0.00 does make a small difference in that the
RC> receiving program likes the 0.00 better then the plain 0 don't ask
RC> me why ask our mainframe colleagues why. I wish I could let perl
RC> do the work for me but it simply does not regardless of how I try
RC> and do it return the "right" numbers when I compare them with the
RC> numbers in the Uniface system. I have a sample set of a month
RC> worth of invoices for 1 country and the below code is the only way
RC> in which I was able to emulate the behaviour of the Uniface system
RC> in all cases. My feeling is that the Uniface system does the same
RC> kind of weird trick.
from perl's perspective 0 and 0.0 are the same as long as you deal with
numbers.
perl -le '$x = 0 ; print $x ; $x = 0.00 ; print $x'
0
0
so you can't tell the difference without getting into the guts with xs
or some special code. you mention a receiving program which may make a
difference but again perl won't know or care. there may be more code
than you are showing which makes a difference between the two but basic
perl doesn't know or care.
I completely agree perl couldn't care less if you return 0 or 0.00 it will
simply interpet it as a 0, but the recieving mainframe system gets all warm
and fuzzy when you send it 0.00 and a bit unhappy when it recieves a lone 0.
Since the whole point of the round routine is to return a whole number with
two digits behind the dot I figured it would be better to have it always
return just that even if the total value ends up being 0. It saves me from
having to format the values later on as i am now 100% certain that i will
always recieve a hole number with two digits after the dot.
*(And that, boys and girls is why adding comments to your code is a good
idea it helps explain these kinds of things) :D*
Sorry for that I should have at least explained that a bit better in a
simple comment behind te return or something along those lines.
RC> On Tue, Apr 27, 2010 at 9:16 AM, Uri Guttman <u...@stemsystems.com> wrote:
RC> so you can't tell the difference without getting into the guts
RC> with xs or some special code. you mention a receiving program
RC> which may make a difference but again perl won't know or
RC> care. there may be more code than you are showing which makes
RC> a difference between the two but basic perl doesn't know or
RC> care.
RC> I completely agree perl couldn't care less if you return 0 or 0.00
RC> it will simply interpet it as a 0, but the recieving mainframe
RC> system gets all warm and fuzzy when you send it 0.00 and a bit
RC> unhappy when it recieves a lone 0. Since the whole point of the
RC> round routine is to return a whole number with two digits behind
RC> the dot I figured it would be better to have it always return just
RC> that even if the total value ends up being 0. It saves me from
RC> having to format the values later on as i am now 100% certain that
RC> i will always recieve a hole number with two digits after the dot.
RC> (And that, boys and girls is why adding comments to your code is a
RC> good idea it helps explain these kinds of things) :D Sorry for
RC> that I should have at least explained that a bit better in a
RC> simple comment behind te return or something along those lines.
you have never specified any IPC between perl and the mainframe. all you
showed was returning 0.00 which is just 0 in perl. perl won't format it
as you wish without doing something yourself. you are not telling the
whole story which is a problem.