Why is Perl inconsistent with sprintf rounding? - perl

Here is a Perl script:
#!/usr/bin/perl
use strict;
use warnings;
use feature 'say';
my #numbers = qw(
0.254
0.255
0.256
);
foreach my $number (#numbers) {
my $rounded = sprintf '%.2f', $number;
say "$number => $rounded";
}
foreach my $number (#numbers) {
$number += 100;
my $rounded = sprintf '%.2f', $number;
say "$number => $rounded";
}
It outputs:
0.254 => 0.25
0.255 => 0.26
0.256 => 0.26
100.254 => 100.25
100.255 => 100.25
100.256 => 100.26
For me it is very strange that Perl is inconsistent with rounding. I expect that both number ending with .255 to be rounded as .26 It is true for 0.255, but it is false for the number 100.255.
Here is the quote from Perl Cookbook, http://docstore.mik.ua/orelly/perl/cookbook/ch02_04.htm,
sprintf . The f format lets you specify a particular number of decimal
places to round its argument to. Perl looks at the following digit,
rounds up if it is 5 or greater, and rounds down otherwise.
But I can't see any evidence that it is correct in http://perldoc.perl.org/functions/sprintf.html
Is it a bug in sprintf or Perl Cookbook is wrong? If it is desired behaviour, why does it work this way?

If you add this line:
$number = sprintf '%.15f', $number;
before printing, you will have:
0.254000000000000 => 0.25
0.255000000000000 => 0.26
0.256000000000000 => 0.26
100.254000000000005 => 100.25
100.254999999999995 => 100.25
100.256000000000000 => 100.26
as you can see, 100.255 is not exactly 100.255 this is due to representation of float numbers.

Perl uses the underlying C library for formatting. What this library does may vary from platform to platform. Even POSIX says "The low-order digit shall be rounded in an implementation-defined manner."
In glibc, which arguably is used by the majority of perl binaries out there, the behavior you see will be affected by a couple of things:
First, as pointed out in another answer, the value you think is being rounded may not be exactly representable in floating point, and which way the rounding goes will be determined by if it is the next higher or lower representable number.
Second, even if the value is exactly representable as halfway between two possible roundings, glibc will use banker's rounding. That is, it will round to an even digit. So sprintf '%.1g', .25 will produce .2, but sprintf '%.1g', .75 will produce .8.
The quote from the Perl Cookbook is just plain wrong.

Related

How to convert number to one type in perl [duplicate]

