Entwickler-Ecke

Sonstiges (Delphi) - Mathematischen Parser schreiben


Benutzername - Fr 05.08.05 22:24
Titel: Mathematischen Parser schreiben
Hi Leute^^

Mich überkam mal wieder die Lust, was neues zu programmieren, das auch nicht zu einfach ist.
Und so bin ich drauf gekommen, mir nen Mathe-Parser zu basteln.

Ich hab auch schon gegoogelt und bin auf verschiedene Anstöße gekommen:
Iterative Parser, rekursive Parser, UPN-Parser, Parser mit Stack usw.
Hab mir auch schon diverse PDFs und andere Seite angeschaut, abe rda war irgendwie nix weiter interessantes dabei, das nicht irgendwie Parser-Generatoren o.Ä. verwendet hätte :?

Aber jetzt meine Frage: Wie würdet ihr das angehen?
Ich hab sowas noch nie gemacht, von daher bin ich praktisch auf eure Hilfe angewiesen ;)

Mir kommts erstmal überhaupt nicht auf Geschwindigkeit an, sondern darauf, dass er erstmal das wesentliche beherrscht: +-*/ (inklusive Punkt-vor-Strich-Regeln) und Klammersetzung, nach dem Motto "FIRST make it work, THEN make it fast" ;)

Ich hoff mal, ihr könnt mir bei dem Thema helfen :)


AXMD - Fr 05.08.05 22:26

Erste Anlaufstelle: die Suche in: Delphi-Forum, Delphi-Library FORENSUCHE. Einfach mal nach Suche in: Delphi-Forum, Delphi-Library PARSER suchen - gibt es einige hier im Forum.

AXMD


Benutzername - Fr 05.08.05 22:28

Die Forensuche hab ich schon oft verwendet, hat auch oftmals geholfen bei diversen anderen Problemen.
Allerdings fand ich eigentlich nur fertige Units für Parser. :?
Ich wollte aber mal wissen, wie man/wie ihr sowas angeht/angehen würdet ;)


LigH - Fr 05.08.05 22:52

Wohl eher gar nicht; schon die String-Zerlegung zu programmieren, wäre mir persönlich viel zu aufwändig! 8)

Das wichtigste ist, die Struktur zu erkennen - wo sind Operatoren, wo Variablen/Zahlen. Dann die Struktur nach Operatoren-Wertigkeit sortiert in einem Baum einbauen. Und dann (wenn sich der Baum fehlerfrei aufbauen lies) den Baum lösen.

Das alles selber zu machen, wäre "Eulen nach Athen tragen", oder "das Rad neu erfinden", oder ... - na, du verstehst: Wozu sich wochenlang den Kopf zerbrechen, das andere über Monate hinweg schon perfektioniert haben?

In den JEDI-Bibliotheken müsste bereits was enthalten sein:

http://sourceforge.net/projects/jcl + http://sourceforge.net/projects/jvcl


Benutzername - Fr 05.08.05 23:03

Öhm, Danke LigH :)
Aber das war eine der Antworten, die ich eher nicht hören wollte :mrgreen: ;)

Ich weiß, dass es vermutlich viel Arbeit sein wird ;)
Ich brauch den Parser ja auch nicht für ein Projekt, ich will das einfach nur ma gemacht haben ;)
Stichwort Lerneffekt ;)

Trotzdem danke, wenn ich mal nen Schnellen brauch, dann schau ich in die JCL :)


delfiphan - Sa 06.08.05 02:48

