Arbejde med tekst

Tags:    delphi
Skrevet af Bruger #173 @ 19.06.2000
Arbejde med tekst

I denne artikel vil jeg fortælle noget om hvordan man arbejder med tekst i Delphi. Jeg vil gøre det med praktiske eksempler, for tit er det sådan, at man ved, hvad man skal gøre, men ikke hvordan, man skal gøre det. Det vil jeg forsøge at rode bod på i denne artikel.

Det grundlæggende

Noget af det man som regel altid støder på, når man arbejder med tekst er hvordan de enkelte tegn i en tekststreng behandles. Det gøres ligesom i et array, men hvor arrayer normalt starter ved 0 og derfor har det sidste element ved Length(arrayet) - 1, indekseres tekststrenge altid fra 1 til længden af tekststrengen, med andre ord Length(teksten). Det er en vigtig ting at huske ved behandling af tekst i en løkke. Og løkker bruges meget ved arbejdet med tekst, netop fordi de indekseres på samme måde som arrays, som jo bruges meget i samarbejde med løkker.

Jeg vil ikke bruge andet end string typen, fordi den er den mest brugte type og opfylder de behov, man som regel har. Hvis du alligevel vil have mere information om andre teksttyper, så se artiklen om emnet her på udvikleren.dk. Det er en god idé at bruge const i parameterlisten, hvis altså typen af parameteren er string. Det både forøger hastigheden syv gange, vist nok, og så kan holde dig fra at ændre værdien af variablen. Om det er en fordel eller ej, er op til dig i den enkelte situation. Nu til det første eksempel på en stringrutine:
UpperCaseLetters = ['A'..'Z', 'Æ', 'Ø', Å];

//------------------------------------------------------------------------------
// Returnerer sandt hvis teksten kun består af store bogstaver.
// Fx.: IsUpperCase('YES') returnerer sandt.
//------------------------------------------------------------------------------
function IsUpperCase(const sText: string): Boolean;
var
  i: Integer;
begin
  Result := True;

  if sText = '' then begin
    Result := False;
    Exit;
  end;
  
  // test hvert tegn i teksten, og hvis der findes et tegn,
  // som ikke er et stort bogstav, så returner falsk
  for i := 1 to Length(sText) do begin  
    if not (sText[i] in UpperCaseLetters) then begin
      Result := False;
      Exit;
    end;
  end;
end;
I eksemplet kører vi igennem alle tegn i teksten, sText. Ved det første tegn, der ikke er et stort bogstav i teksten, returneres False og afsluttes. Det gøres fordi det er unødvendigt at fortsætte, for rutinens formål er at tjekke om hele teksten er skrevet med store bogstaver. Hvis der i teksten findes et bogstav der ikke er stort er det jo ikke tilfældet. Hvis derimod hele løkken er blevet kørt igennem, bevares værdien af Result, som er True.

Eksemplet er ikke det mest avancerede, men hvis du forstår eksemplet, har du forstået det vigtigste i arbejdet med tekster, eller i hvert fald noget, som man bruger meget, og du kan sagtens fortsætte med de lidt sværere eksempler.

Et bibliotek af stringrutiner

Der er intet bedre end at have et stort sæt af rutiner til behandling af tekst i en fil, som har veldefinerede rutiner med passende navne og opgaver. Hvis interfacet af alle rutiner er godt, kan du have meget glæde af dem. Så kan du nemlig kalde en rutine, som gør lige hvad du forventer. Det gode ved sådanne filer, der består af et sæt rutiner, eller bare enhver rutine i sig selv, er at du kun behøver at lave og teste din rutine én gang. For når bare den virker, kan du bruge den så meget du vil, og så kan du lave flere og flere og... Jeg har selv lavet et lille bibliotek af rutiner til arbejde med tekst, og det kan være, at jeg tilføjer den her til downloadsektionen engang. Men ellers findes der andre af slagsen på nettet. Og hvis andre nu har lavet sådanna biblioteker, hvorfor skal du så bekymre dig om at lave rutiner? Det er ikke sikkert at biblioteket har en rutine, der lige passer til din opgave, så derfor kan du være nødt til at lave det selv. Det er altid praktisk at kunne det. Og det kan jo være at du får lyst til selv at skrive nogle rutiner.

Mere avancerede eksempler

