Почему Perl не согласуется с округлением sprintf?

Вот скрипт Perl:

#!/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";
}

Это выводит:

0.254 => 0.25
0.255 => 0.26
0.256 => 0.26
100.254 => 100.25
100.255 => 100.25
100.256 => 100.26

Для меня очень странно, что Perl несовместим с округлением. Я ожидаю, что оба числа, заканчивающиеся на .255, будут округлены до .26 Это верно для 0.255, но это неверно для числа 100.255.

Вот цитата из Perl Cookbook, http://docstore.mik.ua/orelly/perl/cookbook/ch02_04.htm,

спринтф. Формат f позволяет вам указать определенное количество десятичных знаков для округления его аргумента. Perl смотрит на следующую цифру, округляет ее, если она больше 5, и округляет в противном случае.

Но я не вижу никаких доказательств того, что это правильно в http://perldoc.perl.org/functions/sprintf.html

Это ошибка в sprintf или Perl Cookbook не так? Если это желаемое поведение, почему это работает таким образом?

Ответ 1

Perl использует базовую библиотеку C для форматирования. То, что эта библиотека может варьироваться от платформы к платформе. Даже POSIX говорит, что "младшая цифра должна быть округлена в соответствии с реализацией".

В glibc, который, возможно, используется большинством бинарных файлов perl, поведение, о котором вы видите, будет зависеть от нескольких вещей:

Во-первых, как указано в другом ответе, значение, которое вы считаете округленным, может быть не точно представлено в плавающей запятой, и каким образом округление будет определено, если оно является следующим более высоким или меньшим представимым числом.

Во-вторых, даже если значение точно представлено как на полпути между двумя возможными округлениями, glibc будет использовать округление банкира. То есть, он округляется до четной цифры. Таким образом, sprintf '%.1g', .25 будет производить .2, но sprintf '%.1g', .75 будет производить .8.

Цитата из Cookbook Perl просто неверна.

Ответ 2

Если вы добавите эту строку:

$number = sprintf '%.15f', $number;

перед печатью вы получите:

0.254000000000000 => 0.25
0.255000000000000 => 0.26
0.256000000000000 => 0.26
100.254000000000005 => 100.25
100.254999999999995 => 100.25
100.256000000000000 => 100.26

как вы можете видеть, 100.255 не точно 100.255, это связано с представлением чисел с плавающей запятой.