Ошибка округления PHP - PullRequest
       73

Ошибка округления PHP

9 голосов
/ 07 февраля 2011

Я использую PHP 5.2.13 на моем сервере Linux. Я получаю странную ошибку при округлении чисел. Это мой тестовый пример:

<?php
echo "        " . round(1.505, 2) . "\n";
echo "       " . round(11.505, 2) . "\n";
echo "      " . round(111.505, 2) . "\n";
echo "     " . round(1111.505, 2) . "\n";
echo "    " . round(11111.505, 2) . "\n";
echo "   " . round(111111.505, 2) . "\n";
echo "  " . round(1111111.505, 2) . "\n";
echo " " . round(11111111.505, 2) . "\n";
echo "" . round(111111111.505, 2) . "\n";

Это результаты:

        1.51
       11.51
      111.51
     1111.51
    11111.51
   111111.51
  1111111.5
 11111111.51
111111111.51

Кто-нибудь знает, что вызывает это? Я не могу обновить PHP, так как это общий сервер.

Ответы [ 5 ]

10 голосов
/ 07 февраля 2011

Это потому, что число 1111111.505 не может быть точно представлено в записи с плавающей запятой. Самый близкий это может быть 1111111.5049999999. В итоге получается, что он преобразует число в вашем коде в 1111111.50499999999, а затем выполняет округление. Что приводит к 1111111.5. Числа с плавающей запятой имеют проблемы в том, что они не могут представлять множество даже, казалось бы, простых десятичных чисел с полной точностью. Например. число 0.1 не может быть точно представлено с использованием двоичных чисел с плавающей запятой. Используя Python, если вы введете 0.1, он вернет 0.10000000000001. Плюс или минус несколько нулей. Это причина того, что некоторые языки, такие как .Net, предоставляют тип данных «Десятичный», который может представлять все десятичные значения в пределах определенного диапазона и количества десятичных знаков. Недостатком десятичного типа данных является то, что он медленнее, и каждое число занимает больше места для хранения.

5 голосов
/ 18 декабря 2014

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

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

Эта проблема унесла много жизней два десятилетия назад.

25 февраля 1991 г. эта проблема при вычислении плавающего числа в ракетной батарее MIM-104 Patriot не позволила перехватить поступающую ракету "Скад" в Дахране, Саудовская Аравия, что привело к гибели 28 солдат из 14-го квартирмейстера армии СШАОтрыв.

Но почему это происходит?

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

Простой пример:

$a = '36';
$b = '-35.99';
echo ($a + $b);

Вы ожидаете, что он напечатает 0,01, верно?Но он напечатает очень странный ответ, такой как 0,009999999999998

Как и другие числа, числа с плавающей запятой double или float сохраняются в памяти в виде строки 0 и 1.Отличие числа с плавающей точкой от целого состоит в том, как мы интерпретируем 0 и 1, когда хотим посмотреть на них.Есть много стандартов, как они хранятся.

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

Десятичные числа не являютсяхорошо представлен в двоичном коде из-за нехватки места.Итак, вы не можете выразить 1/3 точно так же, как это 0,3333333 ..., верно?Почему мы не можем представить 0.01 как двоичное число с плавающей точкой по той же причине.1/100 - это 0,00000010100011110101110000 ..... с повторением 10100011110101110000.

Если 0,01 хранится в упрощенном виде и усеченной системой в 01000111101011100001010 в двоичном формате, то при переводе обратно в десятичное число будет выглядеть0,0099999 .... в зависимости от системы (64-битные компьютеры обеспечат вам гораздо более высокую точность, чем 32-битные).В этом случае операционная система решает, печатать ли ее, как она видит, или сделать ее более понятной для человека.Таким образом, это зависит от машины, как они хотят представить это.Но это может быть защищено на уровне языка различными способами.

Если вы отформатируете результат, введите echo number_format (0.009999999999998, 2);он напечатает 0,01.

Это потому, что в этом случае вы указываете, как следует читать и какую точность вам требуется.Ссылки: 1 , 2 , 3 , 4 , 5

3 голосов
/ 07 февраля 2011

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

Если вы действительно заботитесь о точности с произвольными числами с плавающей точкой, используйте bcmath или gmp , если оно доступно, хотя при использовании bcmath вам нужносделать функцию bcround ().Единственный, который я нашел, который действительно работает, размещен в комментариях на странице php.net bcscale :

function bcround($number, $scale=0) {
        $fix = "5";
        for ($i=0;$i<$scale;$i++) $fix="0$fix";
        $number = bcadd($number, "0.$fix", $scale+1);
        return    bcdiv($number, "1.0",    $scale);
}
3 голосов
/ 07 февраля 2011

Вам нужно обновить wetwaer, а не версию PHP.

echo "".раунд (1,505, 2)."\ n";

Вы, кажется, думаете, что просите PHP вернуть округленное значение 1,50 - но на самом деле получил , чтобы вернуть десятичную версию округленной версиидвоичной версии 1.505

Попробуйте ввести числа здесь и взгляните на двоичные представления.

2 голосов
/ 08 октября 2013

@ Shabbyrobe: ваша функция неверна: попробуйте округлить это число по шкале 2: 44069.3445

это должно быть 44069.35, но по умолчанию php round () - и ваша функция вернет 44069.34

Рабочий код члена php.net:

function mround($number, $precision=0) {

$precision = ($precision == 0 ? 1 : $precision);   
$pow = pow(10, $precision);

$ceil = ceil($number * $pow)/$pow;
$floor = floor($number * $pow)/$pow;

$pow = pow(10, $precision+1);

$diffCeil     = $pow*($ceil-$number);
$diffFloor     = $pow*($number-$floor)+($number < 0 ? -1 : 1);

if($diffCeil >= $diffFloor) return $floor;
else return $ceil;
}
...