Определить, делится ли $x на $y без остатка в PHP

Я просто хочу знать, делится ли $x на $y без остатка. Например, предположим:

$x = 70;
$y = .1;

Первое, что я попробовал, это:

$x % $y

Кажется, это работает, когда оба числа являются целыми числами, но терпит неудачу, если это не так, и если $y является десятичным числом меньше 1, возвращает ошибку «Деление на ноль», поэтому я попробовал:

fmod($x,$y)

Который возвращает одинаково запутанные результаты: «0,099999999999996».

php.net заявляет fmod():

Возвращает остаток с плавающей запятой от деления делимого (x) на делитель (y)

Ну по моему калькулятору 70 / .1 = 700. Это означает, что остаток равен 0. Может кто-нибудь объяснить, что я делаю неправильно?


person billynoah    schedule 20.02.2014    source источник
comment
Не все значения имеют точное представление с плавающей запятой: таким образом, оно не [идеально] равномерно (за исключением случая с целыми числами) настолько, насколько достаточно близко.   -  person user2864740    schedule 20.02.2014
comment
Это происходит и с bcmod().   -  person John Conde    schedule 20.02.2014


Ответы (5)


Одним из решений было бы выполнить обычное деление, а затем сравнить значение со следующим целым числом. Если результатом является это целое число или очень близкое к этому целому числу, результат делится без остатка:

$x = 70;
$y = .1;

$evenlyDivisable = abs(($x / $y) - round($x / $y, 0)) < 0.0001;

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

php> 0.1 + 0.1 + 0.1 == 0.3
bool(false)
php> serialize(.3)
'd:0.29999999999999999;'
php> serialize(0.1 + 0.1 + 0.1)
'd:0.30000000000000004;'

Посмотрите эту демонстрацию:

php> $x = 10;
int(10)
php> $y = .1;
double(0.1)
php> abs(($x / $y) - round($x / $y, 0)) < 0.0001;
bool(true)
php> $y = .15;
double(0.15)
php> abs(($x / $y) - round($x / $y, 0)) < 0.0001;
bool(false)
person TimWolla    schedule 20.02.2014
comment
Меня интересует это решение. Один вопрос, зачем использовать вычитание и < 0.0001 вместо простого ($x / $y) == round($x / $y)? - person billynoah; 20.02.2014
comment
@billynoah Я изменил свой ответ, объяснив, почему поплавки следует сравнивать именно так. - person TimWolla; 20.02.2014
comment
Спасибо, Тим, кажется, я понимаю, но в каком случае метод, который я предложил в комментарии выше, вернет результат, отличный от вашего? - person billynoah; 20.02.2014
comment
При каких границах и/или ограничениях на $x и $y это гарантирует возврат правильного ответа? - person Eric Postpischil; 20.02.2014
comment
@billynoah $x = .3, $y = .1. Но всегда лучше перестраховаться! - person TimWolla; 20.02.2014
comment
@EricPostpischil Произойдет сбой, если в результате деления получится число больше PHP_INT_MAX. Это приводит к тому, что PHP преобразует его в число с плавающей запятой и теряет точность. Например, $x = 8000000000000000000 и $y = 0.0015 на моем 64-битном PHP. - person TimWolla; 20.02.2014
comment
На самом деле я еще больше запутался, чем когда-либо, делая это с .3 и .1. $x/$y = 3 и round($x/$y) = 3, но ($x/$y) - round($x/$y) = -4.4408920985006E-16 ??? Почему это так сложно? очевидно, .3 / .1 - это целое число, но оно не работает. - person billynoah; 20.02.2014
comment
@billynoah Это, вероятно, требует собственного вопроса, поскольку комментарии не подходят для ответа на эти вопросы. - person TimWolla; 20.02.2014
comment
@billynoah Это прекрасно работает, если вы сравниваете абсолютное значение результата с достаточно маленьким числом с плавающей запятой — как я сказал в ответе. - person TimWolla; 20.02.2014
comment
@TimWolla: Очевидно, что тест дает неправильный ответ для $x = 1.2345, $y = 1.2344, поскольку тест показывает, что это правда, но 1,2345 не делится без остатка на 1,2344. Какие критерии гарантируют, что тест даст правильный ответ? - person Eric Postpischil; 20.02.2014
comment
@ Эрик - я могу ошибаться, но я думаю, что это так же просто, как добавить еще один 0 к сравнению, то есть < 0.00001 должен сделать это, поскольку ваши числа имеют четыре десятичных знака. Кажется, должен быть способ легко сделать это для любого числа, но я еще не нашел лучшего решения, чем это. - person billynoah; 21.02.2014
comment
@billynoah: Это исправляет один случай, но не описывает, когда этот тест работает, а когда нет. Это следует рассматривать как серьезную проблему в программном обеспечении: у вас есть некоторый код, который вроде как работает, но известно, что он иногда ломается, и вы не знаете, когда он ломается, а когда работает. - person Eric Postpischil; 21.02.2014

.1 не имеет точного представления в двоичном формате с плавающей запятой, что и приводит к неправильному результату. Вы можете умножить их на достаточно большую степень 10, чтобы они были целыми числами, затем использовать %, а затем преобразовать обратно. Это зависит от того, что они не отличаются настолько большим коэффициентом, что умножение на степень 10 приводит к тому, что один из них переполняется/теряет точность. Вот так:

$x = 70;
$y = .1;
$factor = 1.0;
while($y*$factor != (int)($y*$factor)){$factor*=10;}
echo ($x*$factor), "\n";
echo ($y*$factor), "\n";
echo (double)(($x*$factor) % ($y*$factor))/$factor;
person Tyler    schedule 20.02.2014
comment
Цифры основаны на пользовательском вводе в форму. Они могут быть любыми числовыми. - person billynoah; 20.02.2014
comment
Я исправил это так, что единственным ограничением размера на основе int является окончательный размер y, а не разница между x и y. Допустимая разница ограничивается только ограничениями поплавков. - person Tyler; 20.02.2014
comment
+1 для .1 не имеет точного представления в двоичном формате с плавающей запятой (см. ошибку Patriot: sydney.edu.au/engineering/it/~alum/patriot_bug.html) - person dognose; 20.02.2014


Представление с плавающей запятой варьируется от машины к машине. Благо стандарты есть. PHP обычно использует формат двойной точности IEEE 754 для представления с плавающей запятой, который является одним из наиболее распространенных стандартов. Дополнительную информацию см. здесь. С учетом сказанного взгляните на этот калькулятор, чтобы лучше понять, почему . Что касается как, мне нравится решение Тима, особенно если вы имеете дело с пользовательским вводом.

person hanleyhansen    schedule 20.02.2014

Как вы сказали, использование оператора модуля отлично работает, когда это целое число, так почему бы не настроить его так, чтобы он работал с целыми числами. В моем случае мне нужно было проверить делимость на 0,25:

$input = 5.251
$x = round($input, 3); // round in case $input had more decimal places
$y = .25;
$result = ($x * 1000) % ($y * 1000);

В твоем случае:

$input = 70.12
$x = round($input, 2);
$y = .1;
$result = ($x * 100) % ($y * 100);
person neal    schedule 25.09.2016