10

I simply want to know if $x is evenly divisible by $y. For example's sake assume:

$x = 70;
$y = .1;

First thing I tried is:

$x % $y

This seems to work when both numbers are integers but fails if they are not and if $y is a decimal less than 1 returns a "Division by zero" error, so then I tried:

fmod($x,$y)

Which returns equally confusing results, "0.099999999999996".

php.net states fmod():

Returns the floating point remainder of dividing the dividend (x) by the divisor (y)

Well according to my calculator 70 / .1 = 700. Which means the remainder is 0. Can someone please explain what I'm doing wrong?

  • Not all values have an exact float-point representation: it's thus not "[perfectly] evenly" (except in the case of integers) so much as "close enough". – user2864740 Feb 20 '14 at 17:36
  • [This happens with bcmod(), too.](http://codepad.viper-7.com/ztb1Y2) – John Conde Feb 20 '14 at 18:10

5 Answers5

11

One solution would be doing a normal division and then comparing the value to the next integer. If the result is that integer or very near to that integer the result is evenly divisible:

$x = 70;
$y = .1;

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

This subtracts both numbers and checks that the absolute difference is smaller than a certain rounding error. This is the usual way to compare floating point numbers, as depending on how you got a float the representation may vary:

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;'

See this demo:

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)
TimWolla
  • 31,849
  • 8
  • 63
  • 96
  • I'm interested in this solution. One question, why use subtraction and `< 0.0001` instead of simply `($x / $y) == round($x / $y)`? – But those new buttons though.. Feb 20 '14 at 17:43
  • @billynoah I amended my answer, explaining why floats should be compared like this. – TimWolla Feb 20 '14 at 17:50
  • Thanks Tim, I think I understand, but in what scenario would the method I suggested in the comment above return different result than yours? – But those new buttons though.. Feb 20 '14 at 18:04
  • For what bounds and/or constraints on `$x` and `$y` is this guaranteed to return a correct answer? – Eric Postpischil Feb 20 '14 at 18:06
  • @billynoah `$x = .3`, `$y = .1`. But it is always better to be safe than sorry! – TimWolla Feb 20 '14 at 18:07
  • @EricPostpischil It's going to fail when the division results in a number that is larger than `PHP_INT_MAX`. This causes PHP to convert it to floating point and lose precision. For example `$x = 8000000000000000000` and `$y = 0.0015` on my 64 bit PHP. – TimWolla Feb 20 '14 at 18:14
  • I'm actually more confused than ever about doing this with .3 and .1. `$x/$y = 3` and `round($x/$y) = 3` but `($x/$y) - round($x/$y) = -4.4408920985006E-16` ??? Why is this so difficult? obviously .3 / .1 is a whole number but it doesn't work. – But those new buttons though.. Feb 20 '14 at 18:51
  • @billynoah This probably warrants an own question, as comments are not suitable for answering these things. – TimWolla Feb 20 '14 at 18:55
  • @billynoah It works fine if you compare the absolute value of the result against a small enough floating number — as I said in the answer. – TimWolla Feb 20 '14 at 18:59
  • @TimWolla: Clearly the test produces a wrong answer for `$x = 1.2345`, `$y = 1.2344`, since the test indicates true, but 1.2345 is not evenly divisible by 1.2344. What criteria would guarantee the test produces a correct answer? – Eric Postpischil Feb 20 '14 at 19:39
  • 1
    @Eric - I could be wrong but I think this is as simple as adding another 0 to the comparison, i.e., `< 0.00001` should do it since your numbers have four decimal places. Seems like there should be a way to easily do this for _any_ number but i've yet to find a better solution than this. – But those new buttons though.. Feb 20 '14 at 20:58
  • 1
    @billynoah: That fixes one case but does not describe when this tests works and when it does not. This should be considered a serious problem in software: You have some code that appears to work, sort of, but it is known to break sometimes, and you do not know when it breaks and when it works. – Eric Postpischil Feb 20 '14 at 21:00
6

.1 doesn't have an exact representation in binary floating point, which is what causes your incorrect result. You could multiply them by a large enough power of 10 so they are integers, then use %, then convert back. This relies on them not being different by a big enough factor that multiplying by the power of 10 causes one of them to overflow/lose precision. Like so:

$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;
Tyler
  • 1,818
  • 2
  • 13
  • 22
  • The numbers are based on user input in a form. They could be anything numeric. – But those new buttons though.. Feb 20 '14 at 17:41
  • I fixed it so that the only int based size limitation is the final size of y, rather than the difference between x and y. The allowable difference is only bounded by the limitations of floats. – Tyler Feb 20 '14 at 17:52
  • 1
    +1 for ".1 doesn't have an exact representation in binary floating point" (See the Patriot bug: http://sydney.edu.au/engineering/it/~alum/patriot_bug.html) – dognose Feb 20 '14 at 18:11
4

There is a pure math library in bitbucket : https://bitbucket.org/zdenekdrahos/bn-php

The solution will be then :

php > require_once 'bn-php/autoload.php';
php > $eval = new \BN\Expression\ExpressionEvaluator();
php > $operators = new \BN\Expression\OperatorsFactory();
php > $eval->setOperators($operators->getOperators(array('%')));
php > echo $eval->evaluate('70 % 0.1'); // 0
0.00000000000000000000

tested on php5.3

credits : http://www.php.net/manual/en/function.bcmod.php#111276

markcial
  • 9,041
  • 4
  • 31
  • 41
0

Float-point representation varies from machine to machine. Thankfully there are standards. PHP typically uses the IEEE 754 double precision format for floating-point representation which is one of the most common standards. See here for more information on that. With that said take a look at this calculator for a better understanding as to the why. As for the how I like Tim's solution especially if you're dealing with user input.

hanleyhansen
  • 6,304
  • 8
  • 37
  • 73
0

As you said, using the modulus operator works fine when it's an integer, so why not set it up so that it operates on integers. In my case, I needed to check divisibility by 0.25:

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

In your case:

$input = 70.12
$x = round($input, 2);
$y = .1;
$result = ($x * 100) % ($y * 100);
neal
  • 33
  • 4