Das hab ich mir eines Tages auch gesagt und dabei ist die Unit tyParser.pas [http://www.tyberis.com/download/tyParser.pas] rausgekommen. :)

Eine Erklärung zur verwendeten Methode findest du hier [http://www.delphi-forum.de/viewtopic.php?p=219936#219936].

Jedoch basiert die neuste Version von tyParser auf anderen (aber ähnlichen) Prinzipien.
Die Unit tyParser enthält übrigens auch einen Compiler, welcher einen Stringausdruck in eine direkt ausführbare Funktion umwandelt.

Das ganze wurde gänzlich von Hand geschrieben.

PS: Im DF hat es übrigens auch noch eine Handvoll andere Parsers. Die kannst du dir ja auch mal anschauen :)


alcaeus - Sa 06.08.05 03:20

So, mein Tipp: regular Expressions. Ein parsen mit Hilfe von Pos(), Copy, Delete() waere hier wohl glatter Selbstmord. Allerdings habe ich im Moment keine RegExp-Unit fuer Delphi (Infos gibts hier [http://www.regular-expressions.info/delphi.html]) installiert, also gibt es nur Pseudo- oder php-Code. Da ich dir keinen fertigen Code geben will (:P) gibts Pseudo-Code. Erstens die Grundlagen:
  1. Der Parser akzeptiert Variablen, die auch mehr als ein Zeichen lang sein koennen. Die Variablen duerfen aber ausschliesslich aus Kleinbuchstaben (a-z) bestehen.
  2. Als Operationszeichen kennt der Parser die Grundrechenarten (+, -, *, /) sowie die Potenz (^)
  3. Geklammerte Ausdruecke sind erlaubt, solange die Klammern geschlossen werden ;)
  4. Ausser diesen Zeichen duerfen natuerlich noch Zahlen sowie Spaces zur Formatierung verwendet werden.
  5. Sollte was anderes auftreten, bricht der Parser einfach ab.


Hier noch die Referenz zu den einzelnen php-Funktionen die ich verwendet habe, ich denke dass es diese auch in den RegExp-Libs fuer Delphi gibt: preg_match_all() [http://de.php.net/preg_match_all], preg_match() [http://de.php.net/preg_match()], preg_replace() [http://de.php.net/preg_replace] und preg_split() [http://de.php.net/preg_split].

Bevor es losgeht, noch eine Kleinigkeit: ich erklaere hier nicht lange die Feinheiten der Regular Expressions. Diese duerft ihr selbst rausfinden :P ;)

So, der Parser besteht aus drei Funktionen. Die beiden ersten sind relativ einfach:


So, und nachdem das jetzt viel Theorie war, gibts erstmal Praxis. Gegeben sei folgender Ausdruck: 2+  a*(b^4 -2)- 6/3
a ist in dem Fall 5, b ist 2.
Man beachte die mehrfachen Leerzeichen, die ich reingebaut habe.


So, ich hab dir (Benutzername) doch gesagt, vor 3 gibts kein Ergebnis. Allerdings ist eine gute Stunde fuers Tippen des Beitrags draufgegangen. Deshalb: danke fuers Lesen. Ich hoffe ihr versteht meine Ausfuehrungen wenigstens ein bisschen, und bei Fragen: nur her damit. Und wer einen Blick auf den Code (php) werfen will, kann sich per PN an mich wenden, und vielleicht habe ich morgen (oder eher heute) Lust, das ganze noch in Delphi zu schreiben ;)

Greetz
alcaeus

Edit: Regexps fuer Reelle Zahlen angepasst


alcaeus - Sa 06.08.05 12:02

So, und nachdem dieses lange Posting so schoen war, gibts heute Abend noch eins. Ich hab soeben das Delphi-Beispiel fertiggestellt, hab aber im Moment keine Lust, es zu erklaeren. Das kommt heute um 2 Uhr nachts :lol:

Greetz
alcaeus


Benutzername - Sa 06.08.05 12:05

:lol:
Auf jeden Fall schonmal Danke ;)
Aber heut nacht um 2 is schon seit etwa 10 Stunden vorbei :P


jakobwenzel - Sa 06.08.05 12:12

In einem alten Turbo-Pascal Buch (so alt ist es auch noch nicht) haben sie einen Parser so programmiert, das die Funktion im Hauptprogramm eingegeben wird, die dann in einen Quelltext integriert wird, der kompiliert wird. Dieser überschreibt eine Dummy-Methode im Hauptprogramm. Nun kannn das Hauptprogramm die eingegebene Funktion nutzen, als hätte sie schon beim kompilieren im Quelltext gestanden.