How do I fix this code so that 1.1 + 2.2 == 3.3? What is actually happening here that's causing this behavior? I'm vaguely familiar with rounding problems and floating point math, but I thought that applied to division and multiplication only and would be visible in the output.
[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
Edit:
Most of the answers thus far are along the lines of "it's a floating point problem, duh" and are providing workarounds for it. I already suspect that to be the problem. How do I demonstrate it? How do I get Perl to output the long form of the variables? Storing the $one + $two computation in a temp variable and printing it doesn't demonstrate the problem.
Edit:
Using the sprintf technique demonstrated by aschepler, I'm now able to "see" the problem. Further, using bignum, as recommended by mscha and rafl, fixes the problem of the comparison not being equal. However, the sprintf output still indicates that the numbers aren't "correct". That's leaving a modicum of doubt about this solution.
Is bignum a good way to resolve this? Are there any possible side effects of bignum that we should look out for when integrating this into a larger, existing, program?
See What Every Computer Scientist Should Know About Floating-Point Arithmetic.
None of this is Perl specific: There are an uncountably infinite number of real numbers and, obviously, all of them cannot be represented using only a finite number of bits.
The specific "solution" to use depends on your specific problem. Are you trying to track monetary amounts? If so, use the arbitrary precision numbers (use more memory and more CPU, get more accurate results) provided by bignum. Are you doing numeric analysis? Then, decide on the precision you want to use, and use sprintf (as shown below) and eq to compare.
You can always use:
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;
}
Output:
1.0 + 2.0 = 3.0
1.1 + 2.2 = 3.3
"What Every Computer Scientist Should Know About Floating-Point Arithmetic"
Basically, Perl is dealing with floating-point numbers, while you are probably expecting it to use fixed-point. The simplest way to handle this situation is to modify your code so that you are using whole integers everywhere except, perhaps, in a final display routine. For example, if you're dealing with USD currency, store all dollar amounts in pennies. 123 dollars and 45 cents becomes "12345". That way there is no floating point ambiguity during add and subtract operations.
If that's not an option, consider Matt Kane's comment. Find a good epsilon value and use it whenever you need to compare values.
I'd venture to guess that most tasks don't really need floating point, however, and I'd strongly suggest carefully considering whether or not it is the right tool for your task.
A quick way to fix floating points is to use bignum. Simply add a line
use bignum;
to the top of your script.
There are performance implications, obviously, so this may not be a good solution for you.
A more localized solution is to use Math::BigFloat explicitly where you need better accuracy.
From The Floating-Point Guide:
Why don’t my numbers, like 0.1 + 0.2 add up to a nice round 0.3, and
instead I get a weird result like
0.30000000000000004?
Because internally, computers use a
format (binary floating-point) that
cannot accurately represent a number
like 0.1, 0.2 or 0.3 at all.
When the code is compiled or
interpreted, your “0.1” is already
rounded to the nearest number in that
format, which results in a small
rounding error even before the
calculation happens.
What can I do to avoid this problem?
That depends on what kind of
calculations you’re doing.
If you really need your results to add up exactly, especially when you
work with money: use a special decimal
datatype.
If you just don’t want to see all those extra decimal places: simply
format your result rounded to a fixed
number of decimal places when
displaying it.
If you have no decimal datatype available, an alternative is to work
with integers, e.g. do money
calculations entirely in cents. But
this is more work and has some
drawbacks.
Youz could also use a "fuzzy compare" to determine whether two numbers are close enough to assume they'd be the same using exact math.
To see precise values for your floating-point scalars, give a big precision to sprintf:
print sprintf("%.60f", 1.1), $/;
print sprintf("%.60f", 2.2), $/;
print sprintf("%.60f", 3.3), $/;
I get:
1.100000000000000088817841970012523233890533447265625000000000
2.200000000000000177635683940025046467781066894531250000000000
3.299999999999999822364316059974953532218933105468750000000000
Unfortunately C99's %a conversion doesn't seem to work. perlvar mentions an obsolete variable $# which changes the default format for printing a number, but it breaks if I give it a %f, and %g refuses to print "non-significant" digits.
abs($three - ($one + $two)) < $some_Very_small_number
Use sprintf to convert your variable into a formatted string, and then compare the resulting string.
# 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);
}
This kind of problem occurs because there is no perfect fixed-point representation for your fractions (0.1, 0.2, etc). So the value 1.1 and 2.2 are actually stored as something like 1.10000000000000...1 and 2.2000000....1, respectively (I am not sure if it becomes slightly bigger or slightly smaller. In my example I assume they become slightly bigger). When you add them together, it becomes 3.300000000...3, which is larger than 3.3 which is converted to 3.300000...1.
Number::Fraction lets you work with rational numbers (fractions) instead of decimals, something like this (':constants' is imported to automatically convert strings like '11/10' into Number::Fraction objects):
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";
}
}
which prints:
1 + 2 == 3
11/10 + 11/5 == 33/10

How can you round down to a given number of decimal places in Perl

