Disassembler

Artificial intelligence is no match for natural stupidity.
01února2012

Polovičaté zaokrouhlování v PHP


Doprogramoval jsem ke stránkování možnost zvolit si číslo stránky. Zatím nemám článků tolik, aby se v přehledu všechny nedaly proklikat za chvilku, ale pomalu přibývají a pokud mě někde nesrazí vlak nebo nezlynčují Apple fanboyové, přibývat budou i nadále. Při psaní třídy, která se o stránkování stará, jsem zjistil, že PHP, při zaokrouhlování s použitím třetího parametru $mode a přesnosti vyšší než 0, zaokrouhluje nesprávně, resp. parametr $mode nefunguje tak, jak by asi měl.

Stránkování


Můj stránkovač funguje tak, že při vysokém počtu stránek nezobrazí všechny, ale začne dělat mezery, čím dále od právě zobrazované stránky, tím větší. Nejprve v řádu desítek, pak v řádu stovek a nakonec vynechá vše až do první nebo poslední stránky. Zobrazení 758 stránky z deseti tisíc, při „rozestupu“ 3 by tedy vypadalo následovně

1 ... 500 ... 600 ... 700 ... 730 ... 740 ... 750 ... 755 756 757 [758] 759 760 761 ... 770 ... 780 ... 790 ... 800 ... 900 ... 1000 ... 10000

Což, při zvolení vhodného fontu, vyjde akorát na patičku stránky a zároveň zachová smysluplné a pohodlné stránkování i při vysokém počtu stran.

Dříve jsem takové stránkování řešil poněkud neefektivně, jedním for cyklem, kterým jsem prohnal všechny čísla stran a v těle cyklu pomocí několika podmínek kontroloval, zda se ta či ona stránka má zobrazit nebo vynechat. Takže na deset tisíc stran bylo deset tisíc průchodů a několikrát více vnořených podmínek. Výpočetní výkon je sice levný, ale jak píšu v článku o xdebugu, prasečina zůstane prasečinou nehledě na to, jak rychle se provede. Optimalizovaný kód, který používám teď, je sice o pár řádek delší, ale na deseti, nebo klidně i sto tisících stránkách provede pouze tolik iterací, kolik stránek se má zobrazit. Tedy při „rozestupu“ 3 bude provedeno 21 iterací, což je přeci jen o trochu méně než deset tisíc. Klíčem k této optimalizaci však bylo správné zaokrouhlování.

Round mode


PHP funkce round() bere tři parametry. Prvním je samotné zaokrouhlované číslo, druhým je „přesnost“, tzn. Na kolik desetinných míst se zaokrouhluje. Toto číslo může být i záporné a v takovém případě jeho absolutní hodnota určuje počet míst před desetinnou čárkou. A třetím parametrem jé „mód“, který může nabývat hodnot

PHP_ROUND_HALF_UP
PHP_ROUND_HALF_DOWN
PHP_ROUND_HALF_EVEN
PHP_ROUND_HALF_ODD

A určuje, jak se má číslo před zaokrouhlením upravit, aby vyšlo v požadované podobě. V mém případě mě zajímaly módy PHP_ROUND_HALF_UP a PHP_ROUND_HALF_DOWN, tedy „půl nahoru“ a „půl dolů“. Co tyto módy dělají, nejlépe ilustruje tento příklad

echo round(3.2, 0);                      // 3
echo round(3.2, 0, PHP_ROUND_HALF_UP);   // 4
echo round(3.2, 0, PHP_ROUND_HALF_DOWN); // 3

echo round(4.8, 0);                      // 5
echo round(4.8, 0, PHP_ROUND_HALF_UP);   // 5
echo round(4.8, 0, PHP_ROUND_HALF_DOWN); // 4

Prostým odečtením nebo přičtením 0,5 k zaokrouhlovanému číslu řekneme, zda chceme zaokrouhlit nahoru nebo dolů. Vytvoříme tím tedy ekvivalenty funkcí ceil() a floor().

Problém však nastane, když potřebujete zaokrouhlovat s jinou přesností než 0, protože PHP_ROUND_HALF_UP i PHP_ROUND_HALF_DOWN prostě a jednoduše otrocky přičítá nebo odčítá 0,5, místo aby přičetl polovinu mocniny přesnosti.

echo round(456, -1);                      // 460
echo round(456, -1, PHP_ROUND_HALF_UP);   // 460
echo round(456, -1, PHP_ROUND_HALF_DOWN); // 460

echo round(142, -1);                      // 140
echo round(142, -1, PHP_ROUND_HALF_UP);   // 140
echo round(142, -1, PHP_ROUND_HALF_DOWN); // 140

Vlastní ceil() a floor()


Nezbylo mi tedy nic jiného, než si round() se správně fungujícím half_up a half_down napsat.

function round_up($value, $precision) { 
     $value = $value+(0.5*pow(10, -$precision)); 
     return round($value, $precision); 
}

function round_down($value, $precision) { 
     $value = $value-(0.5*pow(10, -$precision)); 
     return round($value, $precision); 
}

A voila

echo round(456, -1);      // 460
echo round_up(456, -1);   // 460
echo round_down(456, -1); // 450

echo round(142, -1);      // 140
echo round_up(142, -1);   // 150
echo round_down(142, -1); // 140