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??


person Josh    schedule 01.05.2009    source источник
comment
На самом деле это не имеет ничего общего с PHP и больше связано с тем, как все компьютеры обрабатывают данные с плавающей запятой. Возможно, вы захотите прочитать об этой теме, если вы раньше не сталкивались с неточностями с плавающей запятой.   -  person jmucchiello    schedule 01.05.2009
comment
@jmucchiello, я не согласен. Я понимаю, как компьютеры обрабатывают данные с плавающей запятой, и поэтому вместо этого я использую целочисленные данные. Это проблема PHP, поскольку PHP показывает 115, когда базовые данные явно равны 114.99999999...   -  person Josh    schedule 01.05.2009


Ответы (5)


Попробуйте это как быстрое решение:

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

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

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

Причина, по которой вы получаете 114 вместо 115, заключается в том, что 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 float.

person pts    schedule 01.05.2009
comment
$price_int = intval(floor($price_corrected + 0,5)) совпадает с $price_int = intval(round($price_corrected)). - person chaos; 01.05.2009
comment
@pts, хаос правильный, и на самом деле мой исходный код был таким. Нет разницы. Мой вопрос: почему PHP говорит, что $price_converted равно 115, когда на самом деле это 114,999999? (Я понимаю, почему это 114.99999...) - person Josh; 01.05.2009
comment
@pts, я ошибался, когда утверждал, что хаос правильный, я неправильно прочитал ваш код. Это может сработать. - person Josh; 01.05.2009
comment
Хорошо, это работает. Но что лучше, +0,5 или использовать round()? Каковы последствия использования round() - person Josh; 01.05.2009
comment
Используйте round(), потому что он автономен и чище. +0,5 может привести к большему округлению. - person pts; 02.05.2009
comment
Причина, по которой PHP выводит Corrected price = float(115), заключается в том, что var_dump выводит не точное число (!), а выводит число, округленное до n - 2 (или n - 1) цифр, где n цифр - точность вычисления. - person pts; 02.05.2009

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

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

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

person Chad Birch    schedule 01.05.2009
comment
+1, но я бы добавил, что не уверен, насколько неточным является тип FLOAT в mySQL. Это намного менее неточно, чем выполнение любого дополнительного моджо умножения, и оставляет ваши данные в гораздо лучшем состоянии для анализа. Во всяком случае, что он сказал. - person cgp; 01.05.2009
comment
@Chad Birch, разве INT не использует намного меньше места, чем DECIMAL? Я всегда находил, что логика INT +, чтобы узнать, сколько десятичных знаков нужно сместить, более эффективна, чем DECIMAL. Особенно по таким вещам, как цены. По сути, это столбец price_in_cents. - person Josh; 01.05.2009
comment
Нет, если в вашем DECIMAL много цифр. Согласно руководству, INT использует 4 байта (при условии, что вы используете фактический INT, а не SMALLINT или что-то еще), хранение DECIMAL различается: для каждого кратного девяти цифр требуется четыре байта, а для «оставшихся» цифр требуется некоторая доля четырех байт. Полная информация: dev.mysql.com/doc/refman/5.1 /en/storage-requirements.html - person Chad Birch; 01.05.2009
comment
Это очень полезно. По какой-то причине я думал, что DECIMAL на самом деле просто столбец CHAR()... может быть, это было в более старых версиях MySQL? Может быть, я просто сошел с ума... :-) - person Josh; 01.05.2009

PHP выполняет округление на основе значащих цифр. Он скрывает неточность (в строке 2). Конечно, когда появляется пол, он ничего не понимает и срезает его до упора.

person cgp    schedule 01.05.2009
comment
@altCognito, вы знаете, почему он скрывает неточность в строке 2? Я думаю, что это действительно то, что мой вопрос. - person Josh; 01.05.2009
comment
Я считаю, что это как-то связано с нормализацией, и ответ здесь: en.wikipedia.org /wiki/Floating_point#Умножение :| - person cgp; 01.05.2009

Возможно, это другое возможное решение этой «проблемы»:

intval(number_format($problematic_float, 0, '', ''));
person user498529    schedule 23.04.2013
comment
Вы, да, вы, сэр, заслуживаете плюса. Просто наткнулся на странную ошибку, и это решило ее. Я просто добавил пустые строки к этому вызову number_format(), чтобы результирующее число было целым числом без запятых и точек. - person Tomasz Kowalczyk; 13.01.2015
comment
Исправлена ​​ошибка, из-за которой intval(16.74 * 100) возвращал мне 1673, и принятый ответ совсем не помог. - person Billy; 14.04.2016

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

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

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

используйте $price_corrected = bcmul($price,100);

person Kudehinbu Oluwaponle    schedule 27.04.2013