[PHP] Bug in PHP met vergelijking > of < ?

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • pierre-oord
  • Registratie: April 2002
  • Laatst online: 10-02 23:00
Bij een vergelijking voor een balans krijg ik een uitkomst die helemaal niet klopt. Ik zal wat code laten zien:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Check if the user has enough money on his balance.
$amount = $amount * -1; // amount even positief maken
$query_balance = mysql_query("SELECT * FROM `user_balance` WHERE `userid`='".$users_info['id']."'") or die(mysql_error());
$balance = 0;
while ($disp_balance = mysql_fetch_array($query_balance)){
    $balance = $balance + $disp_balance['change'];
}
#$balance = 1.2;
#$balance = $balance * 1;
echo 'balance is: '.gettype($balance).' and is '.$balance.'<br>';
echo 'amount is: '.gettype($amount).' and is '.$amount.'<br>';
#echo 'after making balance static it is:<br>';
#$balance = 1.2;
#echo 'balance is: '.gettype($balance).' and is '.$balance.'<br>';

if ($amount > $balance){
    $error .= '>Er staat niet genoeg geld op uw balans. Kies voor "Mijn balans" in het menu en voeg door te bellen geld toe aan uw balans. (U heeft nodig: '.$amount.' Euro en op uw balans staat '.$balance.' Euro)';
}


Ik krijg als output:
balance is: double and is 1.2
amount is: double and is 1.2

Er was een probleem bij het toevoegen van uw account:
>Er staat niet genoeg geld op uw balans. Kies voor "Mijn balans" in het menu en voeg door te bellen geld toe aan uw balans. (U heeft nodig: 1.2 Euro en op uw balans staat 1.2 Euro)
Maar dat klopt niet, want het is beide 1.2! En ik gebruik niet een >= maar een > vergelijking!

Echter, als ik nu deze comments weghaal:
#echo 'after making balance static it is:<br>';
#$balance = 1.2;
#echo 'balance is: '.gettype($balance).' and is '.$balance.'<br>';

Dan krijg ik:
balance is: double and is 1.2
amount is: double and is 1.2
after making balance static it is:
balance is: double and is 1.2
Succesvol toegevoegd.

(Dat succesvol krijg je als $error leeg blijft).

Zoals je ziet er helemaal _niets_ veranderd aan de $balance! Het is 1.2, net zoals nadat ik hem dat handmatig nog even vertel! Ik heb ook al eens $balance = $balacen * 1; gedaan, maar dan werkt het ook nog steeds niet.
Jah, mijn manier van de balans opsommen kan ik ook door MySQL laten doen, maar deze quick'n'dirty methode moet toch ook gewoon werken!

Ik heb dit ook nog geprobeerd:
PHP:
1
2
3
4
5
6
7
8
9
10
11
<?
$balance = '1.2';
$amount = 1.2;

if ($amount > $balance){
    echo 'te weinig op balans';
} else {
    echo 'ok';
}
// geeft dus wel ok terug???
?>

$balance dus eerst even als een string. Dat werkt prima?! In mijn script staan verder geen vreemde dingen, $amount wordt gewoon gezet met deze berekening (dat vind ik makkelijk als ik later iets moet aanpassen):
$amount = round((-1.30 + 0.102),2);

En als ik dat in dat bovenstaande script invul, geeft ook dat script nog steeds "ok" terug. Ik draai PHP versie 5.0.5.

Zie ik iets over het hoofd? Ik zit nu een uur al werkelijk _alles_ te proberen, maar het blijft deze fout geven, en ik zie gewoon _niet_ wat ik fout doe, waardoor ik bang ben dat dit een PHP bug is? Bedankt voor het kijken!

Acties:
  • 0 Henk 'm!

  • pierre-oord
  • Registratie: April 2002
  • Laatst online: 10-02 23:00
Fout gevonden! Ik heb ergens overheen gelezen op de PHP site wat ik ook nóóit had verwacht:
It is quite usual that simple decimal fractions like 0.1 or 0.7 cannot be converted into their internal binary counterparts without a little 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 as the result of the internal representation really being something like 7.9999999999....

This is related to the fact that it is impossible to exactly express some fractions in decimal notation with a finite number of digits. For instance, 1/3 in decimal form becomes 0.3333333. . ..

