41

The code:

<?php
$start = 0;
$stop  = 1;
$step = ($stop - $start)/10;
$i = $start + $step;
while ($i < $stop) {
    echo($i . "<br/>");
    $i += $step;
}
?>

The output:

0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1 <-- notice the 1 printed when it shouldn't

Created a fiddle

One more: if you set $start = 1 and $stop = 2 it works fine.

Using: php 5.3.27

Why is the 1 printed?

ST3
  • 8,826
  • 3
  • 68
  • 92
kcsoft
  • 2,917
  • 19
  • 14
  • 1
    surprisingly, when you divide the interval into 20: $step = ($stop - $start)/20; it is also ok – user4035 Sep 05 '13 at 21:13
  • 3
    Possibly a rounding floating-point error. – Blazemonger Sep 05 '13 at 21:15
  • 1
    Related to http://stackoverflow.com/questions/3726721/php-math-precision – j08691 Sep 05 '13 at 21:19
  • 2
    Your question implies that you expect `$i` to be exactly equal to 1 after 9 iterations. You can [never compare floating point numbers for equality](http://floating-point-gui.de/errors/comparison/). – Jon Sep 05 '13 at 21:20
  • Possible duplicate of [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – miken32 Nov 30 '18 at 01:33

3 Answers3

53

Because not only float math is flawed, sometimes its representation is flawed too - and that's the case here.

You don't actually get 0.1, 0.2, ... - and that's quite easy to check:

$start = 0;
$stop  = 1;
$step = ($stop - $start)/10;
$i = $start + $step;
while ($i < $stop) {
   print(number_format($i, 32) . "<br />");
   $i += $step;
}

The only difference here, as you see, is that echo replaced with number_format call. But the results are drastically different:

0.10000000000000000555111512312578
0.20000000000000001110223024625157
0.30000000000000004440892098500626
0.40000000000000002220446049250313
0.50000000000000000000000000000000
0.59999999999999997779553950749687
0.69999999999999995559107901499374
0.79999999999999993338661852249061
0.89999999999999991118215802998748
0.99999999999999988897769753748435

See? Only one time it was 0.5 actually - because that number can be stored in a float container. All the others were only approximations.

How to solve this? Well, one radical approach is using not floats, but integers in similar situations. It's easy to notice that have you done it this way...

$start = 0;
$stop  = 10;
$step = (int)(($stop - $start) / 10);
$i = $start + $step;
while ($i < $stop) {
   print(number_format($i, 32) . "<br />");
   $i += $step;
}

... it would work ok:

Alternatively, you can use number_format to convert the float into some string, then compare this string with preformatted float. Like this:

$start = 0;
$stop  = 1;
$step = ($stop - $start) / 10;
$i = $start + $step;
while (number_format($i, 1) !== number_format($stop, 1)) {
   print(number_format($i, 32) . "\n");
   $i += $step;
}
Community
  • 1
  • 1
raina77ow
  • 103,633
  • 15
  • 192
  • 229
  • 4
    One solution would be to keep your `$step` an integer: `$start=0; $stop=10; $step=1;` and do the division inside the loop. – Blazemonger Sep 05 '13 at 21:19
  • @RichBradshaw The best way to guard would be to switch from floating point arithmetic to integers by multiplying stop by 10 and increasing i by 1 instead of 0.1 – user4035 Sep 05 '13 at 21:20
  • 1
    To keep this from happening, in a language where you're intentionally using floats, the traditional solution is to compare with a margin of error. A hackier way is to make all the numbers bigger (multiply by 10 or something), which can decrease momentary imprecision, but might lead to rounding errors. – ssube Sep 05 '13 at 21:20
  • Oh, your answer was first! I hope my answer will at least contribute to yours. – Kevin A. Naudé Sep 05 '13 at 21:20
  • Lesson learned: avoid comparing floats. Looks like it affects other languages that sotre floats in IEEE754 standard (https://en.wikipedia.org/wiki/IEEE_754#Basic_formats) including JavaScript – kcsoft Sep 05 '13 at 21:24
  • @kcsoft All languages with binary floats will have this problem. Decimal floats are immune, including those of IEEE 754-2008. – Kevin A. Naudé Sep 05 '13 at 21:26
  • I can't help thinking that linking [my question](http://stackoverflow.com/questions/14082287/why-are-floating-point-numbers-printed-so-differently) about float numbers representation here would be helpful as well. This one is about PHP, but the problem it's about is the same - have we had this approximations represented precisely (or at least in the same way), there were a bit less of similar questions, I suppose. – raina77ow Sep 05 '13 at 21:26
  • 1
    @KevinA.Naudé I found your answer very interesting, with very good information about why it works this way. You should definitely leave it up :) – Jordan Sep 05 '13 at 21:39
13

The problem is that the number in the variable $i is not 1 (when printed). Its actual just less than 1. So in the test ($i < $stop) is true, the number is converted to decimal (causing rounding to 1), and displayed.

Now why is $i not 1 exactly? It is because you got there by saying 10 * 0.1, and 0.1 cannot be represented perfectly in binary. Only numbers which can be expressed as a sum of a finite number of powers of 2 can be perfectly represented.

Why then is $stop exactly 1? Because it isn't in floating point format. In other words, it is exact from the start -- it isn't calculated within the system used floating point 10 * 0.1.

Mathematically, we can write this as follows: enter image description here

A 64 bit binary float can only hold the first 27 non-zero terms of the sum which approximates 0.1. The other 26 bits of the significand remain zero to indicate zero terms. The reason 0.1 isn't physically representable is that the required sequence of terms is infinite. On the other hand, numbers like 1 require only a small finite number of terms and are representable. We'd like that to be the case for all numbers. This is why decimal floating point is such an important innovation (not widely available yet). It can represent any number that we can write down, and do so perfectly. Of course, the number of available digits remains finite.

Returning to the given problem, since 0.1 is the increment for the loop variable and isn't actually representable, the value 1.0 (although representable) is never precisely reached in the loop.

Kevin A. Naudé
  • 3,992
  • 19
  • 20
2

If your step will always be a factor of 10, you can accomplish this quickly with the following:

<?php
$start = 0;
$stop  = 1;
$step = ($stop - $start)/10;
$i = $start + $step;
while (round($i, 1) < $stop) { //Added round() to the while statement
    echo($i . "<br/>");
    $i += $step;
}
?>
Jerbot
  • 1,168
  • 7
  • 18