Perl (12) - Pole - základní operace

Perl Naučme se trochu orientovat v seznamovém kontextu a používat některé základní nástroje , mezi které patří třídění polí nebo operátor rozsahu.

1.8.2005 06:00 | Jiří Václavík | přečteno 57425×

Seznam ve skalárním kontextu

Podobně jako mezi řetězci a čísly existují pravidla pro konverzi mezi seznamy a skaláry.

Různé funkce a operátory se chovají ke skalárům a seznamům jinak. Pracujeme-li se seznamem jako se skalární hodnotou (to lze, za chvíli přijde vysvětlení), Perl si s tím šalamounsky poradí. Takovýto seznam ve skalárním kontextu se tváří jako proměnná, jejíž hodnotou je počet prvků seznamu. Možná je to na první pohled neintuitivní řešení, ale této vlastnosti se skutečně často využívá. Na těchto řádcích kódu si ukažme, jak přesně to funguje:

@pole = qw(a b c d e f);
$skalar = @pole;
print $skalar;

Sledujme především druhý řádek. Protože je levý operand skalár, musí do něj být uložena skalární hodnota. Pravý operand tedy bude vyhodnocován také jako skalár, ať je čímkoliv. V našem případě se do proměnné $skalar přiřadí hodnota 6, což je počet prvků @pole.

Avšak pozor na následující příklad:

$skalar = (10, 20, 30); # POZOR!!!
print $skalar;

U seznamů, které nejsou poli, je to však jinak, než by se dalo podle dosavadních tvrzení čekat. Do $skalar je přiřazena poslední hodnota seznamu, tedy 30. Přitom, kdybychom přiřazovali do pole, získali bychom tříprvkové pole. Proč tomu tak je? Závorky zde Perl chápe jako operátor pro změnu priority a čárky jako klasický operátor čárky, který levý výraz vyhodnocuje a zapomíná. Malý test - co kdybychom neuvedli závorky?

Perl obsahuje speciální funkci, která vyjadřuje seznam ve skalárním kontextu. Funkce scalar převádí hodnotu v parametru na skalární. Je-li skalární, nic se neděje. Ale jde-li o pole, funkce vrátí počet jeho prvků.

@pole = qw(a b c d e f);
print scalar @pole; # vytisknuto je opět 6.

Teď si vezměme opačný případ. Přiřazení skalární proměnné do seznamu. Přiřazením vznikne jednoprvkové pole, jehož nultou hodnotou je hodnota na pravé straně přiřazení. Skalární hodnota se konvertuje v jednoprvkový seznam.

$skalar = 666;
@pole = $skalar; # index 0: hodnota 666

Funkce print s parametrem v seznamovém kontextu

Předáme-li funkci print pole, vytiskne hodnoty všech prvků pole.

@pole = qw(a b c d e f);
print @pole; # vytiskne abcdef.

Nemusíme předávat už vytvořené pole, ale třeba jen seznam hodnot, oddělených čárkami

print "a", "b", "c"; # vytiskne abc.

Samozřejmě i v print můžeme použít uzavření do uvozovek pomocí qw:

print qw(a b c); # vytiskne abc.

Speciální proměnné pro pole

Perl obsahuje desítky speciálních proměnných, jež mají různý význam. My si nyní představíme tři z nich, se kterými se občas setkáme při práci s poli.

ProměnnáVýznamImplicitně
$,řetězec, který bude tištěn mezi jednotlivými prvky seznamu (ne v řetězci)""
$"řetězec, který bude tištěn mezi jednotlivými prvky seznamu uvedeném v řetězci označeným uvozovkami." "
$\řetězec, který bude vytištěn na konci seznamu""

Sledujte, jak se změní poslední příklad použitím proměnných $, a $\:

$, = ", ";
$" = "-"; #v tomto případě neovlivní výsledek
$\ = "\n";

print "a", "b", "c";

Vytiskne se řetězec "a, b, c\n". Mezi jednotlivými prvky pole je vždy čárka a mezera a za poslední položkou je znak nového řádku.

$" působí na tisknuté pole, vložené do řetězce:

$, = ", "; #v tomto případě neovlivní výsledek
$" = "-"; #zkuste zakomentovat tento řádek, implicitním obsahem je mezera