Jeg vil her demonstrere nogle eksempler, som alle kommer fra mit lille bibliotek. Eksemplerne har til sammen det formål at kunne få adgang til ord, fx i en kommandolinje, hvor ord i anførselstegn og i parantes betragtes som ét ord. Jeg vil ikke kommentere eksemplerne uderligere end det er gjort i selve koden. Lad os se på det.

Der er brug for en rutine til at gøre det nemmere at finde ordene i teksten. Til det har jeg lavet denne funktion:
//------------------------------------------------------------------------------
// Rydder op i en tekst og returnerer en kopi med kun ét tegn, cTheChar, i træk.
// Fx.: CleanUp('Hej   igen') returnerer 'Hej igen'.
//------------------------------------------------------------------------------
function CleanUp(const sText: string; const cTheChar: Char = ' '): string;
var
  i, j: Integer;
begin
  Result := sText;
  i := 1;

  // løb teksten igennem, og hvis tegnet findes 
  while (i < Length(Result)) do begin
    if Result[i] = cTheChar then begin
      j := Succ(i);

      // så slet alle forekomster af det inden et andet tegn forekommer
      if Result[j] = cTheChar then begin
        while (Result[j] = cTheChar) do begin
          Delete(Result, j, 1);
          i := j;
        end;
      end else
        Inc(i);

    end else
      Inc(i);

  end;
end;
Nu ved vi at vi altid kan finde det næste ord efter et mellemrum. Bemærk at jeg har tænkt at disse eksempler bruges til en kommandolinje, altså uden nogle linjeskift, men det kunne også være lavet. Nu skal vi så finde en måde, hvorpå vi kan få styr på alle ordene i kommandolinjen. Det kan gøres sådan her:
//------------------------------------------------------------------------------
// Returnerer ordene i teksten til en StringList.
// Fx.: GetWords('Hej med dig')[0] returnerer 'Hej'.
//------------------------------------------------------------------------------
function GetWords(const sText: string)): TStringList;
var
  i: Integer;
  sCurrentWord: string;
  sCopyOfText: string; // for at sText skal være konstant
begin
  Result := TStringList.Create;

  // slet alle dobbelte mellemrum og alle mellemrum i starten og slutningen
  sCopyOfText := CleanUp(Trim(sText));

  sCurrentWord := '';

  // start med det første tegn i teksten
  i := 1;

  // gennemløb teksten
  while (i <= Length(sText)) do begin
    // hvis der er et mellemrum så ved vi at vi har et ord til, så læg
    // ordet ind i stringlisten og fortsæt
    if (sCopyOfText[i] = ' ') then begin
      Result.Add(sCurrentWord);
      sCurrentWord := '';
    end
    else
      sCurrentWord := sCurrentWord + sCopyOfText[i];

    // hvis det er sidste tegn, fx: 'hej nu' skal 'nu' lægges ind i Strenglisten
    if i = Length(sCopyOfText) then
      Result.Add(sCurrentWord);

    // fortsæt altid med det næste tegn
    Inc(i);
  end;

end;
Nu har vi styr på ordene, og så er det ikke så svært at få fat i fx. det andet ord:
var
  CommandLine: TStringList;
begin
  CommandLine := TStringList.Create;
  CommandLine := GetWords('delete fil.txt');
  // ved godt at det ikke er nødvendtigt, det er bare noget man skal gøre,
  // hvis man ikke ved hvor mange ord der er
  if CommandLine.Count >= 2 then
    ShowMessage(CommandLine[1]);
end;
Nu ser det da ud til at virke. Men hvad hvis et filnavn indeholder mellemrum? Så vil det blive betragtet som to eller flere ord. For eksempel: delete fil som skal slettes.txt. Det er jo ikke så smart. Vi skal derfor lave endnu en funktion, denne gang en, der returnerer tekst i anførselstegn og i parantes som ét ord. Den kunne se sådan her ud (Vi kunne nøjes med denne funktion, hvis den blev lavet lidt om, men jeg har begge i mit bibliotek, så...):
//------------------------------------------------------------------------------
// Det samme som GetWords, undtagen at tekst mellem et hvilket som helst tegn i
// sChars betragtes som ét ord betragtes som ét ord.
// Fx.: GetWordsEx('Det hedder "Delphi 5"', '"')[2] returnerer '"Delphi 5"'.
//------------------------------------------------------------------------------
function GetWordsEx(const sText, sChars: string): TStringList;
var
  i, j: Integer;
  TheWords: TStringList;
  sQuotedWord: string;
