Многие учебники по программированию об этом умалчивают: числа с плавающей точкой (типы float, double в Си, real в Паскале) нельзя использовать для расчёта стоимости товара, зарплаты сотрудника и т.п. из-за погрешности представления в двоичной системе. Как вы думаете, что выдаст этот код?
$x = 7 / 25 * 25;
echo $x . ' ' . ($x == 7 ? 'равно семи' : 'не равно семи');
На PHP 5.3 результат будет таким:
7 не равно семи
Почему так происходит
Числа с плавающей точкой представляются в компьютере в виде двоичных дробей. Те дроби, у которых в знаменателе — степень двойки, можно представить точно. Например, 50 копеек = 0,5 = 1/2 или 75 копеек = 0,75 = 3/4.
А те, у которых — не степень двойки, невозможно. Например, 10 копеек = 0,10 будет представлено как 7205759403792794 / 2
56 (см.
математические выкладки). А это число равно не 0,1, а чуть больше:
printf("%.56f", 0.1);
0.1000000000000000055511151231257827021182
Если выполнять много операций, то погрешность накапливается:
$sum = 0;
for($i = 0; $i < 1000000; $i++)
$sum += 0.1;
echo $sum;
Этот код выдаёт «100000.000001».
Также возникают ошибки при сравнении чисел (пример выше) и преобразовании в целое (integer).
Что делать?
В некоторых языках есть специальные типы данных, в которых дроби умножаются на степень 10. Проще говоря, сохраняется число копеек, например, целое число 1510 вместо дроби 15,10. В Visual Basic такой тип называется
Decimal.
В PHP есть
функции BC Math. К сожалению, у них невысокая производительность (из-за хранения чисел в строках) и громоздкий синтаксис:
bcscale(2);
$total = bcadd($total, bcmul($price, $count));
вместо
$total += $price * $count;
В идеале, PHP мог бы ввести десятичный тип или даже использовать его по умолчанию, так как web-скрипты чаще всего выполняют денежные расчёты. Пока такого типа нет, приходится мириться с неудобствами функций BC Math.
Если вы ограничиваетесь только сложением, вычитанием и умножением на количество, то ничего сложного в их использовании нет. При вычислении скидок и налогов, делении на число дней и т.п. нужно писать свой код для округления, так как BC Math всегда округляет неполную копейку в меньшую сторону:
bcscale(2);
print(bcdiv('2', '3') . "<br>");
// Выводит 0.66, а не 0.67
print(bcmul('5.45', '0.13'));
// Выводит 0.70 (точный результат 0.7085, нужно округлять до 0.71)
Ещё один вариант (
рекомендуется пользователями Stack Overflow): хранить все суммы в целых числах (integer) в копейках. Опять же, нужно следить за округлением при умножении на дроби (проценты, скидки, налоги) и делении. Этот способ я использовал, когда делал форму заказа по прайс-листу для одной из новокузнецких контор.
Ну и конечно, если где-то всё-таки используется плавающая точка, то не стóит «кричать» об этом, выводя излишнее количество знаков после запятой:
:)
Комментарии (3)
RSS свернуть / развернутьblognya
peter
blognya
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.