P.S.:Wozu einen Mathe-Parser programmieren, wenn man schon einen im Kompiler eingebauten hat?

P.P.S.: eventuell geht das mit dem ersetzen der Dummy-Funktion nicht mehr unter Delphi. Dann müsste man die Ausgabe des Ergebnisses im dynamisch generierten Programm vornehemen.


AXMD - Sa 06.08.05 12:34

user profile iconjakobwenzel hat folgendes geschrieben:
In einem alten Turbo-Pascal Buch (so alt ist es auch noch nicht) haben sie einen Parser so programmiert, das die Funktion im Hauptprogramm eingegeben wird, die dann in einen Quelltext integriert wird, der kompiliert wird. Dieser überschreibt eine Dummy-Methode im Hauptprogramm. Nun kannn das Hauptprogramm die eingegebene Funktion nutzen, als hätte sie schon beim kompilieren im Quelltext gestanden.

P.S.:Wozu einen Mathe-Parser programmieren, wenn man schon einen im Kompiler eingebauten hat?

P.P.S.: eventuell geht das mit dem ersetzen der Dummy-Funktion nicht mehr unter Delphi. Dann müsste man die Ausgabe des Ergebnisses im dynamisch generierten Programm vornehemen.


Mich würde interessieren, wie der Quelltext dazu aussieht. Könntest du den uU mal posten?

AXMD


jakobwenzel - Sa 06.08.05 13:32

