PHP Math bug?
Ik was bezig met een script tot ik steeds allerlei rare excepties terug kreeg die mijn script zelf had gegooid, omdat ik wilde voorkomen dat mijn script een getal groter dan 100 doorkreeg (om wat voor reden dan ook, niet interessant).
Het getal dat ik in het script gooide was echter niet groter dan 100, maar exact 100. Het getal is zo berekend:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
var = 100 / 9
getal = 0
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal = 0
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
getal += var
Nu wil ik vragen: Zie ik iets over het hoofd, doe ik iets fout? En zo niet, kunnen jullie het probleem ook eens testen op jullie php-bak, om te kijken of het op iedere versie voorkomt? Hier is een snippet om het probleem te testen:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
$number = 9;
echo 'Het getal = ' . $number . '. Het getal 100 wordt nu gedeeld door $number<br />';
$numberDivided = 100 / 9;
echo 'Het getal is gedeeld, en daar is deze uitkomst uitgekomen: ' . $numberDivided . '.<br />';
$numberProductByStar = $numberDivided * 9;
echo 'Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: ' . $numberProductByStar . '.<br />';
$numberProductByKruis = 0;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
echo 'Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: ' . $numberProductByKruis . '.<br />';
echo 'Kijk of $numberProductByStar groter is dan 100: ';
echo $numberProductByStar > 100 ? 'Ja' : 'Nee';
echo '.<br />';
echo 'Kijk of $numberProductByKruis groter is dan 100: ';
echo $numberProductByKruis > 100 ? 'Ja' : 'Nee';
?>
$number = 9;
echo 'Het getal = ' . $number . '. Het getal 100 wordt nu gedeeld door $number<br />';
$numberDivided = 100 / 9;
echo 'Het getal is gedeeld, en daar is deze uitkomst uitgekomen: ' . $numberDivided . '.<br />';
$numberProductByStar = $numberDivided * 9;
echo 'Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: ' . $numberProductByStar . '.<br />';
$numberProductByKruis = 0;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
echo 'Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: ' . $numberProductByKruis . '.<br />';
echo 'Kijk of $numberProductByStar groter is dan 100: ';
echo $numberProductByStar > 100 ? 'Ja' : 'Nee';
echo '.<br />';
echo 'Kijk of $numberProductByKruis groter is dan 100: ';
echo $numberProductByKruis > 100 ? 'Ja' : 'Nee';
?>
Code (php)
1
2
3
4
5
6
2
3
4
5
6
Het getal = 9. Het getal 100 wordt nu gedeeld door $number
Het getal is gedeeld, en daar is deze uitkomst uitgekomen: 11.111111111111.
Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: 100.
Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: 100.
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Nee
Het getal is gedeeld, en daar is deze uitkomst uitgekomen: 11.111111111111.
Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: 100.
Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: 100.
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Nee
Als de bug er wel is dan moet de laatste nee Ja worden.
Als dit echt een bug blijkt te zijn zal ik hem submitten op de bug-track van php.net. Bedankt voor jullie hulp alvast.
Gewijzigd op 01/01/1970 01:00:00 door Lasse
100/9 is 11,1111111111111111111111111111 enzovoorts
Dat x negen (ik rond nu op meer decimalen af dan dat PHP dat doet) kom je op:
99,999999999999999999999999999999
Dat is uiteraard niet gelijk aan 100. Een rekenmachine rond vaak af en geeft dus inderdaad terug dat het 100 is (vanwege die afronding, als je de laatste 9 in het rijtje naar boven afrond, dan moet je bij alle negens 1 optellen, dan kom je op 100. Het principe van afronden)
Maar omdat een rekenmachine en PHP zelf ook al tussentijds aan afronden doet, kom je inderdaad niet op 100 uit, maar op die 99,999....
Hij rondt die echter zelf ook weer af en geeft 100 terug. -> Maar de eigenlijke waarde van die berekening (die 99,999...) houdt die wel vast. En 99,999.... is ongelijk aan 100. Vandaar dus, voor de weergave rondt die af.
Nee, dat komt waarschijnlijk door het steeds afronden na de berekening. En 9 minuscule afrondingen kunnen er net voor zorgen dat ie net boven de 100 komt.
Maar zelfs al is het zo, het gaat mij om een vergelijking of het getal boven de honderd komt, en niet onder...
@Santhe: Voorekenen (steeds afronden):
Code (php)
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
0 + 11.111111 = 11.111111 (afronden naar beneden)
11.111111 + 11.111111 = 22.222222 (afronden naar beneden)
22.222222 + 11.111111 = 33.333333 (afronden naar beneden)
33.333333 + 11.111111 = 44.444444 (afronden naar beneden)
44.444444 + 11.111111 = 55.555556 (afronden naar boven)
55.555556 + 11.111111 = 66.666667 (afronden naar boven niet nodig, want de laatst mogelijke decimaal is al 7)
66.666667 + 11.111111 = 77.777778 (afronden naar boven niet nodig, want de laatst mogelijke decimaal is al 8)
77.777778 + 11.111111 = 88.888889 (afronden naar boven niet nodig, want de laatst mogelijke decimaal is al 9)
88.888889 + 11.111111 = 100 (de laatste negen wordt nul, waardoor de een na laatste 8, 9 wordt + nog 1 die er bij op moet maakt 0, en zo wordt het honderd.
11.111111 + 11.111111 = 22.222222 (afronden naar beneden)
22.222222 + 11.111111 = 33.333333 (afronden naar beneden)
33.333333 + 11.111111 = 44.444444 (afronden naar beneden)
44.444444 + 11.111111 = 55.555556 (afronden naar boven)
55.555556 + 11.111111 = 66.666667 (afronden naar boven niet nodig, want de laatst mogelijke decimaal is al 7)
66.666667 + 11.111111 = 77.777778 (afronden naar boven niet nodig, want de laatst mogelijke decimaal is al 8)
77.777778 + 11.111111 = 88.888889 (afronden naar boven niet nodig, want de laatst mogelijke decimaal is al 9)
88.888889 + 11.111111 = 100 (de laatste negen wordt nul, waardoor de een na laatste 8, 9 wordt + nog 1 die er bij op moet maakt 0, en zo wordt het honderd.
Dit zou dus ook exact honderd moeten maken.
Ik snap jou niet, 100 is niet groter dan 100. Je zou de vergelijking >= moeten maken.
@Orhan: Klopt, 100 is niet groter dan 100. En daarom vindt ik het raar dat PHP bij de door mij in de beginpost genoemde manier aangeeft dat 100 wel groter is dan 100 (oftewel, de ene 100 is net iets groter dan de andere 100).
Waar zie jij dat staan?
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Nee
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Ja
De tekst die jij geeft zou je moeten te zien krijgen als de bug er niet is...
0,999..., wat gewoon 1 is, alleen met wat nulletjes meer. Bewijsstuk 1, Bewijsstuk 2.
Orhan daar gaat het nu niet om. Php beweerd dat het getal groter is dan honderd, dit is dus niet zo.
Mjin test:
Hmm... Dit lijkt mij als Orhan daar gaat het nu niet om. Php beweerd dat het getal groter is dan honderd, dit is dus niet zo.
Mjin test:
Code (php)
1
2
3
4
5
6
2
3
4
5
6
Het getal = 9. Het getal 100 wordt nu gedeeld door $number
Het getal is gedeeld, en daar is deze uitkomst uitgekomen: 11.1111111111.
Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: 100.
Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: 100.
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Ja
Het getal is gedeeld, en daar is deze uitkomst uitgekomen: 11.1111111111.
Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: 100.
Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: 100.
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Ja
Ik begreep het verkeerd , de tekst die ik gaf zag ik onderaan je post vandaar dat ik dacht dat dat goed ging. Speed reading is niet altijd goed zo te zien. ;)
edit:
Foutje, ik las niet goed.
Ik heb het getest en inderdaad, hij geeft dat die 100 wordt, maar zegt wel dat die groter is dan 100, dat zou niet mogen. Er zijn 2 opties, of 99.9999 (kleiner dan 100) of precies gelijk aan 100.
Wat ik wel zei en wat blijkt te kloppen is dat die alleen de output afrond, en niet tussendoor.
Gewijzigd op 01/01/1970 01:00:00 door Robert Deiman
Verder klopt de berekening wel, want als je 10 deelt door 5 krijg je 2, als je dat weer keer 5 doet heb je weer 10.
Edit:
Jullie waren mij allebei voor.
@Karl: Correct:D En bedankt voor je test.
@Robert: Gedeeltelijk mee eens:
Als je uitgaat van een oneindig aantal 1-en zul je ook een oneindig aantal 9ens krijgen, en kijkend naar de bewijstukken van Karl is dat 100. Dat php niet afrond zou je gelijk in kunnen hebben, maar aan de andere kant: Waarom geeft hij dan niet 99.999... met zijn aantal decimalen terug in plaats van 100?
Maar dat maakt alsnog niet uit, want zoals ik al zij: Mijn vergelijking was dat het getal groter was dan 100 en niet kleiner. Het maakt dus niet uit of het 99.9999... is of 100. In dat geval zou ik altijd een boolean 0 terug krijgen.
Gewijzigd op 01/01/1970 01:00:00 door Lasse
Het heeft waarschijnlijk wel iets met de decimalen te maken. Als je round gebruikt (maakt niet uit hoeveel decimalen) gaat het wel goed.
Edit:
Bedankt voor jullie reacties. Ik zal het morgen rapporteren. Mocht er nog iemand kunnen bewijzen dat het geen bug is, voel je geroepen...
Gewijzigd op 01/01/1970 01:00:00 door Lasse
bccomp probeer met wat voor scale dan ook, ik krijg 0 terug, wat betekend:
:-)
Idd bug dus. Kijk wel effe of d'r al niet iets staat. Graag hoor ik er iig nog meer over :-).
Edit:
Waarschijnlijk toch ergens over een decimaal, want als ik:
doe, krijg ik nee...
Het klopt iig niet. Ik weet niet waar php over struikeld, maar als ik Quote:
Returns 0 if the two operands are equal
:-)
Idd bug dus. Kijk wel effe of d'r al niet iets staat. Graag hoor ik er iig nog meer over :-).
Edit:
Waarschijnlijk toch ergens over een decimaal, want als ik:
doe, krijg ik nee...
@Karl: Ja het is heel raar. Ik zal dit topic weer omhoog halen als ik meer weet.
Na wat in de bug database te hebben gezocht, en wat onderzoek te hebben gedaan blijkt dit geen bug te zijn.
Dit staat in de php manual onder Floating point numbers:
Quote:
De reden hiervan is dus dat floats in php (en alle andere scripting/programeertalen) in het binaire IEEE 745 worden opgeslagen. Dit wil zeggen dat de afrondingsmanieren anders zijn, en bovendien sommige getallen die decimaal een einde hebben, binair oneindig zijn.Warning
Floating point precision
It is typical that simple decimal fractions like 0.1 or 0.7 cannot be converted into their internal binary counterparts without a small loss of precision. This can lead to confusing results: for example, floor((0.1+0.7)*10) will usually return 7 instead of the expected 8, since the internal representation will be something like 7.9.
This is due to the fact that it is impossible to express some fractions in decimal notation with a finite number of digits. For instance, 1/3 in decimal form becomes 0.3.
So never trust floating number results to the last digit, and never compare floating point numbers for equality. If higher precision is necessary, the arbitrary precision math functions and gmp functions are available.
Floating point precision
It is typical that simple decimal fractions like 0.1 or 0.7 cannot be converted into their internal binary counterparts without a small loss of precision. This can lead to confusing results: for example, floor((0.1+0.7)*10) will usually return 7 instead of the expected 8, since the internal representation will be something like 7.9.
This is due to the fact that it is impossible to express some fractions in decimal notation with a finite number of digits. For instance, 1/3 in decimal form becomes 0.3.
So never trust floating number results to the last digit, and never compare floating point numbers for equality. If higher precision is necessary, the arbitrary precision math functions and gmp functions are available.
De comments op de php manual link die ik gaf zijn trouwens ook interessant leesvoer.
Bewijs dat dit klopt: Voer dit script uit:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
$number = 9;
echo 'Het getal = ' . $number . '. Het getal 100 wordt nu gedeeld door $number<br />';
$numberDivided = 100 / 9;
echo 'Het getal is gedeeld, en daar is deze uitkomst uitgekomen: ' . $numberDivided . '.<br />';
$numberProductByStar = $numberDivided * 9;
echo 'Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: ';
printf("%.40f", $numberProductByStar);
echo '.<br />';
$numberProductByKruis = 0;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
echo 'Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: ';
printf("%.40f", $numberProductByKruis);
echo '.<br />';
echo 'Kijk of $numberProductByStar groter is dan 100: ';
echo $numberProductByStar > 100 ? 'Ja' : 'Nee';
echo '.<br />';
echo 'Kijk of $numberProductByKruis groter is dan 100: ';
echo $numberProductByKruis > 100 ? 'Ja' : 'Nee';
?>
$number = 9;
echo 'Het getal = ' . $number . '. Het getal 100 wordt nu gedeeld door $number<br />';
$numberDivided = 100 / 9;
echo 'Het getal is gedeeld, en daar is deze uitkomst uitgekomen: ' . $numberDivided . '.<br />';
$numberProductByStar = $numberDivided * 9;
echo 'Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: ';
printf("%.40f", $numberProductByStar);
echo '.<br />';
$numberProductByKruis = 0;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
$numberProductByKruis += $numberDivided;
echo 'Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: ';
printf("%.40f", $numberProductByKruis);
echo '.<br />';
echo 'Kijk of $numberProductByStar groter is dan 100: ';
echo $numberProductByStar > 100 ? 'Ja' : 'Nee';
echo '.<br />';
echo 'Kijk of $numberProductByKruis groter is dan 100: ';
echo $numberProductByKruis > 100 ? 'Ja' : 'Nee';
?>
Code (php)
1
2
3
4
5
6
2
3
4
5
6
Het getal = 9. Het getal 100 wordt nu gedeeld door $number
Het getal is gedeeld, en daar is deze uitkomst uitgekomen: 11.111111111111.
Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: 100.0000000000000000000000000000000000000000.
Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: 100.0000000000000142108547152020037174224854.
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Ja
Het getal is gedeeld, en daar is deze uitkomst uitgekomen: 11.111111111111.
Het gedeelde getal is nu weer keer $number gedaan d.m.v. de *-operator. Uitkomst: 100.0000000000000000000000000000000000000000.
Het gedeelde getal is nu weer keer $number gedaan door er 9 keer $numberDivided bij op te tellen. Uitkomst: 100.0000000000000142108547152020037174224854.
Kijk of $numberProductByStar groter is dan 100: Nee.
Kijk of $numberProductByKruis groter is dan 100: Ja
Edit:
En nog deze Decimaal float naar Binair float omzetter, om het zelf te proberen
Niet Bumpen::
Gewijzigd op 01/01/1970 01:00:00 door Lasse