Как мне исправить этот Perl-код, чтобы 1.1 + 2.2 == 3.3? - PullRequest
9 голосов
/ 12 октября 2010

Как мне исправить этот код, чтобы 1.1 + 2.2 == 3.3? Что на самом деле происходит здесь, что вызывает такое поведение? Я смутно знаком с проблемами округления и математики с плавающей запятой, но я думал, что это применимо только к делению и умножению и будет видно в выходных данных.

[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:

Большинство ответов до сих пор совпадают с «это проблема с плавающей запятой, да» и предоставляют обходные пути для нее. Я уже подозреваю, что это проблема. Как мне это продемонстрировать? Как мне заставить Perl выводить длинную форму переменных? Сохранение вычисления $ one + $ two во временной переменной и печать его не демонстрирует проблемы.

Edit:

Используя технику sprintf, продемонстрированную aschepler, я теперь могу «увидеть» проблему. Кроме того, использование bignum, как рекомендовано mscha и rafl, устраняет проблему несоответствия сравнения. Однако вывод sprintf по-прежнему указывает на то, что числа не являются «правильными». Это оставляет немного сомнений относительно этого решения.

Является ли bignum хорошим способом решения этой проблемы? Существуют ли какие-либо возможные побочные эффекты bignum, на которые мы должны обратить внимание при интеграции в более крупную существующую программу?

Ответы [ 8 ]

17 голосов
/ 12 октября 2010

См. Что должен знать каждый компьютерщик об арифметике с плавающей точкой .

Ничто из этого не относится к конкретному Perl: существует бесконечно большое количество действительных чисел, и, очевидно, все они не могут быть представлены с использованием только конечного числа битов.

Специальное "решение""использовать зависит от вашей конкретной проблемы.Вы пытаетесь отслеживать денежные суммы?Если это так, используйте числа произвольной точности (используйте больше памяти и больше ЦП, чтобы получить более точные результаты), предоставленные bignum .Вы делаете числовой анализ?Затем определитесь с точностью, которую вы хотите использовать, и используйте sprintf (как показано ниже) и eq для сравнения.

Вы всегда можете использовать:

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;
}

Выход:

1.0 + 2.0 = 3.0
1.1 + 2.2 = 3.3
6 голосов
/ 12 октября 2010

"Что должен знать каждый компьютерщик об арифметике с плавающей точкой"

По сути, Perl имеет дело с числами с плавающей точкой, в то время как вы, вероятно, ожидаете, что он будет использовать фиксированную точку. Самый простой способ справиться с этой ситуацией - это изменить ваш код так, чтобы вы использовали целые числа везде, кроме, возможно, финальной процедуры отображения. Например, если вы имеете дело с валютой USD, храните все суммы в долларах в копейках. 123 доллара и 45 центов становятся «12345». Таким образом, во время операций сложения и вычитания не возникает неопределенности с плавающей запятой.

Если это не вариант, рассмотрите комментарий Мэтта Кейна . Найдите хорошее значение эпсилон и используйте его всякий раз, когда вам нужно сравнить значения.

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

5 голосов
/ 12 октября 2010

Быстрый способ исправить числа с плавающей запятой - использовать bignum . Просто добавьте строку

use bignum;

к началу вашего скрипта. Очевидно, что это влияет на производительность, поэтому это может быть не лучшим решением для вас.

Более локализованное решение - использовать Math :: BigFloat явно, где вам нужна более высокая точность.

4 голосов
/ 12 октября 2010

Чтобы увидеть точные значения для ваших скаляров с плавающей запятой, задайте большую точность для sprintf:

print sprintf("%.60f", 1.1), $/;
print sprintf("%.60f", 2.2), $/;
print sprintf("%.60f", 3.3), $/;

Я получаю:

1.100000000000000088817841970012523233890533447265625000000000
2.200000000000000177635683940025046467781066894531250000000000
3.299999999999999822364316059974953532218933105468750000000000

К сожалению,% C99 для преобразования нене похоже на работу.perlvar упоминает устаревшую переменную $#, которая изменяет формат по умолчанию для печати числа, но прерывается, если я даю ей% f, а% g отказывается печатать "несущественные" цифры.

4 голосов
/ 12 октября 2010

С Руководство по числам с плавающей запятой :

Почему мои числа, такие как 0,1 + 0,2, не складываются в хороший раунд 0,3, а вместо этого яполучить странный результат, такой как 0.30000000000000004?

Поскольку внутренне, компьютеры используют формат (двоичная с плавающей запятой), который не может точно представить число как 0.1, 0.2 или 0.3 вообще.Когда код скомпилирован или интерпретирован, ваш «0.1» уже округляется до ближайшего числа в этом формате, что приводит к небольшой ошибке округления даже до того, как произойдет вычисление.

Что я могу сделать, чтобыизбежать этой проблемы?

Это зависит от того, какие вычисления вы делаете.

  • Если вам действительно нужны ваши результаты для точного суммирования, особенно когда вы работаете сденьги: используйте специальный десятичный тип данных .
  • Если вы просто не хотите видеть все эти дополнительные десятичные разряды: просто отформатируйте свой результат, округленный до фиксированного числа десятичных разрядов, при его отображении,
  • Если у вас нет доступного десятичного типа данных, альтернативой является работа с целыми числами, например, производите денежные вычисления полностью в центах.Но это больше работы и имеет некоторые недостатки.

Вы также можете использовать "нечеткое сравнение" , чтобы определить, достаточно ли два числа достаточно близки, чтобы предположить, что они будутбудь таким же, используя точную математику.

2 голосов
/ 12 октября 2010

abs($three - ($one + $two)) < $some_Very_small_number

1 голос
/ 12 октября 2010

Number :: Fraction позволяет работать с рациональными числами (дробями) вместо десятичных, что-то вроде этого (': constants' импортируется для автоматического преобразования строк типа '11/10' в Number :: Фракция объектов):

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";
        }
}

который печатает:

1 + 2 == 3
11/10 + 11/5 == 33/10
1 голос
/ 12 октября 2010

Используйте sprintf, чтобы преобразовать вашу переменную в форматированную строку, а затем сравнить полученную строку.

# 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);   
}

Этот тип проблемы возникает из-за того, что не существует идеального представления с фиксированной точкой для ваших дробей (0,1, 0,2 и т. Д.).Таким образом, значения 1.1 и 2.2 на самом деле сохраняются как что-то вроде 1.10000000000000...1 и 2.2000000....1 соответственно (я не уверен, станет ли оно немного больше или чуть меньше. В моем примере я предполагаю, что они становятся немного больше).Когда вы складываете их вместе, оно становится 3.300000000...3, что больше 3.3, что преобразуется в 3.300000...1.

...