@p = ("a", "b", "c");
print "@p";

Vytiskne se řetězec "a-b-c". Zkusme nyní pro zvýraznění rozdílu odstranit z příkazu print uvozovky. Pole by potom nebylo v řetězci, místo $" by se mezi jednotlivé prvky tiskla hodnota v proměnné $, a výsledkem by bylo "a, b, c".

Poslední prvek pole

Zápis $#pole vrací poslední (nejvyšší) index pole. Je ekvivalentní zápisu $pole[-1].

Operátor rozsahu

Přiřaďme nyní do prvků s indexy 5-14 pole @pole2 hodnoty prvků s indexy 2-11 pole @pole:

@pole2[5 .. 14] = @pole[2 .. 11];

Výraz 5 .. 14 má stejný význam jako 5, 6, 7, 8, 9, 10, 11, 12, 13, 14. Při použití na místě indexu pole jde o tzv. řez polem.

Na základě této informace vytvořme pole s hodnotami 1 až 100:

@pole = (1 .. 100);

Operátor rozsahu funguje i na malá a velká písmena anglické abecedy:

@pole = ("c" .. "z"); # výsledkem je pole obsahující znaky c až z

Podívejme se na několik dalších příkladů.

$, = ", ";
$\ = "\n";

print 1.88 .. 2.21;                        #stejné jako 1 .. 2. Desetinná část se odřízne
print "001" .. "010";                      #001, 002, 003 ... 010\n
print "abc" .. "abx";                      #abc, abd, abe ... abx\n
print "bc" .. "de";                        #bc, bd, be ... bz, ca, cb ... dd, de\n
print 0 .. 9, "A" .. "F";                  #nic nového, pouze se tiskne seznam složený ze dvou seznamů
print 0 .. 3, "a" .. "c", $promenna, "XXX";#prakticky totéž
print -10 .. 10;                           #záporná čísla nejsou překážkou
print 10 .. 1;                             #ale pozpátku neumí

Setřídění pole - funkce sort

Pro řazení polí má Perl speciální vestavěnou funkci. Funkce sort řadí (implicitně) podle ASCII tabulky. Další funkce - reverse - obrací pořadí prvků pole. Poslední prvek prohodí s prvním, předposlední s druhým atd.

@p = qw(a g d f e b c); #spřeházený seznam
print @p;               #agdfebc
@p = sort @p;           #setřídí podle abecedy
print @p;               #abcdefg
@p = reverse @p;        #obrátí pořadí prvků
print @p;               #gfedcba

Tento systém má háček. Porovnává po znaku, zleva doprava, takže 6 je větší než 44. Na čísla je předchozí zápis nepoužitelný.

Řazení čísel ale zvládne sort také. Slouží k tomu speciální konstrukce {$a <=> $b}. Porovnává vždy dva prvky pole, určuje, který je větší a takto je řadí. Proměnné $a a $b jsou dvě speciální proměnné určené pro tento účel. Když prohodíme jejich pořadí, setřídí se pole sestupně.

@p = (6, 1, 3, 5, 4, 2);  #přeházený seznam
print @p;                 #613542
@p2 = sort {$a <=> $b} @p;#setřídí podle velikosti vzestupně
print @p2;                #123456
@p3 = sort {$b <=> $a} @p;#setřídí podle velikosti sestupně
print @p3;                #654321

Poznamenejme, že sort bez uvedeného bloku vlastně znamená totéž, co bychom získali uvedením {$a cmp $b}. Do bloku lze napsat v podstatě cokoliv, co vrací 0, 1 nebo -1 na základě speciálních proměnných. Například pro setřídění názvů souborů podle času jejich vzniku bychom mohli napsat toto:

@files = sort {(stat($a))[9] <=> (stat($b))[9]} @files;

Nastavení jazyka

Dalším problémem pro třídění podle abecedy je čeština. Implicitně Perl třídí písmena s háčky až na konec a písmeno ch za c. To se dá vyřešit pomocí locales. V systému nastavme hodnotu proměnné LANG na czech:

$ export LANG=czech

Je dobré si tento řádek připsat do nějakého konfiguračního souboru, který se spouští při zapnutí systému.

