Entwickler-Ecke

Sonstiges (Delphi) - Viele if-Anweisungen kürzen -> Case-Anweisung (wie richtig?)


probare - Mi 04.12.13 15:38
Titel: Viele if-Anweisungen kürzen -> Case-Anweisung (wie richtig?)
Hi,

ich lese eine Textdatei in eine Listbox (ListBoxRead) ein.
Bei einem Klick auf einen Button (btnRead) möchte ich nun jede einzelne Zeile der Liste auf einen bestimmten String prüfen und speziell behandeln.

Folgend erst einmal ein Auszug aus dem Quellcode - wie er bisher war:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
procedure TfrmMain.btnReadClick(Sender: TObject);

var
i : Integer;
...

begin
...
   for i := 0 to ListBoxRead.Items.Count -1 do
   begin 

   if ListBoxRead.Items[i] := '## 123'
      begin
      ...
      end;

   if ListBoxRead.Items[i] := '## 234'
      begin
      ...
      end;
    ...
   end;
end;


Das funktioniert für kleine Textdateien zwar wunderbar, aber für größere ist der Rechenaufwand aufgrund der ganzen if-Anweisungen (sind natürlich nicht nur 2, sondern in diesem Fall eine ganze Menge) ziemlich hoch.

Verbesserung des Rechenaufwands kann hier die case-Anweisung schaffen, wenn ich mich richtig informiert habe.
Leider bekomme ich das in diesem Fall nicht so ganz auf die Reihe und dementsprechend Fehlermeldungen.
Eventuell kann mir hier ja jemand helfen.
Ich poste kurz meine Gehversuche:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
for i := 0 to ListBoxRead.Items.Count -1 do
begin
...
   case (ListBoxRead.Items[i]) of //<- Ordinaltype required
       '## 123' :                //<- Incompatible types 'Integer' and 'String'
           begin
           ...
           end;

       '## 234' :
           begin
           ...
           end;
end;



Wie funktioniert es richtig?

Moderiert von user profile iconMartok: Quote- durch Delphi-Tags ersetzt


baumina - Mi 04.12.13 15:54

Case geht mit Strings nicht. Aber wenn du innerhalb deiner if's ein continue einbaust, muss er sobald er das richtige if gefunden hat nicht alle anderen if#s noch prüfen, sondern springt in deiner for-Schleife gleich weiter.

Delphi-Quelltext
1:
2:
3:
4:
5:
if ListBoxRead.Items[i] := '## 123' 
 begin 
 ... 
   continue;
 end;


probare - Mi 04.12.13 15:57

user profile iconbaumina hat folgendes geschrieben Zum zitierten Posting springen:
Case geht mit Strings nicht. Aber wenn du innerhalb deiner if's ein continue einbaust, muss er sobald er das richtige if gefunden hat nicht alle anderen if#s noch prüfen, sondern springt in deiner for-Schleife gleich weiter.

Delphi-Quelltext
1:
2:
3:
4:
5:
if ListBoxRead.Items[i] := '## 123' 
 begin 
 ... 
   continue;
 end;


Herzlichen Dank, hilft mir schonmal sehr.


WasWeißDennIch - Mi 04.12.13 16:10