I'd like to round down to N decimal places (two places for cents). For example
5.0166 => 5.01
0.933 => 0.93
sprintf rounds to the nearest digit so doesn't produce what I want. For example
sprintf("%.2f", 5.0166) => 5.02
Use POSIX::floor (http://perldoc.perl.org/POSIX.html):
use POSIX;
$original = 5.0166;
$rounded = POSIX::floor($original*100)/100;
print "$rounded\n";
If you deal with negative numbers and you'd rather round towards zero rather than rounding down, then use:
$rounded = ($original < 0)?(POSIX::ceil($original*100)/100)
:(POSIX::floor($original*100)/100);
or just
$rounded = int($original*100)/100;
You can mess with a call to int together with multiplication and division to shift the decimal point back and forth, or you can use a string substitution, as shown below, which will work for anything less than 1E15 and greater than 1E-4
This will work
use strict;
use warnings;
use 5.010;
for (5.0166, 0.933) {
say s/\.\d\d\K.+//r;
}
output
5.01
0.93
Using more lines to show the steps.
$x=5.0166;
$y=int($x*100);
$y=$y/100;
print "$y\n";

Perl rounding error again

The example to show the problem:
having a number 105;
divide with 1000 (result 0.105)
rouded to 2 decimal places should be: 0.11
Now, several scripts - based on answers to another questions:
This is mostly recommented and mostly upvoted solution is using printf.
use 5.014;
use warnings;
my $i = 105;
printf "%.2f\n", $i/1000; #prints 0.10
but prints a wrong result. In the comment to
https://stackoverflow.com/a/1838885 #Sinan Unur says (6 times upvoted comment):
Use sprintf("%.3f", $value) for mathematical purposes too.
but, it didn't works "sometimes"... like above.
The another recommented solution 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 ;(
Wrong result too. Another recommened one bignum:
use 5.014;
use warnings;
use bignum ( p => -2 );
my $i = 105;
my $r = $i/1000;
say "$r"; #0.10 ;(
wrong again. ;(
Now the working ones:
use 5.014;
use warnings;
use Math::Round;
my $i = 105;
say nearest(0.01, $i/1000); #GREAT prints 0.11 :)
good result 0.11, however a comment here https://stackoverflow.com/a/571740
complains about it.
and finally another recommendation "by my own" function:
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);
}
prints 0.11 too, but can't prove it's correctness.
For the reference I was read:
How do you round a floating point number in Perl?
In Perl, how can I limit the number of places after the decimal point but have no trailing zeroes?
How do I set the floating point precision in Perl?
and many-many others...
and finally this too:
http://www.exploringbinary.com/inconsistent-rounding-of-printed-floating-point-numbers/
what gives a an really good overall view to the problem.
I understand than it is common problem to all languages, but please, after all
above reading - I still have this question:
What is the error-proof way in perl to round a floating point number to N
decimal places - with mathematically correct way, e.g. what will round results
like 105/1000 correctly to N decimal places without "surprises"...
You're expecting a specific behaviour when the number is exactly 0.105, but floating point errors mean you can't expect a number to be exactly what you think it is.
105/1000 is a periodic number in binary just like 1/3 is periodic in decimal.
105/1000
____________________
= 0.00011010111000010100011 (bin)
~ 0.00011010111000010100011110101110000101000111101011100001 (bin)
= 0.10499999999999999611421941381195210851728916168212890625
0.1049999... is less than 0.105, so it rounds to 0.10.
But even if you had 0.105 exactly, that would still round to 0.10 since sprintf rounds half to even. A better test is 155/1000
155/1000
____________________
= 0.00100111101011100001010 (bin)
~ 0.0010011110101110000101000111101011100001010001111010111 (bin)
= 0.1549999999999999988897769753748434595763683319091796875
0.155 should round to 0.16, but it rounds to 0.15 due to floating point error.
$ perl -E'$_ = 155; say sprintf("%.2f", $_/1000);'
0.15
$ perl -E'$_ = 155; say sprintf("%.0f", $_/10)/100;'
0.16
The second one works because 5/10 isn't periodic, and therein lies the solution. As Sinan Unur said, you can correct the error by using sprintf. But you have to round to an integer if you don't want to lose your work.
$ 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
That will fix the rounding error, allowing sprintf to properly round half to even.
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
If you want to round half up instead, you'll need to using something other than sprintf to do the final rounding. Or you could add s/5\z/6/; before the division by 10.
But that's complicated.
The first sentence of the answer is key. You're expecting a specific behaviour when the number is exactly 0.105, but floating point errors mean you can't expect a number to be exactly what you think it is. The solution is to introduce a tolerance. That's what rounding using sprintf does, but it's a blunt tool.
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
There's probably existing solutions that work along the same lines.
In the old Integer math days of programming, we use to pretend to use decimal places:
N = 345
DISPLAY N # Displays 345
DISPLAY (1.2) N # Displays 3.45
We learned a valuable trick when attempting to round sales taxes correctly:
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
By adding in 1/2 of the precision, I display the rounding correctly. When the number is 1.344, adding .005 made it 1.349, and chopping off the last digit displays dip lays 1.344. When I do the same thing with 1.345, adding in .005 makes it 1.350 and removing the last digit displays it as 1.35.
You could do this with a subroutine that will return the rounded amount.
Interesting...
There is a PerlFAQ on this subject. It recommends simply using printf to get the correct results:
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
For Pi, this works, but not for .105. However:
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
This looks like an issue with the way Perl stores .105 internally. Probably something like .10499999999 which would be correctly rounded downwards. I also noticed that Perl warns me about using round and rounding as possible future reserved words.
Your custom function should mostly work as expected. Here's how it works and how you can verify it's correct:
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);
}
However, the following it will throw an error for $float = 0, so there's an additional if statement at the beginning.
The nice thing about the above function is that it's possible to round to negative decimal places, allowing you round to the left of the decimal. For example, myround(123, -2) will give 100.
I'd add use bignum to your original code example.
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.
This solves the error you made in your use Math::BigFloat example by parsing your numbers into objects imediately and not waiting for you to pass the results of a round off error into Math::BigFloat->new
According to my experience, Perl printf/sprintf uses wrong algorithm. I made this conclusion considering at least the following simple example:
# 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
The integer part should not have any influence in this example, but it has.
I got this result with Perl 5.8.8.