Dalším krokem je přidat do samotného programu příkaz use locale.

#!/usr/bin/perl
use locale;

$, = ", ";
$\ = "\n";

@p = qw(a d f č ď e c ch u); #přeházený seznam
print sort @p;

Pokud máme správně nastavenou proměnnou prostředí LANG, měli bychom spatřit výstup:

$ ./sort.pl
a, c, č, d, ď, e, f, ch, u
$

Teď smažme řádek use locale;:

$ ./sort.pl
a, c, ch, d, e, f, u, č, š
$

Třídění pole - funkce grep

grep je analogií stejnojmenného unixového příkazu. Vyhledává prvky v seznamu, ve kterých byl nalezen určitý podřetězec a ty vrací jako seznam. Existují dvě možnosti zápisu grepu:

@vyhovujici_prvky = grep {test} seznam
@vyhovujici_prvky = grep /regulární_výraz/, seznam

V prvním řádku grep postupně přiřazuje každý prvek seznamu do výchozí proměnné $_, ověří, zda vyhovuje výrazu ve složených závorkách, a pokud ano, prvek se ocitne i ve výsledném poli.

$, = ", ";
@pole = (8, 2, 4, 6, 9, 5);
print grep {$_ < 7} @pole; #vytiskne 2, 4, 6, 5 - tedy ty prvky, které jsou menší než 7

V druhém zápisu je namísto testovacího výrazu použit vzor. To je záležitost regulárních výrazů. grep vrací seznam prvků z původního pole, které danému vzoru vyhovují

$, = ", ";
@pole = ("chomp", "print", "grep", "sin", "index");
print grep /^.+in/, @pole;

Vypsáno bude "print, sin" - tedy řetězce, které obsahují podřetězec in, ale nezačínají jím. Regulární výrazy budeme již brzy řešit v samostatných dílech.

Příklad - výčet potřebných platidel

Podívejme se opět na trochu delší ukázku programu. Zadání úlohy je následující: Napište program, který načte ze vstupu číslo, vyjadřující peněžitý obnos. Definujte pole, ve kterém budete mít seznam hodnot českých platidel (haléřové mince neuvažujeme - tedy v poli budou prvky pro 1 Kč, 2 Kč, 5 Kč, 10 Kč ... 2000 Kč, 5000 Kč). Obnos poté rozdělte na hodnoty jednotlivých platidel tak, aby byl rozložen do co nejvyšších platidel. Přitom se snažte o obecnost - tedy například přibude-li platidlo, musí stačit jeho hodnotu přiřadit do pole platidel. Ať pracuje program. Výsledky (kolik kterých platidel) tiskněte na výstup.

Řešení

Začátek je jasný, definujeme pole platidel:

#!/usr/bin/perl

@platidla = (1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000);

Načteme částku. Tady se na chvilku zastavíme. Musíme ošetřit hned tři nestandardní případy. Uživatel vůbec nemusí zadat číslo nebo může být záporné. Oba problémy vyřešíme tak, že otestujeme, zda je částka větší než 0 (poznámka: pokud tedy uživatel zadá "506xyz", bude částka zkonvertována na 506). Dalším problémem jsou desetinná čísla. Je zadáno, že haléře máme zanedbat, takže funkcí int zadanou haléřovou částku ořízneme.

print "Zadej částku v Kč: ";
$castka = int <STDIN>;

if ($castka < 1){
    die "Toto není regulérní častka.\n";
}

Zamysleme se nad tím, jak budeme postupovat dál. Určitě budeme mít nějaký cyklus. V každé jeho iteraci budeme odečítat příslušný počet jednoho druhu platidla. Protože mají být platidla maximálně vysoké, bude výhodné v první iteraci odečítat nejvyšší platidlo, v druhé iteraci druhé nejvyšší atd. Proto nejprve seřaďme pole sestupně:

@platidla = sort {$b <=> $a} @platidla;

Zbývá najít nějaký vhodný test, který by rozhodoval o pokračování cyklu. Iterovat se bude tolikkrát, kolik prvků má pole @platidla. Zaveďme ještě před cyklem proměnnou $pozice, kterou nastavíme na 0. Každou iterací se inkrementuje. Až nabude hodnoty stejné jako počet prvků pole @platidla, cyklus se zastaví. A protože předem známe počet iterací, použijeme cyklus for. Nic nám nebrání začít se samotným cyklem.