Case funktioniert nur mit ordinalen Datentypen, d.h. Typen, die einen klar definierten Vorgänger und Nachfolger besitzen. Dazu gehören ganze Zahlen und Zeichen, aber keine Strings. Etwas komfortabler ginge es evtl. noch mit AnsiIndexText [http://docwiki.embarcadero.com/Libraries/XE3/de/System.StrUtils.AnsiIndexText].


Delete - Mi 04.12.13 17:26

... und wenn in deinen Strings ausschließlich "Zahlenwerte" stehen, wandelst du die Strings in einen Integer um, was dich in die Lage versetzt, doch noch einen Case-Block einzusetzen.


WasWeißDennIch - Mi 04.12.13 17:33

Mit AnsiIndexText ist man ja auch in der Lage, case zu verwenden:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
uses StrUtils;

const
  POSSIBLEVALUES: array[0..4of string = (
    '## 123',
    '## 234',
    '## 345',
    '## 456',
    '## 567');

procedure TFormTest.ButtonTestClick(Sender: TObject);
begin
  case AnsiIndexText('## 345', POSSIBLEVALUES) of
    -1:
      ShowMessage('Nicht enthalten');
    0:
      ShowMessage('Erster');
    1:
      ShowMessage('Zweiter');
    2:
      ShowMessage('Dritter');
    3:
      ShowMessage('Vierter');
    4:
      ShowMessage('Fünfter');
  end;
end;


probare - Mi 04.12.13 17:42

user profile iconPerlsau hat folgendes geschrieben Zum zitierten Posting springen:
... und wenn in deinen Strings ausschließlich "Zahlenwerte" stehen, wandelst du die Strings in einen Integer um, was dich in die Lage versetzt, doch noch einen Case-Block einzusetzen.


Stehen leider nicht nur Zahlenwerte drin.
Der jeweils 3. String ist ein Buchstabe, der jeweils 5. ist eine Zahl und das auch nur Blöckeweise - also Block wird mit '##X 123' geöffnet und mit '##Y 123' geschlossen.
Habe das Ganze jetzt erst einmal so gelöst, dass ich per if-Abfrage das 1. und das 3. Zeichen auslese, damit ich nur mit geöffneten Blöcken arbeite (stoppt sobald '##Y' ) auftaucht.
In der Bearbeitung verwende ich eine weitere Variable, so dass ich die Blockeinträge auslesen kann.
Ist der Block beendet, bin ich wieder in der for-Schleife.
Da ich ja nun weiß, dass die 2. Zeile für mich vermutlich uninteressant ist, arbeite ich mit einer 3. Variablen, die den Lesezähler künstlich hochschraubt.
Somit wird nicht jede Zeile der Textdatei durch alle if-Abfragen gejagd, sondern nur Blockanfang + Blockende, was die Rechenzeit wesentlich verbessert hat.

Das das hier sich natürlich etwas unverständlich liest, das Ganze einmal als Quellcode:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
...
s := 0;
  for i := 0 to ListBoxRead.Items.Count -1 do
    begin
    k := i+s; //schiebt die Schleife künstlich vor, um unnötige Abfragen zu überspringen
    if k >= ListBoxRead.Items.Count then break; //sorgt dafür, dass List out of bounds vermieden wird
    if ListBoxRead.Items[k][1] = '#' then
        if ListBoxRead.Items[k][3] = 'H' then
        begin
        s := 0//setzt den Hilfszähler zurück
      //2
        if ListBoxRead.Items[k] ='##X 2' then
          begin
            while ListBoxRead.Items[k+s] <> '##Y 2' do begin
              StringList2.Add(ListBoxRead.Items[k+s]);
              s := s+1;
            end;
            StringList2.Add('##Y 2');
            continue;
          end;
         ...
     end;
...



@WasWeißDennIch: Danke, werde mich damit mal näher beschäftigen müssen.


jaenicke - Mi 04.12.13 18:17

Eine TStringList statt einer Listbox kann auch noch ein wenig mehr Geschwindigkeit bringen. Eine Listbox ist nur zur Anzeige da, nicht zur Verarbeitung. Du kannst es dem Benutzer natürlich dort zusätzlich anzeigen, aber verarbeiten würde ich es in einer TStringList.

Dazu kommt dann das Einlesen, das geht mit einer MMF sehr schnell, wenn die Datei größer ist.

Zum Vergleich:
Ich habe eine 350 MiB große Registrydatei in wenigen Sekunden komplett geparst und in einem Baum angezeigt. Und da muss ich jeden Eintrag entsprechend zerlegen und prüfen...


Xion - Mi 04.12.13 19:00

Statt continue würde es wesentlich naheliegender auch ein else tun:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
if ... then
  begin
  end
else if ... then
  begin
  end
else if ...

Wenn du die Zweige dann noch nach "Häufigkeit" sortierst, dann bringt das auch nochmal was.

Für eine etwas ausgefeiltere Lösung wäre wohl eine Hashtabelle/-funktion geeignet. Du übersetzt deinen String in einen (weitestgehend eindeutigen) Integer und benutzt den dann für case (oder als Funktionspointer...). Das wird wohl die effizienteste Möglichkeit sein.
//Edit: ich nehme an, dass genau dies durch AnsiIndexText, wie oben erwähnt, realisiert wird


Tranx - Mi 04.12.13 19:20

Wenn Deine Strings in einer Stringlist stehen, kannst Du auch folgendes abfragen:



Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
//function IndexOf(const S: string): Integer; override;
//Beschreibung
//Mit IndexOf können Sie das erste Auftreten des Strings S ermitteln. IndexOf gibt den auf Null basierenden Index des Strings zurück. Wenn S also mit dem ersten String in
//der Liste übereinstimmt, liefert IndexOf 0 zurück, beim zweiten String 1 usw. Ist der String nicht vorhanden, gibt IndexOf -1 zurück.

  var
    StrgLst : TStringList;
    s : string;
  begin
    StrgLst := TStringList.Create;
    // hier die Stringliste StrgLst füllen
    
    //z.B.: s = '## 123':
    s := '## 123';
    n = StrgLst.IndexOf(s); 
    case n of 

    0 : ..;
    1 : ..;
    else
    ..;
    end;
  end;


probare - Mi 04.12.13 20:41

user profile iconTranx hat folgendes geschrieben Zum zitierten Posting springen:
Wenn Deine Strings in einer Stringlist stehen, kannst Du auch folgendes abfragen:



Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
//function IndexOf(const S: string): Integer; override;
//Beschreibung
//Mit IndexOf können Sie das erste Auftreten des Strings S ermitteln. IndexOf gibt den auf Null basierenden Index des Strings zurück. Wenn S also mit dem ersten String in
//der Liste übereinstimmt, liefert IndexOf 0 zurück, beim zweiten String 1 usw. Ist der String nicht vorhanden, gibt IndexOf -1 zurück.

  var
    StrgLst : TStringList;
    s : string;
  begin
    StrgLst := TStringList.Create;
    // hier die Stringliste StrgLst füllen
    
    //z.B.: s = '## 123':
    s := '## 123';
    n = StrgLst.IndexOf(s); 
    case n of 

    0 : ..;
    1 : ..;
    else
    ..;
    end;
  end;


Das habe ich auch schon in Betracht gezogen. Da s aber ~600 verschiedene Varianten annehmen kann, hätte es denselben Effekt einer if-Abfrage, wenn ich mich nicht irre.



user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Eine TStringList statt einer Listbox kann auch noch ein wenig mehr Geschwindigkeit bringen. Eine Listbox ist nur zur Anzeige da, nicht zur Verarbeitung. Du kannst es dem Benutzer natürlich dort zusätzlich anzeigen, aber verarbeiten würde ich es in einer TStringList.

Dazu kommt dann das Einlesen, das geht mit einer MMF sehr schnell, wenn die Datei größer ist.

Zum Vergleich:
Ich habe eine 350 MiB große Registrydatei in wenigen Sekunden komplett geparst und in einem Baum angezeigt. Und da muss ich jeden Eintrag entsprechend zerlegen und prüfen...

Im Moment speicher ich jeden Block in seine zugehörige TStringList (es gibt hier ca 600 verschiedene), um diese einerseits zu sortieren, andererseits nur eine Ausgabe brauche.
Das Programm braucht zur Zeit ca 12 Sekunden für das Sortieren einer 1,6 mb großen Datei. Dabei ist die Prozessorauslastung recht hoch, was ich zu Minimieren versuche, soweit es denn geht.


OlafSt - Mi 04.12.13 23:29

Die Prozessorauslastung kann egal sein - solange sie angenehm kurz ist ;)

Wie wäre es mit einem anderen Ansatz ?

Zuerst: Keine Listbox nehmen, die sind dafür nicht gedacht, der Overhead für die ganze visuelle Aufbereitung ist gewaltig. Eine TStringList ist wirklich besser geeignet, zur Visualisierung reicht ein Progressbar.

Als nächstes würde ich die Datei gar nicht mit LoadFromFile in die TSL einsaugen. Diese Art Listen haben das Problem, das sie mit wachsender Zeilenzahl immer langsamer werden, bis es irgendwann eine Minute dauert, um eine Zeile anzuhängen... Stattdessen würde ich die Datei zeilenweise lesen und dort schon mal schauen, was überhaupt relevant ist. Das Relevante geht in die StringList, die dann genauer untersucht wird. Beim Einfügen in die StringList kann man sich dann überlegen, ob man wirklich "###X 123" einfügen muß - oder man das gleich übersetzt in "04123", das "##Y 123" in "05123" etcpp... Die restlichen Ideen zur Verarbeitung überlasse ich Dir ;)


