int21h

Převod malých/velkých písmen

Je podivné, jak málo pozornosti je programátory věnováno této drobnosti. Přitom téměř v každém programu je potřeba převést občas nějaký řetězec na malá nebo naopak velká písmena. Bohužel Turbo pascal nemá žádnou funkci, která by toto usnadňovala.
Existuje pouze funkce na převod jednoho znaku na velká písmena:
Function UpCase(c:char):char;
Jestli chceme převést celý řetězec, musíme funkci rozšířit:
Function UpString(s:string):string;
var a:byte;
begin
for a:=1 to Length(s) do s[a]:=UpCase(s[a]);
UpString:=s;
end;  
Pořád jednoduché. Co když ale chceme naopak převádět na malá písmena? Tady Turbo pascal selhává zcela, nebo? neposkytuje vůbec žádnou pomoc. Naštěstí jsou písmenka v ASCII tabulce uspořádána podle abecedy: velké A má kód 65, velké B 66 až velké Z má kód 90
Malá písmena jsou uložena obdobně:
malé a - 97, b - 98 až z - 122

Z uvedeného vyplývá, že ke kódu znaku velkého písmena stačí přičíst 32 a dostaneme kód malého písmena. Tedy:
Function DownCase(c:char):char;
begin
DownCase:=char(byte(c)+32);
end;  
Funkce má jednu vadu na kráse, a to, že by "omylem" mohla převést i jiné znaky než písmena a my bychom dostávali nesmysly. (zkuste převést na velká písmena takový otazník :-)
Takže znovu a lépe:
Function DownCase(c:char):char;
begin
if (c>64) and (c<91) then DownCase:=char(byte(c)+32);
                     else DosnCase:=c;
end;  
Naopak pomocí odčítání 32 si můžeme funkci UpCase napsat i sami.
Jednoduchost těchto převodů usnadňuje přepis do assembleru:
function DownCase(c:char):char;assembler;
asm
mov al,c
cmp al,64
jng @nic
cmp al,91
jnl @nic
add al,32
@nic:
end;  

Dosud jsme se jen tak rozpinkávali. Všechny tyto funkce totiž mají zásadní nedostatek. Pracují jenom se znaky anglické abecedy. Kdybychom chtěli převádět české znaky, máme smůlu. Ještě palčivější je situace například v ruských programech, kde se píše azbukou a všechny znaky jsou tedy neanglické. Co s tím?
DOS má na to naštěstí funkce. Sice polovičaté, ale aspoň něco. DOS totiž v paměti udržuje strukturu, která se nazývá Popis národního prostředí. Tam je zaznamenán kód země, formát data a času, formát měny a podobně. Pro nás je ale podstatné, že je tam také tabulka na převod znaků včetně znaků národních - tedy neanglických. Aby to fungovalo, tak musí být podpora národního prostředí nainstalována a aktivní. Jestli máte puštěná windows, tak máte o starost méně, protože ta národní prostředí DOSu nastavují sama. Jestli jste v čistém DOSu, tak musíte mít v CONFIG.SYS nastavenou hodnotu COUNTRY=042,852,c:\dos\country.sys anebo musíte na příkazové řádce zavolat NLSFUNC
Bohužel je k dispozizi jenom tabulka pro převod na velká písmena, převodník na malá chybí a musíme si ho dotvořit sami.
Proto je praktické, aby náš program, respektive unit, který bude mít tuto problematiku na starosti, si vytvořil dvě převodní tabulky znaků - z malých na velké a z velkých na malé.
var male_na_velke,
    velke_na_male:array[0..255] of char  

DOSovou tabulkou zaplníme tabulku male_na_velke a poté odvodíme velke_na_male. DOSová tabulka má malinko zádrhel, že obsahuje hodnoty pouze pro ASCII kódy > 127, což je sice v pořádku, protože spodní polovina ASCII tabulky je neměnná, ale příjde mi to trošičku nepohodlné. Já osobně proto používám malý trik. Místo přímého načtení DOSové tabulky si nechám převést řetězec obsahující všechny (tedy 0..255) kódy ASCII. Na to existuje šikovná funkce INT 21h/AX=6521h, která funguje jako funkce UpString ze začátku článku.
Pojďme tedy na věc:
Procedure PripravPrevodniTabulky;
{Pokud prekladate pomoci FP, tak musite mit v uses jednotku Go32}
var i:byte;
    {$IFDEF FPC}r:TRealregs;{$ENDIF}
