[PHP] Błąd z porównywaniem określonych liczb

Podgląd - kod i wyniki

Tak jakby pewne wartości zmiennoprzecinkowe wymnożone przez 100 nie zgadzały się z wartością całkowitą. Z tym że ja tu nie widzę logiki. Na błąd trafiłem totalnie przypadkowo. Czy ktoś może się wypowiedzieć w tym temacie?

Komputer nie jest w stanie przechowywać dokładnych wartości liczb zmiennoprzecinkowych - mają one ograniczoną dokładność. Szerzej:

warning w manualu php

wyjaśnienie na stackoverflow

Moim zdaniem wynika to z natury liczb zmiennoprzecinkowych, 2,3 * 100 może dać np, 230,32.

Ale nie daje. Rozumiem że gdyby to były liczby 2,3000000001*100 albo inne dziwne wartości jak w linkach w drugim poście to mogło by się coś dziać. Tutaj są zwykłe wartości.

Jest wyraźnie napisane var_dump($string_amount*100); to float(27296) - czyli 27296 zapisane jako wartość zmiennoprzecinkowa, która jest równa wartości 27296 wartości stałoprzecinkowej. Co ciekawe podobna wartość o tej samej wartości po przecinku daje poprawne wskazania.

Zobaczcie na odświeżony link - dodałem wartość zmiennoprzecinkową o większej ilości zdjęć.

Przyznam że totalnie mnie to zdziwiło, ponieważ funkcja w której się spotkałem z tym problemem działa od długiego czasu, a dopiero po natrafieniu na tą wartość się wywaliła.

W skrócie - liczby wymierne, które są skończone w systemie dziesiętnym, niekoniecznie będą skończone w systemie binarnym, co powoduje nieznaczne utraty części danych.

To by było bez sensu, bo co by się działo w przypadku maszyn, które muszą robić poważniejsze obliczenia niż mnożenie cen przez 100.

Pod odświeżonym linkiem umieściłem działanie string_amount*1000000000 które zwraca wartość float(272960000000). Większej dokładności nie mogę użyć, bo wychodzi poza zakres, czyli w tym konkretnym przypadku teoretycznie wartość musi być poprawna, nie ma żadnych ułamków.

A gdyby nawet były te ułamki zapisanie w postaci binarnej (w sumie jak, skoro to maszyna 32b) to mam w jednym konkretnym elemencie ewidentny brak logiki.

(int)(string_amount*100) int(27296) - wyraźnie zwraca wartość integer, nie ważne czy string_amount*100 to 27295.99999 czy 27296.000001 , to zawsze musi zwrócić liczbę całkowitą i taka wartość musi gdzieś być zapisana w pamięci. Wg mnie w tym przypadku (int)(string_amount*100) musi być równoznaczne z jakas_zmienna = 27296.

Mógłby ktoś sprawdzić ten przypadek z tą wartością (lub innymi) w innych językach programowania niż PHP?

Tutaj jeszcze jedna stronka opisująca problem: http://floating-point-gui.de/basic/ Te drobne niedokładności powodują takie własnie problemy. 272.97*100 = 27297.000000000004

272.96*100 = 27295.999999999996

A rzutowanie do inta po prostu obcina część po przecinku, stąd 27295!=27296.

A co gdy mimo wszystko potrzebujemy tej dokładności? Można użyć specjalnego typu danych, np. w Javie BigDecimal, w PHP się nie orientuję za bardzo, oprócz tego, że istnieją specjalne funkcje do operacji na liczbach

btw. w każdym języku będzie tak samo. Tj. zakładając ten sam typ co w PHP - double (tak, sprawdzałem w Javie :P).

Raczej 27296!=27296 jest true - wg tych wyliczeń co napisałem. Wygląda na to, że var_dump() zwraca to co oczy chcą widzieć, ale maszynowo wyniki działań to są bzdury;-) Niestety teorię nabytą w szkole i moją logikę szlag trafił;-)

Ale raczej faktycznie coś musi w tym być.

MySQL

SELECT '272.96' *100 AS `string` , 272.96 *100 AS `int` , ROUND( 272.96 ) AS `round`

Wyniki

string=27295.999999999996

int=27296.00

round=27296

jak wyrzucasz liczbę zmiennoprzecinkową na ekran to odpalasz magicznego __toString();

zawsze to co jest na ekranie jest rzutowane na stringa

btw. przypadkowo odkryłem kiedyś ten sam problem w JavaScript

Dodane 15.07.2013 (Pn) 21:17

Właśnie badałem ten temat na XAMPIE w WIndows 7 i za każdym razem robi mi się blue screen :twisted: (o dziwo pierwszy raz w życiu widzę blue screen w windows 7)

Jeszcze raz powtórzę:

(double) 272.96*100 != 27296

(double) 272.96*100 = 27295.999999999996

PHP domyślnie zaokrągla liczbę przy echo (à propos tego, że PHP ogłupia). number_format(272.96*100,12) pokaże Ci prawdziwą wartość tego wyrażenia. A skoro rzutowanie do inta obcina część po przecinku porównujesz liczby 27295 i 27296

:arrow: http://ideone.com/f4Wk83

Przykład z wypisaniem surowej zawartości zmiennych.

Wyraźnie widać, że 272.96 * 100 != 27296.

Liczby zmiennoprzecinkowe są przechowywane z ograniczoną dokładnością, podaczas kolejnych operacji kumulują się kolejne błędy zaokrągleń (czasem większe, czasem mniejsze).

Nigdy nie należy porównywać liczb zmiennoprzecinkowych uzyskanych z obliczeń, jeśli jest taka potrzeba należy sprawdzić czy ich bezwzględna różnica jest mniejsza od zadanego marginesu.

Jak to zrobić?

Jest napisane tutaj: PHP: Floating point numbers - Manual - Comparing floats

#include

W PHP używasz funkcji round( LICZBA_ZMIENNOPRZECINKOWA , OPCJONALNIE_ILOŚĆ_MIEJSC_PO_PRZECINKU );

W MySQL ROUND( LICZBA_ZMIENNOPRZECINKOWA * 10^ILOSC_MIEJSC_PO_PRZECINKU ) / ILOSC_MIEJSC_PO_PRZECINKU - choć tutaj robiąć działanie np. ROUND( 12345.6789 * 100 ) / 100 otrzyma się wynik 12345.6700. Wynik w MySQL to też w sumie osobny temat;-)

W JS i chyba C++ podobnie jak w MySQL, a w Pascal, C# i VB chyba podobnie jak w PHP - niestety nie pamiętam.