Die .bgi und .chr Dateien sind für den Grafikmodus. Funktion.exe/.pas ist das Hauptprogramm, hilfprg.exe/.pas das Hilfsprogramm. Das Programm braucht den Borland Pascal-Compiler TPC.exe im gleichen Verzeichnis. Da ich nicht weiß ob das Programm inzwischen Freeware ist, habe ich es weggelassen. Das Programm hat den Runtime-Error 200 Bug auf PC mit >150 MHZ Prozesor. (Hat ja inzwischen fast jeder so einen) Deshalb braucht man das Programm "PC-Bremse" (http://www.lrz-muenchen.de/~leoklein/downloads.html, 3. Programm von oben) benutzen. Ich musste bei meinem Rechner auf 75% gehen (1.5 GHZ).


jakobwenzel - Sa 06.08.05 13:34

Mist, Datei vergessen... Hier ist sie... :oops:

Moderiert von user profile iconraziel: 32 mal :oops: entfernt.


retnyg - Sa 06.08.05 13:40

user profile iconjakobwenzel hat folgendes geschrieben:
Das Programm hat den Runtime-Error 200 Bug auf PC mit >150 MHZ Prozesor. (Hat ja inzwischen fast jeder so einen) Deshalb braucht man das Programm "PC-Bremse" (http://www.lrz-muenchen.de/~leoklein/downloads.html, 3. Programm von oben) benutzen. Ich musste bei meinem Rechner auf 75% gehen (1.5 GHZ).

gibt auch einen patch für kompilierte programme: Suche bei Google CTBPPAT, ausserdem ein update für die crt.tpu, damit neue progs den fehler nicht mehr haben


jakobwenzel - Sa 06.08.05 13:59

Ich glaube, so einen Patch habe ich schon drauf, das Programm habe ich dann wohl auf meinem alten PC (150 MHZ) kompiliert. :lol:


retnyg - Sa 06.08.05 14:02

user profile iconjakobwenzel hat folgendes geschrieben:
Ich glaube, so einen Patch habe ich schon drauf, das Programm habe ich dann wohl auf meinem alten PC (150 MHZ) kompiliert. :lol:

der patch ctbppat patcht fertigt kompilierte programme. dadurch funktionieren diese auf jedem system einwandfrei.
der fix patcht gleich die verantwortlichen dateien in der TP installation, so dass der fehler in neu kompilierten .exe'n gar nicht drin ist.


jakobwenzel - Sa 06.08.05 14:20

Ich meinte das Update für die crt.tpu


Benutzername - Sa 06.08.05 15:10

Ich habs mal jetzt nicht runtergeladen, aber ich hab auch schonmal Überlegungen angestellt, dass man kompilietren Code "einfach" austauscht.
Aber das scheint mir nicht sehr sauber zu sein :?


jakobwenzel - Sa 06.08.05 15:25

Das Hilfsprogramm kopiert den Maschinencode der Funktion in einen der "unteren" Speicherbereiche, die, zumindest unter DOS (bzw. "Das 16-Bit Teilsystem" unter XP) jedes Programm auslesen kann. Das Hauptprogramm überschreibt seine Dummy-Funktion nur im Speicher mit dem neuen Code, der dann anstatt der Dummy-Funktion aufgerufen wird. Die Dummy-Funktion muss halt länger als die richtige Funktion sein, sonst werden andere Funktionen überschrieben.

Wahrscheinlich funktioniert ein Teil dessen, was unter DOS so wunderbar geht, unter Windows mit Delphi nicht mehr.


delfiphan - So 07.08.05 13:00

Der Vorschlag mit Regular Expressions ist nett, würde ich persönlich aber nicht empfehlen.

Normalerweise besteht ein Parser aus einer Komponente, die den Text in "Atome" (oder Tokens) zerlegt (Lexer) und einer Komponente, die die Tokens dann auswertet. (In meinem Parser ist das ganze zwar nicht so klar getrennt aber egal)

Der "Suchen und Ersetzen"-Ansatz ist meiner Meinung nach langsam, kompliziert (deswegen fehleranfällig) und nicht universell einsetzbar (man denke an Fälle wie: "1 - (-1) + 2e-1" (wobei 2e-1 = 0.2)).
Ausserdem sollte man meiner Meinung nach nicht am Ausdruck selbst arbeiten. Der Parser sollte sich den String anschauen und den genau so interpretieren, wie er da steht. Im Idealfall sollte sich der Parser einen Baum generieren, welcher er dann noch optimieren kann. Aus diesem Baum ist es dann relativ einfach möglich, einen Bytecode oder direkt Maschinencode zu generieren. Das beschleunigt die Auswertung ganz entscheidend.

Last but not least: Man sollte sich bei Parsern zuerst eine Grammatik definieren (z.B. in EBNF). Spätestens wenn du kompliziertere Sachen drin hast sind alle anderen Ansätze unmöglich zu implementieren. Mit EBNF kannst du ganze Programmiersprachen (Delphi, Java, etc.) definieren. Mit Regular Expressions kommst du da nicht sehr weit... Ausserdem würde ich bezweifeln, dass du es mit RegExp schaffst, eine halbwegs sinnvolle Fehlermeldung zu generieren, wenn was schief läuft.


Benutzername - So 07.08.05 16:07

Ja, so ganz begeistert war ich von den Regular Expressions auch nicht, aber wenns funktioniert... ;)
Du sagst, man sollte das mit nem Baum lösen: Gibts da schon ne fertige Klasse in Delphi, mit der man daslösen könnte?

Und noch ne andere Frage: Angenommen ich hab jetzt einfach nen Term wie 3+6*5*7. Wie mach ich dem Parser klar, dass er doch bitte Punkt-vorStrich beachten soll? Such ich da "einfach" nach dem ersten vorkommenden * oder / -Zeichen und schau dann, welche Zahlen drumrum stehen?

Vermute ich richtig, dass ich zuerst alle Klammern von innen nach außen ausrechnen muss?
Weil dann mach ich mich zuerst mal an ne Funktion, die mir den Term in der innersten Klammer raussucht :)


LigH - So 07.08.05 21:44

Es wird sicher darauf ankommen, welche Operanden du überhaupt zuläßt (unäre, binäre, Funktionen, Klammern, evtl. noch logische?) - "Punkt vor Strich" ist ja nur eine Ebene, ich kenne Parser mit mehr als einem halben Dutzend Operanden-Hierarchie-Ebenen.