for my $pozice (0 .. @platidla){

    ... n krát odečtení hodnoty platidla ...

}

Teď je třeba se zamyslet, jak to bude vypadat v cyklu. Zjistíme počet kusů daného platidla, které lze maximálně odečíst, aby byl výsledek kladný. Stačí od zbývající částky odečíst zbytek, který by zbyl po dělení hodnotou aktuálního platidla - výsledkem bude nejvyšší částka dělitelná hodnotou platidla, ale stále nižší než zbývající částka. Tuto vzniklou částku vydělíme aktuální hodnotou platidla:

    $pocet_kusu = ($castka - ($castka % $platidla[$pozice])) / $platidla[$pozice];

Získali jsme počet kusů aktuálního platidla, který lze odečíst od zbývající částky. Tak to udělejme:

    $castka -=  $pocet_kusu * $platidla[$pozice];

Poznámka: Možná jste si všimli, že pokud bychom tyto dva řádky spojili do jednoho (za $pocet_kusu v druhém dosadili hodnotu prvního), získali bychom zlomek, který obsahuje v čitateli i jmenovateli hodnotu $platidla[$pozice], a tudíž jde zkrátit. $pocet_kusu ale ještě budeme potřebovat.

To je téměř celé. Zbývá zařídit nějaký výstup. Využijme cyklu a výsledky budeme vypisovat přímo v něm. Ještě před cyklus ale vložme pro pořádek velikost původního obnosu:

print "K vyplacení $castka Kč bude potřeba:\n";

A v cyklu bude:

    print "$pocet_kusu kusů platidla $platidla[$pozice].\n";

Jsme hotovi, nyní se pokochejme pohledem na chod právě vzniklého programu:

$ ./vycet.pl
Zadej částku v Kč: 19749
K vyplacení 19749 Kč bude potřeba:
3 kusů platidla 5000.
2 kusů platidla 2000.
0 kusů platidla 1000.
1 kusů platidla 500.
1 kusů platidla 200.
0 kusů platidla 100.
0 kusů platidla 50.
2 kusů platidla 20.
0 kusů platidla 10.
1 kusů platidla 5.
2 kusů platidla 2.
0 kusů platidla 1.
$

Zkusme do pole @platidla přidat ještě bankovku v hodnotě 10000. Mělo by to teoreticky také fungovat.

$ ./vycet.pl
Zadej částku v Kč: 19749
K vyplacení 19749 Kč bude potřeba:
1 kusů platidla 10000.
1 kusů platidla 5000.
2 kusů platidla 2000.
0 kusů platidla 1000.
1 kusů platidla 500.
1 kusů platidla 200.
0 kusů platidla 100.
0 kusů platidla 50.
2 kusů platidla 20.
0 kusů platidla 10.
1 kusů platidla 5.
2 kusů platidla 2.
0 kusů platidla 1.
$

Možná vám přijde, že není jednoduché se ve výstupu zorientovat okamžitě. Je tam moc řádků. Ještě program trochu pozměníme. Není-li od nějakého platidla ani kus, nebudeme řádek výbec vypisovat. Použijme konstrukci podmínka za příkazem. Vykonání příkazu print v cyklu podmíníme nenulovým počtem kusů daného platidla:

    print "$pocet_kusu kusů platidla $platidla[$pozice].\n" if $pocet_kusu;

Ještě odstraníme platidlo 10000 a program opět vyzkoušíme:

$ ./vycet.pl
Zadej částku v Kč: 19749
K vyplacení 19749 Kč bude potřeba:
3 kusů platidla 5000.
2 kusů platidla 2000.
1 kusů platidla 500.
1 kusů platidla 200.
2 kusů platidla 20.
1 kusů platidla 5.
2 kusů platidla 2.
$

Je to hned o něco lepší. Celý zdrojový kód je možné stáhnout. K dokonalosti chybí přidat deklarace proměnných a zformátovat výstup. Formátování časem budeme věnovat celý díl.

Jako bonus si můžete zkusit program upravit tak, aby správně česky skloňoval slovo kus.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=891