So never trust floating number results to the last digit and never compare floating point numbers for equality. If you really need higher precision, you should use the arbitrary precision math functions or gmp functions instead.
Ik heb nu dus $balance = round($balance,2); toegevoegd, dit werkt.
Ik vind het zeer verwarrend dat PHP dan niet bij de echo "iets als 1,9999999999999"weergeeft, want zo blijf je zoeken natuurlijk :/

Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 23:05
Sorry, maar eigenlijk is je huidige oplossing gewoon fout. Voor getallen die exact gerepresenteerd moeten worden, moet je fixed-point ('decimal' in veel databases) getallen gebruiken. Bij gebrek daaraan kun je in PHP het bedrag representeren met een integer die bijvoorbeeld het aantal centen voorstelt.

Elke serieuze applicatie die met 'geld' werkt zal dan ook fixed point arithmetic gebruiken. Je huidige hack met round() werkt misschien in jouw specifieke situatie, maar in andere situaties zullen er allemaal soortgelijke problemen optreden. Je kunt het probleem dus het beste bij de bron oplossen door direct op een fixed-point representatie over te schakelen.

Trouwens, de gebruikelijke manier om floating point getallen van een bekende orde-van-grootte te vergelijken is door een kleine constante marge te kiezen waarbinnen je twee getallen als gelijk ziet. Dan krijg je dus een vergelijking als:
PHP:
1
2
3
4
5
6
define(ETA, 0.00001);
if(abs($a - $b) < ETA) {
  // Gelijk!
} else {
  // Verschillend
}

[ Voor 26% gewijzigd door Soultaker op 28-01-2006 21:23 ]


Acties:
  • 0 Henk 'm!

  • alx
  • Registratie: Maart 2002
  • Niet online

alx

Soultaker schreef op zaterdag 28 januari 2006 @ 21:16:
[...]
Trouwens, de gebruikelijke manier om floating point getallen van een bekende orde-van-grootte te vergelijken is door een kleine constante marge te kiezen waarbinnen je twee getallen als gelijk ziet.
[...]
Daar ben ik ook al een tijd van op de hoogte, maar wat er vaak niet bij wordt verteld is hoe je dan die ETA of epsilon bepaalt. Voor geld lijkt me 0.00001 wel voldoende, maar hoe bepaal je die constante in het algemeen? Wat als de applicatie wijzigt? Dit lijkt me belangrijk, want een te grote constante kan de kans ontoelaatbaar vergroten dat iets als gelijk wordt opgevat, terwijl dat niet had gemoeten. Het testen van alle gevallen lijkt me vaak ondoenlijk en corner cases zijn niet zo eenvoudig te bepalen, denk ik. Ik zou denken dat je moet kijken naar domein specifieke eisen en de garanties van je fp hw, maar ik heb het idee dat het daar niet eenvoudig van wordt... Nou zijn de details van fp dat ook niet...

Acties:
  • 0 Henk 'm!

  • NMe
  • Registratie: Februari 2004
  • Laatst online: 09-09 13:58

NMe

Quia Ego Sic Dico.

simulacrum schreef op zaterdag 28 januari 2006 @ 21:39:
Daar ben ik ook al een tijd van op de hoogte, maar wat er vaak niet bij wordt verteld is hoe je dan die ETA of epsilon bepaalt. Voor geld lijkt me 0.00001 wel voldoende, maar hoe bepaal je die constante in het algemeen? Wat als de applicatie wijzigt?
Daarom gebruikt Soultaker zonder twijfel ook een define, die je lekker makkelijk kan aanpassen. ;)
Dit lijkt me belangrijk, want een te grote constante kan de kans ontoelaatbaar vergroten dat iets als gelijk wordt opgevat, terwijl dat niet had gemoeten.
En daarom is het de tweede optie die hij noemt, na het vermelden van een veel betere oplossing met fixed point representatie. :)

'E's fighting in there!' he stuttered, grabbing the captain's arm.
'All by himself?' said the captain.
'No, with everyone!' shouted Nobby, hopping from one foot to the other.


Acties:
  • 0 Henk 'm!

Verwijderd

Kleine opmerking nog :

PHP:
1
$amount = $amount * -1; // amount even positief maken 