GuaAck - Mi 04.12.13 23:34

Hallo Jaenicke,

das MMF liest sich gut, aber was bedeutet MMF? Weder in den Hilfen für Delphi und Windows noch mit Google habe ich was sinnvolles finden können.

Gruß
GuaAck


jaenicke - Mi 04.12.13 23:56

MMF = Memory Mapped File
http://www.entwickler-ecke.de/topic_SJ+MMF+File+Reader+02++Schneller+Textdatei+Reader_99933,0.html


probare - Mi 04.12.13 23:56

user profile iconOlafSt hat folgendes geschrieben Zum zitierten Posting springen:
Die Prozessorauslastung kann egal sein - solange sie angenehm kurz ist ;)

Wie wäre es mit einem anderen Ansatz ?

Zuerst: Keine Listbox nehmen, die sind dafür nicht gedacht, der Overhead für die ganze visuelle Aufbereitung ist gewaltig. Eine TStringList ist wirklich besser geeignet, zur Visualisierung reicht ein Progressbar.

Als nächstes würde ich die Datei gar nicht mit LoadFromFile in die TSL einsaugen. Diese Art Listen haben das Problem, das sie mit wachsender Zeilenzahl immer langsamer werden, bis es irgendwann eine Minute dauert, um eine Zeile anzuhängen... Stattdessen würde ich die Datei zeilenweise lesen und dort schon mal schauen, was überhaupt relevant ist. Das Relevante geht in die StringList, die dann genauer untersucht wird. Beim Einfügen in die StringList kann man sich dann überlegen, ob man wirklich "###X 123" einfügen muß - oder man das gleich übersetzt in "04123", das "##Y 123" in "05123" etcpp... Die restlichen Ideen zur Verarbeitung überlasse ich Dir ;)


