Lista megjelenítése táblázattal php-ben (oszloponként)

Írtam már hasonló témáról, de ott feltételeztem, hogy a lista elemeit sorban szeretnénk kiírni és majd egy adott oszlopszám után sort törni. De felmerülhet olyan igény, hogy az elemeket egymás alatt, egy oszlopban akarjuk megjeleníteni, ám ekkor is a maximális oszlopszámot szabjuk meg és nem a maximális sorszámot. Így azt külön ki kell számolni, hogy mikor kell "oszlopot törni". Ez még nem is tűnne bonyolultnak, de ne feledjük, hogy az ehhez szükséges forráskódot csak sorban lehet kiírni.

Az egyik legkézenfekvőbb megoldás a korábbi hasonló cikk alapján megoldani a feladatot. Ahol is az array_chunk -ot használtam a tömb feldarabolására. A probléma csak annyi, hogy az oszlopszám adott csak és nem a szükséges sorszám. Márpedig jelen esetben épp arra lenne szükség, hogy az egyes oszlopok elemeit kapjuk egy-egy tömbben. Mi sem egyszerűbb ennél:

$row_num = ceil($c / $col_num);

Ahol a $c a tömb mérete, és $col_num az oszlopok száma egy sorban. Itt azért van egy apró probléma. Valóban megkapjuk, hogy hány sor szükséges, de ez még nem jelenti azt, hogy ki is fogjuk használni az összes oszlopot. Ha ugyanis a lista elemeinek száma nem pontosan osztható a megadott oszlopok számával, akkor maradnak mindenképpen üres cellák. Ha az üres cellák száma nem kevesebb, mint ahány sorra szükségünk van, akkor ennek megfelelően az utolsó oszlopok teljesen üresek lesznek. Pedig ha a listát sorban írjuk ki, akkor ezek az üres cellák az utolsó sor utolsó elemei lesznek. És az oszlopszám fix marad. Ahhoz, hogy ezt a hatást érjük el, kicsit át kell rendezni a tömböt, vagy ki kell találni egy algoritmust a megfelelő indexelésre.

Az egyszerűség kedvéért tegyük fel, hogy vannak számaink 1-től 18-ig (Ezt az egyszerű listát könnyű lesz előállítani a range() függvény segítségével. ). És ezeket kell egy 7 oszlopos táblázatban megjeleníteni a következő módon:

1 4 7 10 13 15 17
2 5 8 11 14 16 18
3 6 9 12      

Az első megoldás legyen pár beépített függvény használatával a tömb átalakítása egy olyan szerkezetté, ami könnyen bejárható. A megoldás lényege, hogy legyen egy tömb, aminek minden eleme egy oszlop. Amit szintén tömbként tárolunk. Az elemei pedig a cellák tartalmai lesznek. Az üres helyeket kitölthetjük bármivel. Én   karakterrel fogom.

Oké, tehát legyen a következő függvény, aminek első paramétere az átalakítandó tömb. A második, hogy hány oszlopot szeretnénk:

function array2columns($array, $col_num)
{
    //...
}

Na most az biztos, hogy ha az átadott tömb üres, akkor az eredmény is egy üres tömb lesz.

function array2columns($array,$col_num)
{
        $c = count($array);
        if (!$c) return array();
        //... és a többi ...
}

De itt jön csak a neheze. Nézegetve a minta táblázatot, egy valami azért feltűnhet. A táblázatnak van egy bal oldala, ahol hosszabb oszlopok vannak, és van egy jobb oldala, ahol 1-el rövidebb oszlopok vannak. Tehát egy jó ötlet lehet a tömböt felbontani két részre. Ehhez az array_splice() függvényt fogom használni. Ez első paraméterben a szétvágandó tömböt kéri, és másodikban pedig hogy az első hány elemet akarom meghagyni az első paraméterben megadott tömbben. A függvény visszatérési értéke pedig a maradék elemek tömbje. Ehhez viszont kell pár adat. Például a jobb és baloldali tömbök maximális sorszámai. Ez még egyszerű, hisz már megbeszéltük.

        $lcol_len = ceil($c / $col_num);
        $rcol_len = $lcol_len -1;

Aztán hogy hány oszlopból állnak. És ebből már kiszámítható lesz, hogy hány elemet tartalmaznak, ami felhsználható az array_splice-ben. Tehát:

        $lcol_num = $c % $col_num;
        if (!$lcol_num)
        {
                $lcol_num = $col_num;
        }
        $rcol_num = $col_num - $lcol_num;