begin
for i:=0 to 255 do male_na_velke[i]:=char(i);


{$IFDEF FPC}
r.ds:=tb_segment;
r.eax:=$6521;
r.edx:=tb_offset;
r.ecx:=255;
CopyToDOS(male_na_velke,255);
RealIntr($21,r);
CopyFromDOS(male_na_velke,255);
{$ELSE}
asm
 push ds
 lea dx,male_na_velke
 mov cx,255
 mov ax,6521h
 int 21h
 pop ds
end;
{$ENDIF}


{Parada,  mame tabulku  male_na_velke a  ted si  odvodime tabulku opacnou}


for i:=0 to 64 do velke_na_male[i]:=char(i);
for i:=65 to 90 do velke_na_male[i]:=char(i+32);
for i:=91 to 127 do velke_na_male[i]:=char(i);
fillchar(velke_na_male[128],127,0);
for i:=128 to 255 do
    if velke_na_male[i]=#0 then
       begin
       velke_na_male[i]:=char(i);
       if male_na_velke[i]=char(i) then velke_na_male[i]:=char(i) else
          velke_na_male[byte(male_na_velke[i])]:=char(i);
       end;
end;  
Vidíte, že jsem napřed naplnil všemi ASCII kódy proměnnou male_na_velke a nechal si ji převést na velká písmena. Tím jsem získal první tabulku. Potom ručně vytvářím opačnou tabulku. Znaky menší než 128 můžu vyřešit z fleku, ale horší je to s horní polovinou tabulky. Procházím celou horní polovinu ASCII a dívám se do "zvětšovací" tabulky. Jestliže je ASCII kód a příslušná hodnota ve "zvětšovací" tabulkce shodná, tak nejde o písmeno a tuto hodnotu můžu bez obav dát i do tabulky "zmenšovací" anebo je to velké národní písmeno, které tam ovšem můžu dát taky, protože vím, že ho později přepíšu.
Když se ale neshodují, tak jde o malé písmeno. Dám ho tedy do "zmenšovací" tabulky a potom udělám trochu zvláštní věc:
Najdu si pro toto písmenko "ve zvětšovadle" velký ekvivalent a "opravím" pomocí jeho "zpětného převodu" opravím případ, kdy jsem před tím dal do "zmenšovadla" velké národní písmeno.

Určitě jste si všimli, že jsem napsal obojživelný kód pro TP i FP. Znovu na něm můžete vidět, jak se ve FP přistupuje k datovým strukturám DOSu pomocí Transerových bufferů. Nezapomeňte také zahrnout do vašeho uses jednotku Go32.
Ve skutečnosti ale FreePascal převádět národní znaky v řetězcích umí i sám od sebe, takže v podstatě tento článek nepotřebujete :-)
UpCase sice funguje stejně jako v TP, ale FreePascal má navíc jednotku SysUtils, která definuje (mimo jiné) mnoho pěkných řetězcových funkcí. Pro zvětšení použijte
AnsiUpperCase a pro zmenšení AnsiLowerCase

Pojďme ale ještě dokončit ten Turbo pascal :-)
Když už máme vytvořené převodní tabulky, tak napsat příslušné převodní funkce je triviální:
function Convert_Up(s:string):string;
var b:byte;
begin
for b:=1 to Length(s) do
    s[b]:=male_na_velke[byte(s[b])];
Convert_Up:=s;
end;


function Convert_Down(s:string):string;
var b:byte;
begin
for b:=1 to Length(s) do
    s[b]:=velke_na_male[byte(s[b])];
Convert_Down:=s;
end;  

Pro praktické použití dám jednu radu. Už jsem to psal v článku o rozboru příkazového řádku. Určitě máte nějakou svoji unitu, kterou dáváte prakticky do každého vašeho programu. Radím vám, přidejte do ní proceduru PripravPrevodniTabulky a nejspíše také funkce Convert_Up a Convert_Down
Současně také včleňte PripravPrevodniTabulky do inicializační sekce vaší jednotky. Tím si zajistíte, že převodníky budou vždy připraveny a nemůže se vám stát, že byste zapomněli zavolat PripravPrevodniTabulky.
DOS-u-akbar!
2007-01-05 | Laaca
Reklamy: