Why is Perl inconsistent with sprintf rounding?

2020-07-03 11:05发布

问题:

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?

回答1:

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.



回答2:

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.