Und da - wenn du einen Zweig des Baumes als Operand ansiehst, und ein Blatt als Operator - jeder Operanden-Zweig sowohl unterschiedlich viele Verzweigungen haben kann, als auch unterschiedliche Inhalte (entweder ein Operanden-Blatt, oder einen kompletten neuen Operatoren-Zweig), wäre mit Sicherheit eine eigene Klassenheirarchie sinnvoll, so etwas dürfte man kaum fertig vorfinden.


Benutzername - So 07.08.05 23:28

Hm, klingt recht kompliziert :? *g*
Ich werds mir bei Gelegenheit nochmal durch den Kopf gehen lassen^^

Aber ich hab nun einfach mal so drauflos geschrieben, und hab meinen Parser nun soweit, dass er * und / -Rechnungen richtig ausrechnet :)
Jetz fehlen "nurnoch" + und - Rechnungen und vielleicht Potenzen.
Und das unäre Minus muss ich noch einbauen, dass passt die Sache *g*


delfiphan - Mo 08.08.05 04:28

user profile iconBenutzername hat folgendes geschrieben:
Vermute ich richtig, dass ich zuerst alle Klammern von innen nach außen ausrechnen muss?

Es gibt Parsers, die arbeiten von aussen nach innen; es gibt welche, die arbeiten von innen nach aussen. Du kannst einen Parser (iirc) sogar mit einer Finite State Maschine programmieren (à la Windows-Calculator). Es gibt bestimmt mind. 5 komplett verschiedene Ansätze, wie man das programmieren kann.
Ich bevorzuge meine rekursive Variante (keine Finite State Maschine), die den String mehr oder weniger einmal von links nach rechts passiert. Die Rekursion ist dabei so gemacht, dass man direkt die richtige Reihenfolge der Operationen für die Umwandlung zu Maschinencode erhält.

Beispiel:
1+(2+3)*(4+5)

Der Parser bringt das ganze in folgende Reihenfolge:
1 2 3 + 4 5 + * +

Zum Auswerten geht man diese Liste von links nach rechts durch. Man muss dabei lediglich zwei Regeln beachten:
- Eine Zahl wird auf den Stack gepusht.
- Eine binäre Operationen "poppt" zwei Zahlen aus dem Stack, führt die Operation durch und pusht anschliessend wieder das Ergebnis.

Beispiel:
Zeitschritt  Token   Stack        Rechnung
0                    []
1            1       [1]
2            2       [1,2]
3            3       [1,2,3]
4            +       [1]          2+3
5                    [1,5]
6            4       [1,5,4]
7            5       [1,5,4,5]
8            +       [1,5]        4+5
9                    [1,5,9]
10           *       [1]          5*9
11                   [1,45]
12           +       []           1+45
13                   [46]

(46 ist das Ergebnis)

Mein Parser beschäftigt sich also vereinfacht gesprochen mit der Aufgabenstellung
1+(2+3)*(4+5)
in
1 2 3 + 4 5 + * +
umzuwandeln.


Benutzername - Mo 08.08.05 16:15

Ah, Danke für die Erklärung, Hintergrundwissen ist immer gut :)

Aber ich hab das jetz ohne die Postfix-Notation gemacht (so heißt die doch (bzw. UPN) ? *gg*) und auch ohne Stack.
Alles was jetzt noch fehlt, ist die Rechnung mit Potenzen und das mit den negativen Zahlen.
Wobei Potenzen schnell einzubauen sind *g*

Ich parse jetzt einfach die Klammern von innen nach außen, bis ich beim "Grundterm" angekommen bin, und löse das dann so auf, klappt auch wunderbar. Nicht-Ganzzahlen kann er auch schon, war auch kein Problem :)

Ich meld mich wieder, wenns Neuigkeiten und/oder Fragen gibt :stupid:


delfiphan - Mo 08.08.05 17:12

user profile iconBenutzername hat folgendes geschrieben:
Aber ich hab das jetz ohne die Postfix-Notation gemacht (so heißt die doch (bzw. UPN) ? *gg*) und auch ohne Stack.
Yep, Infix heisst die "normale" Schreibweise und Postfix die Notation mit den nachgestellten Operatoren (aka. RPN (UPN ist wohl Deutsch)).

user profile iconBenutzername hat folgendes geschrieben:
Ich meld mich wieder, wenns Neuigkeiten und/oder Fragen gibt :stupid:


Anschliessend einige Ausdrücke, die ich testen würde:

1-(-1) = 2 (Doppelnegation)
-2^2 = -4 (Hoch vor Punkt vor Strich)
(-2)^2 = 4 (ausser bei Klammerung...)
2^3^4 = mathematisch gesehen 2.4179e+024, bei Taschenrechnern (z.B. der Firma TI) z.T. aber auch 4096
1+(-(1+2)) = -2 (Negation einer Klammer)

je nachdem sollten auch folgende Ausdrücke akzeptiert werden:
1---2 = -3 (verschachtelte Minusoperationen)
1*-2 = -2 (Negation ohne explizite Klammerung)
1e+3^2 = 1000000 ("1e+3" ist eine korrekte Float-Zahl)
9^5e-1 = 3 (ditto)
2(3) = 6 (Standardoperator = "*")


LigH - Mo 08.08.05 19:06

UPN (Umgekehrte Polnische Notation) ist das, das mit dem Stack arbeitet - "Postfix-Notation". "Infix" ist die aus der Schule gewohnte Schreibweise.


Benutzername - Mi 10.08.05 17:13

So, er ist nun fertig :)

@delphifan: Danke für die Ausdrücke die ich testen sollte, klappt wunderbar :)

Aber zu deinem zweiten Beispiel hätt ich noch ne Frage:
Du sagtest ja, dass -2^2 = -4 ist, wegen Hoch vor Punkt vor Strich, hab ich auch verstanden :lol:
Aber angenommen, ich hätte --2^2. Wäre das dann das gleiche wie -(-2^2) oder wäre das dann -(-2)^2 ?
Da blick ich nämlich noch net ganz durch *g*

//edit: Noch was *g*
Gibts irgendwelche Vergleichswerte von der Geschwindigkeit her, wo ich in etwa liege?
Das wäre find ich ganz interessant zu wissen. :)


AXMD - Mi 10.08.05 17:22

--2^2 = 2^2 = 4

nur wenn du -(-2)^2 hast, erhältst du -4.

AXMD


Benutzername - Mi 10.08.05 17:27

Okay, Danke :)
Alles, was ich wissen wollte *g*

Dann bügel ich den Fehler noch schnell aus (war mir nämlich nicht sicher) und dann kann ich den Parser (mit Beispielprojekt) ja mal posten, wenns jemanden interessiert *g*


delfiphan - Mi 10.08.05 18:11

Ich teste ihn gern aus für dich. Ich hab sicher schon 5 verschiedene Parser auf Herz und Nieren geprüft ;)

Geschwindigkeitsmässig müsste man unterscheiden zwischen Parsen und Auswerten. Mein Parser und auch andere Parser parsen zuerst den Ausdruck und erzeugen daraus einen Bytecode, Maschinencode oder sonst eine schnell auswertbare Struktur (wie z.B. Bäume). So kann ein Ausdruck schnell mehrmals ausgewertet werden (z.B. zum Graphen zeichnen).

Die eigentliche Auswertung geht dann relativ schnell; das Parsen geht etwas länger.


Benutzername - Mi 10.08.05 18:20

Okay, dann wende ich mich mal per PN an delfiphan und wenn dann alles funktioniert, stell ich ihn hier rein :)


delfiphan - Mi 10.08.05 18:22

Die PN schreibst du am besten an delfiphan :) ;)
Bin ja schon gespannt...