Truncate (not round) decimal places in sprintf?

I want to display the dollar value with two digits after the decimal point to denote the cents. In the below program the output is 23.24. Perl rounds the decimal places. How to avoid it. I want the output to be 23.23.
$val=23.2395;
$testa=sprintf("%.2f", $val);
print "\n$testa\n $val";
print int(23.2395*100)/100; # => 23.23
Math::Round has different rounding methods.
use Math::Round 'nlowmult';
print nlowmult( 0.01, 23.2395 ); # 23.23

How do I set the floating point precision in Perl?

Is there a way to set a Perl script's floating point precision (to 3 digits), without having to change it specifically for every variable?
Something similar to TCL's:
global tcl_precision
set tcl_precision 3
Use Math::BigFloat or bignum:
use Math::BigFloat;
Math::BigFloat->precision(-3);
my $x = Math::BigFloat->new(1.123566);
my $y = Math::BigFloat->new(3.333333);
Or with bignum instead do:
use bignum ( p => -3 );
my $x = 1.123566;
my $y = 3.333333;
Then in both cases:
say $x; # => 1.124
say $y; # => 3.333
say $x + $y; # => 4.457
There is no way to globally change this.
If it is just for display purposes then use sprintf("%.3f", $value);.
For mathematical purposes, use (int(($value * 1000.0) + 0.5) / 1000.0). This would work for positive numbers. You would need to change it to work with negative numbers though.
I wouldn't recommend to use sprintf("%.3f", $value).
Please look at the following example:
(6.02*1.25 = 7.525)
printf("%.2f", 6.02 * 1.25) = 7.52
printf("%.2f", 7.525) = 7.53
Treat the result as a string and use substr. Like this:
$result = substr($result,0,3);
If you want to do rounding, do it as string too. Just get the next character and decide.
Or you could use the following to truncate whatever comes after the third digit after the decimal point:
if ($val =~ m/([-]?[\d]*\.[\d]{3})/) {
$val = $1;
}