Возвращаемое значение php intval() иfloor() слишком низкое?
-
03-07-2019 - |
Вопрос
Поскольку тип данных с плавающей запятой в PHP неточен, а FLOAT в MySQL занимает больше места, чем INT (и является неточным), я всегда сохраняю цены как INT, умножая их на 100 перед сохранением, чтобы гарантировать, что у нас есть ровно 2 десятичных знака точности. .Однако я считаю, что PHP ведет себя неправильно.Пример кода:
echo "<pre>";
$price = "1.15";
echo "Price = ";
var_dump($price);
$price_corrected = $price*100;
echo "Corrected price = ";
var_dump($price_corrected);
$price_int = intval(floor($price_corrected));
echo "Integer price = ";
var_dump($price_int);
echo "</pre>";
Произведенный выпуск:
Price = string(4) "1.15"
Corrected price = float(115)
Integer price = int(114)
Я был удивлен.Когда окончательный результат оказался на 1 ниже ожидаемого, я ожидал, что результат моего теста будет выглядеть примерно так:
Price = string(4) "1.15"
Corrected price = float(114.999999999)
Integer price = int(114)
что продемонстрировало бы неточность типа float.Но почему Floor(115) возвращает 114??
Решение
Попробуйте это как быстрое решение:
$price_int = intval(floor($price_corrected + 0.5));
Проблема, с которой вы столкнулись, не является ошибкой PHP: все языки программирования, использующие действительные числа с арифметикой с плавающей запятой, имеют схожие проблемы.
Общее практическое правило денежных расчетов — никогда не использовать числа с плавающей запятой (ни в базе данных, ни в вашем скрипте).Вы можете избежать всевозможных проблем, всегда храня центы, а не доллары.Центы являются целыми числами, и вы можете свободно складывать их и умножать на другие целые числа.Всякий раз, когда вы отображаете номер, обязательно вставляйте точку перед двумя последними цифрами.
Причина, по которой вы получаете 114 вместо 115, заключается в том, что floor
округляет в меньшую сторону до ближайшего целого числа, таким образом, Floor(114,999999999) становится 114.Более интересный вопрос: почему 1,15 * 100 равно 114,999999999, а не 115.Причина в том, что 1,15 — это не совсем 115/100, а немного меньше, поэтому, если вы умножите на 100, вы получите число чуть меньше 115.
Вот более подробное объяснение того, что echo 1.15 * 100;
делает:
- Он анализирует 1,15 в двоичное число с плавающей запятой.Это включает в себя округление, иногда округление немного в меньшую сторону, чтобы получить двоичное число с плавающей запятой, ближайшее к 1,15.Причина, по которой вы не можете получить точное число (без ошибки округления), заключается в том, что 1,15 имеет бесконечное количество цифр по основанию 2.
- Он преобразует 100 в двоичное число с плавающей запятой.Это предполагает округление, но поскольку 100 — небольшое целое число, ошибка округления равна нулю.
- Он вычисляет произведение двух предыдущих чисел.Это также включает в себя небольшое округление, чтобы найти ближайшее двоичное число с плавающей запятой.Ошибка округления в этой операции оказывается равной нулю.
- Он преобразует двоичное число с плавающей запятой в десятичное число по основанию 10 с точкой и печатает это представление.Это также предполагает небольшое округление.
Причина, по которой PHP печатает удивительные результаты Corrected price = float(115)
(вместо 114.999...) заключается в том, что var_dump
не печатает точное число (!), но печатает число, округленное до n - 2
(или n - 1
) цифр, где n цифр — точность расчета.Вы можете легко убедиться в этом:
echo 1.15 * 100; # this prints 115
printf("%.30f", 1.15 * 100); # you 114.999....
echo 1.15 * 100 == 115.0 ? "same" : "different"; # this prints `different'
echo 1.15 * 100 < 115.0 ? "less" : "not-less"; # this prints `less'
Если вы печатаете плавающие числа, помните: вы не всегда видите все цифры при печати числа с плавающей запятой.
См. также большое предупреждение в начале PHP с плавающей запятой документы.
Другие советы
Я считаю, что другие ответы охватывают причину и хорошее решение проблемы.
Чтобы решить проблему с другой стороны:
Для хранения значений цен в MySQL вам, вероятно, следует взглянуть на ДЕСЯТИЧНЫЙ тип, что позволяет хранить точные значения с десятичными знаками.
PHP выполняет округление по значащим цифрам.Он скрывает неточность (в строке 2).Конечно, когда появляется пол, он ничего не знает и опускает его до упора.
Возможно, это еще одно возможное решение этой «проблемы»:
intval(number_format($problematic_float, 0, '', ''));
Как уже говорилось, это не проблема PHP как такового. Это скорее проблема обработки дробей, которые не могут быть выражены как конечные значения с плавающей запятой, что приводит к потере символов при округлении.
Решение состоит в том, чтобы гарантировать, что когда вы работаете со значениями с плавающей запятой и вам необходимо поддерживать точность, используйте функции gmp или математические функции BC — bcpow, bcmul и др.и проблема легко решится.
Например, вместо $ pryal_corrected = $ price*100;
используйте $price_corrected = bcmul($price,100);