2013-09-05 26 views
41

Kod:Dlaczego drukowana jest ostatnia cyfra (1)?

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

Wyjście:

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 

Utworzony fiddle

Jeden więcej: jeśli ustawisz $start = 1 i $stop = 2 to działa dobrze.

Zastosowanie: php 5.3.27

Dlaczego 1 drukowane?

+1

zaskakująco, kiedy dzielimy przedział na 20: $ kroku = ($ przystanek - start $)/20; jest również ok. – user4035

+3

Prawdopodobnie zaokrąglony błąd zmiennoprzecinkowy. – Blazemonger

+1

Związane z http://stackoverflow.com/questions/3726721/php-math-precision – j08691

Odpowiedz

53

Ponieważ nie tylko matematyka pływaka jest wadliwa, czasami jest reprezentowana jako is flawed too - i tak jest w tym przypadku.

rzeczywistości nie dostać 0,1, 0,2, ... - i to dość łatwe do sprawdzenia:

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

Jedyna różnica tutaj, jak widać, jest to, że echo zastąpione number_format rozmowy. Ale wyniki są drastycznie różne:

0.10000000000000000555111512312578 
0.20000000000000001110223024625157 
0.30000000000000004440892098500626 
0.40000000000000002220446049250313 
0.50000000000000000000000000000000 
0.59999999999999997779553950749687 
0.69999999999999995559107901499374 
0.79999999999999993338661852249061 
0.89999999999999991118215802998748 
0.99999999999999988897769753748435 

Zobacz? Tylko raz to było 0.5 w rzeczywistości - ponieważ ta liczba może być przechowywana w pojemniku pływaka. Wszystkie pozostałe były tylko przybliżeniami.

Jak rozwiązać ten problem? Cóż, jednym radykalnym podejściem jest używanie nie pływaków, ale liczb całkowitych w podobnych sytuacjach. Łatwo zauważyć, że zrobiliście to w ten sposób ...

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

... to działa ok:

Alternatywnie, można użyć number_format przekonwertować pływaka do jakiegoś napisu, a następnie porównać to string z preformatowanym floatem. W ten sposób:

$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; 
} 
+1

Jaki jest dobry sposób na ochronę przed tym zdarzeniem? –

+4

Jednym z rozwiązań byłoby zachowanie '$ kroku' jako liczby całkowitej:' $ start = 0; $ stop = 10; $ step = 1; 'i wykonaj podział wewnątrz pętli. – Blazemonger

+0

@RichBradshaw Najlepszym sposobem ochrony byłoby przełączenie z arytmetyki zmiennoprzecinkowej na liczby całkowite poprzez pomnożenie jej przez 10 i zwiększenie o 1 zamiast 0.1 – user4035

13

Problem polega na tym, że liczba w zmiennej $ i nie jest równa 1 (po wydrukowaniu). Jego rzeczywisty wynik jest mniejszy niż 1. Tak więc w teście ($ i < $ stop) jest prawdą, liczba jest konwertowana na liczbę dziesiętną (powoduje zaokrąglenie do 1) i jest wyświetlana.

Dlaczego teraz $ 1 nie ma dokładnie 1? To dlatego, że dostałeś się tam mówiąc 10 * 0,1, a 0.1 nie może być doskonale reprezentowany w systemie binarnym. Tylko liczby, które można wyrazić jako sumę skończonej liczby potęg 2, można doskonale przedstawić.

Dlaczego więc $ stop wynosi dokładnie 1? Ponieważ nie jest to format zmiennoprzecinkowy. Innymi słowy, jest on dokładny od początku - nie jest obliczany w systemie wykorzystującym zmiennoprzecinkowy 10 * 0,1.

Matematycznie możemy napisać to w następujący sposób: enter image description here

A 64 bit pływak binarny może posiadać tylko pierwszych 27 niezerowe warunki sumy, która przybliża 0,1. Pozostałe 26 bitów znaczeń pozostaje zerem, aby wskazać warunki zerowe. Powodem, dla którego 0.1 nie jest fizycznie reprezentowalny, jest to, że wymagana sekwencja terminów jest nieskończona. Z drugiej strony liczby takie jak 1 wymagają tylko niewielkiej skończonej liczby terminów i są reprezentowalne.Chcielibyśmy, żeby tak było w przypadku wszystkich liczb. To dlatego dziesiętna zmiennoprzecinkowa jest tak ważną innowacją (jeszcze nie jest szeroko dostępna). Może reprezentować dowolną liczbę, którą możemy zapisać i zrobić to doskonale. Oczywiście liczba dostępnych cyfr pozostaje skończona.

Powracając do zadanego problemu, ponieważ 0,1 jest przyrostem dla zmiennej pętli i nie jest faktycznie reprezentowalny, wartość 1,0 (chociaż reprezentowalna) nigdy nie jest precyzyjnie osiągnięta w pętli.

2

Jeśli krokiem będzie zawsze czynnik 10, można tego dokonać szybko z następujących czynności:

<?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; 
} 
?> 
Powiązane problemy