Mert ugye azért lett több elem a baloldali táblázatban, mert az utolsó sorba kerül a "maradék". Ami nem tesz ki már egy sort. Ha nincs maradék, az viszont nem azt jelenti, hogy a baloldali táblázat üres, hanem hogy nincs szükség jobb oldalra. És a baloldal pont olyan hosszú, mint ahány oszlop összesen kell. Most pedig az array_splice -el kettévágom a tömböt. A második paramétere a baloldali tömbbe kerülő elemek száma, ami a baloldali táblázat oszlopainak száma szorozva az oszlopok hosszával (sorok száma).

        $right = array_splice($array, $lcol_num * $lcol_len);
        $left = &$array;

Csak átnevezés céljából a már megcsonkított $array tömböt referenciaként átadom egy $left nevű tömbnek. Így feleslegesen nem másolok adatokat. Csak hivatkozom egy másik adatra. Eddig a függvény tehát így néz ki:

function array2columns($array,$col_num)
{
        $c = count($array);
        if (!$c) return array();

        $lcol_len = ceil($c / $col_num);
        $rcol_len = $lcol_len -1;

        $lcol_num = $c % $col_num;
        if (!$lcol_num)
        {
                $lcol_num = $col_num;
        }
        $rcol_num = $col_num - $lcol_num;

        $right = array_splice($array, $lcol_num * $lcol_len);
        $left = &$array;
        //... és a többi ...
}

Ezen a két tömbön pedig már lehet alkalmazni az array_chunk-ot is, amiről a cikk elején beszéltem. A baloldali tömb biztos, hogy $lcol_len elemű tömböket fog tartalmazni. Csak a jobb oldalnál kell figyelni, hogy ha a bal oldal egy soros volt, akkor a jobb oldal 0 soros. De az array_chunk nem kaphat nullát. Ezt viszont egy if-el el lehet intézni. Majd még a jobb oldal oszlopait ( ha van ) ki kell pótolni egy plusz elemmel hogy a táblázat teljes legyen. Ez után már csak össze kell olvasztani a két tömböt array_merge -el és kész vagyunk.
A kész függvény tehát:

function array2columns($array,$col_num)
{
        $c = count($array);
        if (!$c) return array();

        $lcol_len = ceil($c / $col_num);
        $rcol_len = $lcol_len -1;

        $lcol_num = $c % $col_num;
        if (!$lcol_num)
        {
                $lcol_num = $col_num;
        }
        $rcol_num = $col_num - $lcol_num;

        $right = array_splice($array, $lcol_num * $lcol_len);
        $left = &$array;

        $left = array_chunk($left, $lcol_len);
        if ($rcol_len)
        {
                $right = array_chunk($right, $rcol_len);
                foreach($right as &$item)
                {
                        $item[] = " ";
                }
        }
        return array_merge($left, $right);     
}

Ezzel simán betesszük egy $result tömbbe az eredményt és akkor már csak a táblázatot kell kiírni:

$col_num = 7;
$array = range(1,18);
$result = array2columns($array,$col_num);

A listázásban már csak annyi a trükk, hogy két for ciklus segítségével sorban végig kell menni az összes oszlop első elemén, majd a másodikon és így tovább.

<table border="1">
<?php
$row_num = count($result[0]);
for($i=0; $i < $row_num; $i++)
{
        print "<tr>";
        for ($j=0; $j < $col_num; $j++)
        {
                print "<td>".$result[$j][$i]."</td>";  
        }
        print "</tr>";
}
?>
</table>

Hosszan írtam le, de annyira nem is bonyolult utólag belegondolva. Nemde?

Jöjjön egy második módszer is. Mert mindig van alternatíva. A kérdés csak az, melyik az optimálisabb az adott feladatra. Az első megoldásban bár több beépített függvényt használtam, és eredményül egy jól használható függvényt kaptam, valójában felesleges művelet a tömböt átrendezni, ha tudok rá egy olyan indexelési módszert írni, amivel gyorsabban ki tudom írni ugyanazt a táblázatot. Persze programozásnál mindig fontos az áttekinthető forráskód. Néha beáldozzuk a sebességet a szebb kódért, de ha fontos a sebesség, legyen inkább picit bonyolultabb a kiírás, de gyorsabb a megoldás. Itt ugyanúgy szükség van az első megoldásban kiszámolt adatokra a bal és jobb oldali táblázatokról. De vegyünk még fel két új változót. Nevezetesen $row és $col változókat, melyek rendre 0 ( azaz nulla ) értéket kapnak kezdőértékül. Ezekben tároljuk az aktuális sor és oszlop számát.

$c = count($array);
$row_num = ceil($c / $col_num);
$lcol_num = $c % $col_num;
if (!$lcol_num)
{
        $lcol_num = $col_num;
}
$rcol_num = $col_num - $lcol_num;
$max = $col_num * $row_num;
$col = 0;
$row = 0;