begin
  Result := TStringList.Create;
  TheWords := TStringList.Create;
  TheWords := GetWords(sText);
  sQuotedWord := '';
  i := 0;
  j := 0;
  // løb alle ordene igennem og hvis der findes et tegn, der skal tjekkes for,
  // i starten af et ord, så lav kun ét ord ud af det, indtil der findes
  // et andet af det samme tegn.
  while i < TheWords.Count do begin
    if Pos(TheWords[i][1], sChars) = 0 then begin
      Result.Add(TheWords[i]);
      Inc(i);
      Continue;
    end;
    // hvis vi er kommet hertil findes der 'omkransende' tegn i starten af ordet
    j := i;
    while j < TheWords.Count do begin
      // ordets sidste tegn indeholder ikke et omkransende tegn
      if Pos(TheWords[j][Length(TheWords[j])], sChars) = 0 then begin
        sQuotedWord := sQuotedWord + TheWords[j] + ' ';
        Inc(j);
        Continue;
      end
      // der er fundet det omkransende tegn
      else begin
        sQuotedWord := sQuotedWord + TheWords[j];
        Result.Add(sQuotedWord);
        sQuotedWord := '';
        i := j;
        Break;
      end;
    end;
    Inc(i);
  end;

end;
Så er vi klar til at bruge alle disse rutiner til noget nyttigt, og hvad med at lave et eksempel på en kommandolinje, der udfører forskellige kommandoer med deres parametre. Alt hvad du skal bruge er en TEdit, eller en TCombobox. I eksemplet bruger jeg en combobox, hvor teksten er nulstillet fra start. I type og var-sektionerne i toppen af filen, skriver du dette:
[...]
type
  TCommand = function: Boolean;

// en kommando
function DeleteFile: Boolean;

var
  Commands = array ['a'..'z'] of TCommand;
[...]
Og Comboboksens OnKeyDown-procedure ser sådan her ud:
procedure TForm1.ComboBox1KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  TheWords: TStringList;
begin
  // hvis der trykkes Enter, afgøres hvilken kommando der skal kaldes
  if Key = VK_Return then begin
    TheWords := TStringList.Create;
    TheWords := GetWordsEx(Combobox1.Text, '"');

    // jeg ville nok udfylde kommandoerne et andet sted, men
    // set bliver altså bare gjort her
    Commands['d'] := DeleteFile;

    // ...

    // udfører kommandoen hvis den findes
    try
      if Commands[TheWords[0][1]] then
        ShowMessage('Kommando udført korrekt.');
    except
      ShowMessage('Ugyldig kommando.');
    end;

  end;

end;
Funktionen DeleteFile's job ville nok være at slette en fil, og returnere sandt eller falsk alt efter om det lykkedes, men for enkelthedens skyld har jeg valgt at lave det på denne simple måde:
function DeleteFile: Boolean;
begin
  Result := True;
end;
Så burde alt være klar til en afprøvning, så kør programmet med det samme. Hvis det virker kan du i comboksen skrive 'd' og en dialogboks vil komme frem og sige at kommandoen er udført korrekt, ellers siges der, at det er en ugyldig kommando. Det er en ret primitiv måde at skrive kommandoer på med kun ét bogstav, og det er nok ikke det du vil gøre i rigtige programmer, men det har jeg altså valgt. Spørg mig ikke hvorfor. Jeg har ikke udnyttet rutinerne til fulde; der kunne også arbejdes med parametre og mere til, men det kan du måske lave, hvis du har lyst. Jeg håber du - hvis du ikke havde det før - har fået en bedre forståelse for arbejdet med tekst, og du kan nu selv lave nogle rutiner, hvis du har lyst. Du vidste nok godt, hvordan man indekserede tekststrenge, men der skal tit nogle praktiske eksempler frem på bordet, før man kan forstå det bedre og selv arbejde med det. Held og lykke med det!


Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (1)

User
Bruger #5097 @ 09.05.04 20:46
Er enig med Zackie:
Du skal være logget ind for at skrive en kommentar.
t