Dit is ook alles behalve een waterdichte manier om een waarde positief te maken...

Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 23:05
De gebruikelijke term is inderdaar epsilon; ik zei onterecht eta. (Het maakt voor het punt niet uit, maar toch is het natuurlijk handiger om de gebruikelijke term te hanteren)
simulacrum schreef op zaterdag 28 januari 2006 @ 21:39:
Daar ben ik ook al een tijd van op de hoogte, maar wat er vaak niet bij wordt verteld is hoe je dan die ETA of epsilon bepaalt. Voor geld lijkt me 0.00001 wel voldoende, maar hoe bepaal je die constante in het algemeen?
Zoals -NMe- ook al herhaalde is voor geld een floating point type eigenlijk gewoon niet geschikt, maar in het algemene geval gebruik je geen constante epsilon. Je vergelijkt de getallen dan relatief, zo bijvoorbeeld: abs((A-B)/A) < E (pas op, werkt niet als A=0!) waarbij E een relatieve fout is.

Dat is logisch, want een constante epsilon werkt heel slecht als de getallen in allerlei ordes van groottes kunnen zitten. Bijvoorbeeld 0.0000001 en 0.0000002 zijn volgens de eerdergenoemde methode altijd gelijk, terwijl dat niet de bedoeling is, en 1000000001 en 1000000002 zijn juist verschillend.

Welke waarde je voor de relatieve fout kiest hangt af van twee dingen: het aantal en de soort opeaties die je uitvoert en het gebruikte datatype. Als je meer bewerkingen uitvoert op getallen dan wordt de rekenfout vanzelf steeds groter. Verder zijn verschillende floating point formaten (i.h.a. single, double en extended precision) die allemaal een ander aantal significante cijfers hebben; een single precision float heeft bijvoorbeeld een 23 bits mantissa, dus de relatieve fout zal op z'n minst 2-23 moeten zijn (maar liever een stuk groter).

Acties:
  • 0 Henk 'm!

  • alx
  • Registratie: Maart 2002
  • Niet online

alx

Bedankt voor de antwoorden, maar het is me nog niet helemaal helder hoe je gegeven een willekeurig fp vergelijkingsprobleem, een goede epsilonwaarde bepaalt. (De rest was me wel al vrij duidelijk; het laat me weer eens zien dat ik nog beter een vraag moet formuleren :) )

Dus maar even een rondje google gedaan. Een aardige is
Comparing floating point numbers
Het probleem verplaatst soms van een te kiezen epsilon naar een te kiezen max aantal representaties die de waarden mogen verschillen. Maar hoe die nou te bepalen gegeven datatype en uitgevoerde berekeningen voor de vergelijking wordt aan voorbij gegaan. Wellicht te domein specifiek. Het zal toch uit een foutanalyse moeten komen, denk ik. Gezien het gedrag van fp berekeningen, lijkt me dat een klus als je het een beetje goed wilt doen. En als je berekening voorafgaande aan de vergelijking verandert, kan je weer aan de gang...

Ik had eigenlijk gehoopt dat er een soort alg of prog was dat een goede epsilon/max_#repres_ernaast kan bepalen of daarin kan assisteren. Gehoopt, niet omdat ik het nodig heb, maar omdat het handig lijkt gezien de complexiteit van fp. Ik zal op een ander tijdstip eens beter zoeken :z

Acties:
  • 0 Henk 'm!

  • ATS
  • Registratie: September 2001
  • Laatst online: 18-09 15:14

ATS

simulacrum schreef op zondag 29 januari 2006 @ 02:03:
Bedankt voor de antwoorden, maar het is me nog niet helemaal helder hoe je gegeven een willekeurig fp vergelijkingsprobleem, een goede epsilonwaarde bepaalt.
Je zou gebruik kunnen maken van het relatieve verschil tussen de twee waarden, en daar een grens voor nemen:

gelijk = ( (waarde1-waarde2)/(waarde1+waarde2) < grenswaarde )

Die grenswaarde is op zich wel redelijk te bepalen, omdat je weet hoe nauwkeurig floats zijn. Ga daar een beetje boven zitten, en je zit vrij aardig denk ik. Gewoon je grenswaarde op 1/100000 ofzo stellen zal ook wel werken. Alles is natuurlijk afhankelijk van je toepassing, maar als je echte precisie nodig hebt dan moet je gewoon geen floating point getallen gebruiken.

My opinions may have changed, but not the fact that I am right. -- Ashleigh Brilliant


Acties:
  • 0 Henk 'm!

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 13-09 00:05
In theorie allemaal waar, maar de TS heeft natuurlijk helemaal geen floating point nodig. Als je in euro's werkt is de regel simpel: tel in miljoenste euro's in berekingen, en rond af op centen in het resultaat.De bron is de ECB, maar een URL heb ik even niet.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein

Pagina: 1