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

Я использую 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, так как это общий сервер.


person PiotrL    schedule 07.02.2011    source источник
comment
Что возвращает sprintf('%.2f', $n)?   -  person binaryLV    schedule 07.02.2011
comment
Кажется, это ошибка в PHP 5.2, потому что я могу воспроизвести это на PHP 5.2.11, но не в любой версии PHP 5.3.   -  person Björn    schedule 07.02.2011
comment
Вы заметили, что в ошибочной строке 7 цифр в целой части?   -  person Shrinath    schedule 07.02.2011
comment
@binaryLV sprintf выдает: 1111111,50   -  person PiotrL    schedule 07.02.2011
comment
Только что воспроизвел это с самой последней версией PHP 5.2 (5.2.17). Не удалось воспроизвести с 5.3.   -  person binaryLV    schedule 07.02.2011
comment
@binaryLV, если это воспроизводится в 5.2.17, я отправлю отчет об ошибке. Спасибо.   -  person PiotrL    schedule 07.02.2011
comment
Скорее всего, это просто репрезентативная ошибка. Ошибочное значение может существовать внутри просто как 1111111.5049999999. Значения с плавающей запятой по своей природе неточны. А в PHP5.3, вероятно, просто больше эвристики.   -  person mario    schedule 07.02.2011
comment
еще эхо вы можете использовать str_pad()   -  person powtac    schedule 07.02.2011
comment
Как сказал Марио - printf('%.10f', 1111111.505) печатает 1111111.5049999999.   -  person binaryLV    schedule 07.02.2011
comment
не смог воспроизвести его с 5.3.2   -  person powtac    schedule 07.02.2011
comment
PHP ответил на сообщение об ошибке, что эта проблема затрагивает только PHP 5.2, который больше не поддерживается. И действительно - при выпуске 5.2.16 было написано, что выпуск 5.2.16 знаменует собой конец поддержки PHP 5.2.   -  person binaryLV    schedule 07.02.2011


Ответы (6)


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

person Kibbee    schedule 07.02.2011
comment
Вы упомянули Python, но есть ли способ обойти это для PHP? Есть ли десятичный класс PHP? - person Wouter; 17.10.2018
comment
Лучшим вариантом для PHP является представление десятичных знаков в виде строк и использование расширения BCMath. - person dRamentol; 19.12.2019

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

Это не имеет прямого отношения к 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

person Selay    schedule 18.12.2014
comment
Насколько я понимаю, отказ ракеты был связан не с точностью вычислений с плавающей запятой, а с внутренними часами ракеты en.wikipedia.org/wiki/MIM-104_Patriot - person simonthesorcerer; 05.11.2015
comment
Копните глубже, чтобы понять, как работали эти внутренние часы. Этот пример преподается в университетах с оригинальным исходным кодом для иллюстрации. Из-за того, как компьютер Patriot выполняет свои вычисления, и того факта, что его регистры имеют длину всего 24 бита, преобразование времени из целого числа в действительное не может быть более точным, чем 24 бита. cs.drexel.edu/~introcs /Fa10/notes/07.1_FloatingPoint/ - person Selay; 06.11.2015
comment
Регистры компьютера имели длину всего 24 бита. Это, а также согласованность во временных задержках позволяют предположить, что ошибка была вызвана 24-битным представлением с фиксированной точкой 0,1 по основанию 2. Представление 0,1 по основанию 2 не является завершающим; для первых 23 двоичных разрядов после двоичной точки значение равно 0,1! (1 - 2-20). Использование 0,1 ! (1 - 2-20) при получении значения времени с плавающей запятой в секундах приведет к уменьшению всех значений времени на 0,0001%. ima.umn.edu/~arnold// катаклизмы/Patriot-dharan-skeel-siam.pdf Эта проблема широко изучается в субъектах тестирования программного обеспечения в большинстве университетов. - person Selay; 06.11.2015
comment
Если вы предпочитаете википедию как надежный источник, о чем я могу судить по вашей ссылке, вы можете увидеть, что эта проблема обсуждается в разделе с плавающей запятой в том же надежном источнике: en.wikipedia.org/wiki/Floating_point - person Selay; 06.11.2015
comment
Привет, спасибо за разъяснения. Я не предпочитаю википедию - она ​​просто первая выскочила, когда я погуглил название ракеты и Саудовской Аравии. без обид - person simonthesorcerer; 07.11.2015

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

эхо " ". раунд(1.505, 2) . "\п";

Кажется, вы думаете, что просите PHP вернуть округленное значение 1,505, но на самом деле он должен вернуть десятичную версию округленной версии двоичной версии 1,505.

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

person symcbean    schedule 07.02.2011

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

Если вам действительно важна точность с произвольными числами с плавающей запятой, используйте bcmath или gmp, если он доступен, хотя при использовании bcmath вам потребуется сделать функцию bround(). Единственный, который я нашел, который действительно работает, размещен в комментариях 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);
}
person Shabbyrobe    schedule 07.02.2011
comment
Я могу подтвердить это. То же самое происходит и в JS. Граница происходит с десятичными знаками выше 16383, что в виде целого числа равно 11111111111111 в двоичном формате, и ниже 32767, что равно 111111111111111 в двоичном формате. Сравните: (32767.505).toFixed(2) (32767,51) и (32768.505).toFixed(2) (32768,50). Если вам нужно придерживаться 2-значной точности и вы не можете использовать вышеуказанные библиотеки, рассмотрите возможность умножения всего на 100 (выполняйте расчеты в копейках, а не в долларах). - person Andrew; 07.02.2011

@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;
}
person Saint    schedule 08.10.2013

Код в ответе Shabbyrobe не обрабатывает отрицательные числа. Это рабочее улучшение кода, которое обрабатывает отрицательные числа так же, как и шкалу по умолчанию:

function bcround($number, $scale = null) {
    if (is_null($scale)) {
        $scale = bcscale();
    }
    $fix = ($number < 0) ? '-' : '';
    $fix .= '0.' . str_repeat('0', $scale) . '5';
    $number = bcadd($number, $fix, $scale + 1);
    return bcdiv($number, "1.0", $scale);
}
person Andreas Bogavčić    schedule 03.07.2020