Der Ansatz gefällt mir.
Werde mir morgen mal näher dazu Gedanken machen.


Horst_H - Do 05.12.13 07:34

Hallo,

ist das Problem, ohne Platzhalter freilich, in http://www.entwickler-ecke.de/viewtopic.php?t=83336 mittels "wu-manger" angegangen worden?

Gruß Horst


mandras - Do 05.12.13 12:35

Du kannst für das Suchen eine StringList mit sorted=true einsetzen,
dann erfolgt bei indexof oder find eine binäre suche die recht flott ist.


probare - Do 05.12.13 23:21

user profile iconHorst_H hat folgendes geschrieben Zum zitierten Posting springen:
Hallo,

ist das Problem, ohne Platzhalter freilich, in http://www.entwickler-ecke.de/viewtopic.php?t=83336 mittels "wu-manger" angegangen worden?

Gruß Horst


Noch nicht, sind auch schon eine ganze Menge Ideen und Ansätze auf einmal, was ich so auf Anhieb nicht erwartet hätte. Werde am Wochenende mal einige Varianten ausprobieren und testen.

Herzlichen Dank


probare - Fr 06.12.13 14:22

user profile iconmandras hat folgendes geschrieben Zum zitierten Posting springen:
Du kannst für das Suchen eine StringList mit sorted=true einsetzen,
dann erfolgt bei indexof oder find eine binäre suche die recht flott ist.


wie gesagt, stehen in der textdatei blöckeweise informationen.
setze ich nun sorted = true, werden die blöcke durcheinander geworfen.

trotzdem danke, die information könnte mir in anderen projekten evtll noch behilflich sein.