Php intval() и floor() возвращаемое значение слишком низкое?

Поскольку тип данных float в 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. Но почему слово (115) возвращается 114?

Ответ 1

Попробуйте сделать это быстро:

$price_int = intval(floor($price_corrected + 0.5));

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

Общее правило для денежных расчетов заключается в том, чтобы никогда не использовать float (ни в базе данных, ни в вашем script). Вы можете избежать всех проблем, всегда сохраняя центы вместо долларов. Центами являются целые числа, и вы можете свободно добавлять их вместе и умножать на другие целые числа. Всякий раз, когда вы показываете номер, убедитесь, что вы вставили точку перед двумя последними цифрами.

Причина, по которой вы получаете 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 - маленькое целое число, ошибка округления равна нулю.
  • Он вычисляет произведение двух предыдущих чисел. Это также требует небольшого округления, чтобы найти ближайшее двоичное число с плавающей запятой. Ошибка округления в этой операции равна нулю.
  • Он преобразует двоичное число с плавающей запятой в десятичное число базы с точкой и печатает это представление. Это также связано с небольшим округлением.

Причина, по которой 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'

Если вы печатаете поплавки, помните: вы не всегда видите все цифры при печати float.

См. также большое предупреждение в начале PHP float docs.

Ответ 2

В других ответах, я считаю, объясняется причина и хороший обход проблемы.

Чтобы исправить проблему под другим углом:

Для хранения значений цены в MySQL вы должны, вероятно, посмотреть тип DECIMAL, который позволяет хранить точные значения с десятичными знаками.

Ответ 3

PHP делает округление на основе значительных цифр. Это скрывает неточность (в строке 2). Конечно, когда приходит этаж, он не знает ничего лучшего и полностью останавливает его.

Ответ 4

Возможно, это еще одно возможное решение для этой "проблемы":

intval(number_format($problematic_float, 0, '', ''));

Ответ 5

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

Решение состоит в том, чтобы убедиться, что, когда вы работаете с значениями с плавающей запятой, и вам нужно поддерживать точность - используйте функции gmp или функции математики BC - bcpow, bcmul et al. и проблема будет легко решена.

Например, вместо $ price_corrected = $price * 100;

использовать $price_corrected = bcmul ($ price, 100);