És most jön az a bizonyos bonyolultabb ciklus az indexelő algoritmussal. Abban viszont már csak a cellákat és a sorok között levő sortöréseket kell kiírni, tehát a táblázat eleje és vége már előre leírható:

<table border="1">
<tr>
<?php
for($i = 0; $i < $c; $i++)
{
   //... és a többi ...
}
?>
</tr>
</table>

És itt ki kellene írni mindig az $x-edik cellát, de mi lesz az $x? Először hagyjuk figyelmen kívül, hogy itt bizony az utolsó oszlopokat is ki kell tölteni és csak az oszlopok utolsó eleme hiányozhat. Ekkor a minta táblázatot megnézve feltűnhet, hogy a bal oldalon minden oszlop első eleme 3-al nagyobb, mint az előző oszlopban levő első elem. Általánosan az indexeket tekintve ez nem más, mint az aktuális oszlop indexe szorozva a sorok számával. Az oszlop többi indexe pedig az elsőhöz hozzáadva az aktuális sor indexe.

$x = $col*$row_num+$row;

És ez működik is egészen addig, míg el nem érjük a jobb oldali részt, ahol eggyel rövidebb oszlopokról van szó. Na most ebben az esetben is két eset van. Vagy az utolsó sor jön, vagy nem. Ha az utolsó jön, akkor az üres kell legyen. Tehát elég, ha túlindexeljük a tömböt, amire majd kiíráskor kell figyelni. Mivel $max számú elem lehet a táblázatban összesen, ez maximális indexnek pont megfelel, mert a maximális index eggyel kisebb, mint az elemek száma, ha 0-tól kezdjük az indexelést. Ha viszont nem az utolsó sor jön, akkor megint kicsit számolni kell. Azt kell megfejteni, hogy az eredeti $x indexhez képest mennyivel lesz kisebb vagy nagyobb. Jelen esetben ugye kisebb. Mégpedig minél távolabb van az oszlop a baloldali táblázattól, annál kisebb indexű elem kerül az oszlop első helyére. Egész pontosan annyival kisebb, amekkora a távolság.

        if ($col >= $lcol_num)
        {
                $x = ($row+1 == $row_num) ? $max : $x+$lcol_num-$col;
        }

Ahogy korábban említettem, a kiírásnál figyelni kell, hogy a tömböt $x változóval túlindexeltük. Olyankor egy helykitöltő karakter kerül a cellába.

print '<td>'.(isset($array[$x]) ? $array[$x] : "&nbsp;").'</td>';

Természetesen az oszlopszámot mindig növelni kell, amíg el nem érjük az utolsó oszlopot. Mert akkor újra nullázni kell. Sorszámot pedig pontosan ekkor kell növelni. És ha már növeljük a sorszámot, ki kell írni a sortöréseket is. De csak akkor, ha nem az utolsó sornál tartunk.
A 2. megoldás tehát a következő:

<?php
$c = count($array);
$row_num = ceil($c / $col_num);
$lcol_num = $c % $col_num;
if (!$lcol_num)
{
        $lcol_num = $col_num;
}
$rcol_num = $col_num - $lcol_num;
$max = $col_num * $row_num;
$col = 0;
$row = 0;
?>
<table border="1">
<tr>
<?php
for($i = 0; $i < $max; $i++)
{
        $x = $col*$row_num+$row;
        if ($col >= $lcol_num)
        {
                $x = ($row+1 == $row_num) ? $max : $x+$lcol_num-$col;
        }

        print '<td>'.(isset($array[$x]) ? $array[$x] : "&nbsp;").'</td>';
        $col++;
        if ($col >= $col_num)
        {
                $col=0;
                $row++;
                if ($row < $row_num)
                {
                        print "</tr><tr>";
                }
        }
}

?>
</tr>
</table>

A két megoldás sebességét összevetve két dolgot tapasztaltam. Az egyik, hogy a második megoldás majdnem 2-szer gyorsabb, mint az első. A másik pedig, hogy ha a második megoldás sebességét úgy mértem, hogy még az első is benne volt előtte a forrásban, akkor még ennél is gyorsabb volt. Ez arra enged következtetni, hogy bizonyos műveleteket nem végez el a program újra, mert felismeri, hogy már megtette egyszer. Nem nyomoztam ki, hogy itt melyek ezek, de például lehetne a tömb méretének vizsgálata. Az viszont gyakorlatilag bár azonos struktúrájú, mégis két különböző tömbön történik. Akinek van ebben tapasztalata, leírhatja kommentben a véleményét. Én egyelőre megelégszem a ténnyel, hogy lehet igazság az észrevételemben, mivel az tény, hogy bizonyos fordítók megpróbálják optimalizálni a kódot. Miért ne tehetné meg a PHP értelmező is?

Kategóriák: 
Megosztás/Mentés