FormelParser und ExpressionTree

    • VB.NET

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      FormelParser und ExpressionTree

      Bei meine Beschäftigung mittm Thema Expressionparser stieß ich auf ein paar hochinteressante Algorithmen, und auch paar listenreiche Klassen.
      Zunächst mal Expression-Beispiel:

      Quellcode

      1. Infix-Notation
      2. (3 + 2) * (15 / 3 - 8)
      Das ist ein Ausdruck in Infix-Schreibweise, und unter Berücksichtigung der Klammer-Regeln und der Vorrang-Regeln der Operatoren, kann man das ausrechnen, kommt -15 bei raus.
      Beim Rechnen geht man so vor, dass man Teil-Ausdrücke bildet, die jeweils mehrere Werte zusammenrechnen zu einem Wert. Dabei darf man nicht einfach von links nach rechts vorgehen, sondern muß die Operatoren mit höherem Vorrang vorrangig abarbeiten (deshalb heißt das auch "Operator-Vorrang" ;)).
      Ums ganz schlimm zu machen, wird durch Klammerung auch noch in die Vorränge eingegriffen, also dafür einen gscheiten Algorithmus zu erfinden ist Aufgabe für Genies.
      Nun gabs mal ein Genie in Polen, der hat eine klammerfreie Notation erfunden, also eine Schreibweise, wie man obigen Ausdruck auch ohne Klammern und Vorränge eindeutig formulieren kann. Als noch praktischer erwies sich, wenn man die Operatoren hinter die Operanden schreibt, und so sieht dann die "umgekehrt-polnische Notation" obiger Expression aus, alias "Postfix-Notation":

      Quellcode

      1. Postfix-Notation
      2. 3 2 + 15 3 / 8 - *
      Sieht völlig krank aus, aber Computer finden das klasse, denn in dieser Notation können sie den Ausdruck ganz einfach von links nach rechts abarbeiten, und kommt auch -15 bei raus.
      Also von links nach rechts: Wenns eine Zahl ist, dann legt man sie auf einen Zahlen-Stack. Wenns ein Operator ist, dann nimmt dieser sich seine Operanden von genau diesem Stack runter, berechnet damit einen Wert, und legt den wieder drauf:

      Quellcode

      1. Berechnung von (Postfix)
      2. 3 2 + 15 3 / 8 - *
      3. Schritt ResultStack
      4. 3 3
      5. 2 3 2
      6. + 5 + nimmt 3 2 runter, verrechnet, und gibt 5 wieder drauf
      7. 15 5 15
      8. 3 5 15 3
      9. / 5 5 / nimmt 15 3 runter, verrechnet, und gibt 5 wieder drauf
      10. 8 5 5 8
      11. - 5 -3 - nimmt 5 8 runter, verrechnet, und gibt -3 wieder drauf
      12. * -15 * nimmt 5 -3 runter, verrechnet, und gibt -15 wieder drauf
      Nicht wahr: bei einer Zahl nimmt der Stack um eins zu, bei einem binären Operator um eins ab, denn 2 Zahlen werden runtergenommen, und eine neue wieder drauf.
      Das ist nur bei binären Operatoren so, bei unären bleibt der Stack gleich, weil eine runter und eine drauf.
      Unärer Operator?? Naja, zB. die Negation, also das Minus-Zeichen vor einer Zahl:

      Quellcode

      1. 15 / -3 - 8
      Dassis postfix-mäßig schlecht mit dem Minus-Zeichen, weils dann nicht mehr eindeutig ist. Also auf "Postfix" notiere ich die Negation mit '!'.

      Quellcode

      1. Infix: 15 / !3 - 8
      2. Postfix: 15 3 ! / 8 -
      3. Schritt ResultStack
      4. 15 15
      5. 3 15 3
      6. ! 15 -3 ! nimmt nur die 3 runter, und gibt -3 wieder drauf
      7. / -5 verrechne 15 / -3
      8. 8 -5 8
      9. - -13 verrechne -5 - 8
      Gesehen? In Zeile#7 bleibt die Höhe des Stacks konstant, nur der letzte Wert wurde ausgetauscht durch seine Negation.

      Aber ich spinne noch weiter, denn wenns binäre und unäre Operatoren gibt, gibts dann auch nulläre? Also Operatoren, die gar keine Operanden brauchen, um einen Wert zu bilden?
      Klar gibts das - Zahlen zum Beispiel! Und wie man sieht: Zahlen verhalten sich genau nach Postfix-Rechenregel als nulläre Operatoren: Sie nehmen keinen Wert runter, und legen einen Wert drauf.
      Zahlen - genauer gesagt - Konstanten sind also nulläre Operatoren. :D
      Unds gibt noch mehr nulläre Operatoren, nämlich die Variablen - kommen wir noch zu. :D :D

      Zunächst aber der "Shunting-yard-Algorithmus" zur Konvertierung von Infix- in Postfix-Notation:
      Zu deutsch "Rangierbahnhof"-Algo, weils einen Operator-Stack gibt, in den jeder Operator erst mal (vorwärts) hineinfährt, und nach gewissen Regeln fährt er dann (rückwärts) wieder raus in die Ausgabe.
      Die operatorStack-Regel lautet: Bevor ein neuer Operator auf den Stack darf, werden alle Operatoren höheren oder gleichen Ranges vom Stack in die Ausgabe geflusht. Also rangniedere Operatoren verdrängen rang-gleiche und -höhere vom Stack in die Ausgabe. Somit ist der Stack auch sortiert: der höchste obenauf.
      Kleine Zwischenfrage: Nulläre Operatoren - welchen Rang haben die? Antwort: 99, also maximal. Dadurch verdrängt eine Zahl nur eine andere Zahl, aber keinen der anderen Operatoren.
      Und Klammern, was ist mit denen? Die kriegen Rang 0, und Sonderbehandlung. Rang 0 bewirkt, dass die öffnende Klammer von anneren Operatoren überhaupt nicht verdrängt werden kann.
      Die Sonderbehandlung besteht darin, dass trotz niedrigsten Ranges die öffnende Klammer auch ihrerseits keinen Operator vom Stack drängt.
      Schließende Klammer flusht dann den Stack in die Ausgabe, bis hin zur öffnende Klammer. Beide Klammern verfallen, also die öffnende wird runtergepoppt, und die schließende wird garnicht erst draufgelegt.

      Quellcode

      1. Infix->Postfix - Konvertierung
      2. (3 + 2) * (15 / 3 - 8)
      3. Schritt Op-Stack Postfix-Ausgabe
      4. ( (
      5. 3 ( 3
      6. + ( + 3 + verdrängt 3 vom Op-Stack in die Ausgabe
      7. 2 ( + 2 3
      8. ) 3 2 + ) Op-Stack + 2 -> Ausgabe 2 +
      9. * * 3 2 +
      10. ( * ( 3 2 +
      11. 15 * ( 15 3 2 +
      12. / * ( / 3 2 + 15 / Op-Stack 15 -> Ausgabe 15
      13. 3 * ( / 3 3 2 + 15
      14. - * ( - 3 2 + 15 3 / - Op-Stack / 3 -> Ausgabe 3 /
      15. 8 * ( - 8 3 2 + 15 3 /
      16. ) * 3 2 + 15 3 / 8 - ) Op-Stack - 8 -> Ausgabe 8 -
      17. <rest-flush> 3 2 + 15 3 / 8 - *
      Beachte, wenn mehrere Operatoren verdrängt werden (Zeilen #9, #15, #17), dass sie in umgekehrter Reihenfolge zur Ausgabe kommen.
      So sind Stapel nunmal: Was man als letztes drauflegt, bekommt man als erstes wieder runter. ;)

      Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Zunächstmal der "Shunting-yard-Algo" für Infix->Postfix:

      VB.NET-Quellcode

      1. Private Function Infix2Postfix(infixTokens As List(Of Token)) As List(Of Token)
      2. Dim operatorStack As New Stack(Of Token)
      3. Dim output = New List(Of Token)
      4. For Each tk In infixTokens
      5. Select Case tk.OpCode
      6. Case ")"c ' operatorStack bis ausschließlich "(" in die Ausgabe flushen, dann "(" und ")" verfallen lassen
      7. Do
      8. Dim op = operatorStack.Pop
      9. If op.OpCode = "("c Then Continue For ' "(" kommt nicht in den Output, und ")" kommt nicht auf den operatorStack
      10. output.Add(op)
      11. Loop
      12. Case "("c 'operatorStack nicht flushen
      13. Case Else 'operatorStack bis zum rangniederen Operator in die Ausgabe flushen
      14. While operatorStack.Count > 0 AndAlso operatorStack.Peek.IsHigherThan(tk)
      15. output.Add(operatorStack.Pop())
      16. End While
      17. End Select
      18. operatorStack.Push(tk) 'ausser ")" werden alle token gepusht
      19. Next
      20. output.AddRange(operatorStack) ' rest-flush
      21. Return output
      22. End Function
      Man erkennt vlt, dassich eine Klasse Token erfunden habe, die ihren eigenen Rang kennt (Token.IsHigherThan(otherToken)), und die einen Operator-Code hat (OpCode), um anzugeben, was eiglich gerechnet werden soll.
      OpCode kann noch mehr darstellen als die im Code ersichtlichen (), nämlich die Operatoren +-*/^! (wer hätte das gedacht?). Diese werden alle im Case Else verarbeitet.

      So, jetzt Ausrechnen einer Liste postfix-notierten Token:

      VB.NET-Quellcode

      1. Public Function Calculate(s As String) As Double
      2. 'jedes' Token Operator holt seine Operanden vom numbStack und pusht sein Ergebnis wieder drauf. Dabei ggfs. die umgedrehte Operanden-Reihenfolge berücksichtigen
      3. '"0"c-Token sind Zahlen, "!" ist der Negations-Postfix, die anneren Operatoren wie in Vb.
      4. Dim operators = New Dictionary(Of Char, Action(Of Stack(Of Double), Token)) From {
      5. {"0"c, Sub(stk, tk) stk.Push(tk.Number)},
      6. {"!"c, Sub(stk, tk) stk.Push(-stk.Pop)},
      7. {"^"c, Sub(stk, tk)
      8. Dim right = stk.Pop
      9. stk.Push(stk.Pop ^ right)
      10. End Sub},
      11. {"*"c, Sub(stk, tk) stk.Push(stk.Pop * stk.Pop)},
      12. {"/"c, Sub(stk, tk) stk.Push(1 / stk.Pop * stk.Pop)},
      13. {"+"c, Sub(stk, tk) stk.Push(stk.Pop + stk.Pop)},
      14. {"-"c, Sub(stk, tk) stk.Push(-stk.Pop + stk.Pop)}}
      15. Dim numbStack As New Stack(Of Double)
      16. For Each tk In Infix2Postfix(Tokenize(s))
      17. operators(tk.OpCode)(numbStack, tk)
      18. Next
      19. Return numbStack.Pop
      20. End Function
      Tja - zuächst mal ist da von Token garkeine Rede, sondern das komische da von #4 - #14 ist ein Einzeiler, der ein Dictionary(Of Char, Action(Of Stack(Of Double), Token)) initialisiert, also ein Dictionary, wo man für einen Char-Key eine Methode bekommt, die mit einem Token was greuliches auf einem Double-Stack anrichtet.
      Was diese Action(Of Stack, Token) jeweils anrichtet, ist hofflich zu erkennen, etwa bei OpCode "0"c (nullärer Konstanten-Operator) wird kein Wert vom Stack genommen, und ein Wert gepusht, nämlich die im Token gespeicherte Nummer.
      OpCode "!"c (unäre Negation) nimmt einen Wert runter, und tut dessen Negation wieder drauf.
      OpCode "^"c (binärer Potenz-Operator) nimmt 2 Werte: den ersten zwischenspeichert er als right, denn bei Potenzierung muß auf richtige Reihenfolge der Operatoren geachtet werden.
      Die restlichen OpCodes sind ebenfalls binär, wobei "/"c und "-"c auch ohne Zwischenspeicherung richtig rechnen trotz falscher Reihenfolge.
      Jo, bei so gründlicher Vorbereitung fällt die eigentliche Schleife natürlich absolut minimalistisch aus:

      VB.NET-Quellcode

      1. For Each tk In Infix2Postfix(Tokenize(s))
      2. operators(tk.OpCode)(numbStack, tk)
      3. Next
      Jedes Token weiß wasses zu tun hat, und tut es auch.

      Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Der Expression-Tree

      Neben der Postfix-Notation gibts eine weitere klammerfreie Darstellung für Expressions - den Expression-Tree:

      Dassis sogar viel intuitiver als die Postfix-Notation, man erkennt doch gleich, dass obiger Ausdruck aus 2 Teilausdrücken besteht, wovon der erste 2 Zahlen addiert, während der zweite Teilausdruck von einer Division (3. Teilausdruck) eine Zahl subtrahiert.
      Also wenn man eine schachtelbare Operator-Klasse hätte - das würde beim Ausrechnen selbst den Postfix-Algo alt aussehen lassen.
      Einem Root-Expression-Node weist du einfach an: "Ausrechnen!", und in einer lichtschnellen Kettenreaktion pflanzt sich diese Anweisung durch den Baum fort und das Ergebnis ist da.
      Da wird nichtmal eine Schleife benötigt, sondern die Addition rechnet einfach Operand1 + Operator2, die Subtraktion rechnet Operand1 - Operator2, und nicht anders machens Division und Multiplikation.
      Und dabei ists egal, ob die Operanden selbst wieder zusammengesetzt sind oder atomar - eine Variable oder eine Konstante ist ein Ausdruck wie jeder andere, nur atomar.

      Aber wie baut man son Bäumle auf?
      Naja, den Algo zeige ich vlt. am besten mit TreeNodes, die sind ja dazu da, um Baum-Strukturen zu bilden:

      VB.NET-Quellcode

      1. Private Function BuildTreeNodes(postfixTokens As List(Of Token)) As TreeNode
      2. 'Zwischen 0 und 2 Treenodes werden vom nodeStack gepoppt, und in den neuen TreeNode eingehängt (als Operand), welcher anschließend selbst auf den Stack gepusht wird
      3. Dim nodeStack As New Stack(Of TreeNode)
      4. For Each tk In postfixTokens
      5. Dim ndNew = New TreeNode(tk.OpCode) ' per default OpCode als Nodetext
      6. Select Case tk.OpCode
      7. Case "0"c
      8. ndNew.Text = tk.Number.ToString 'bei Zahlen die Zahl als Nodetext
      9. Case "!"c
      10. ndNew.Nodes.Add(nodeStack.Pop) 'unärer Operator "Negation" poppt einen Operanden runter
      11. Case "+"c, "-"c, "*"c, "/"c, "^"c
      12. ndNew.Nodes.Add(nodeStack.Pop)
      13. ndNew.Nodes.Insert(0, nodeStack.Pop) 'bei mehreren Operanden die umgekehrte Reihenfolge beachten
      14. Case Else 'bei Parametern bleibt nix zu tun - der OpCode ist bereits Nodetext
      15. End Select
      16. nodeStack.Push(ndNew)
      17. Next
      18. Return nodeStack.Pop
      19. End Function
      Prinzipiell dasselbe wie Postfix ausrechnen, nur statt eines Stack(Of Double) hat man einen Stack(Of TreeNode):
      Ein Operator nimmt mehrere Operanden vom Stack runter, und tut sein Ergebnis wieder drauf. Nur bei Treenodes ist das Ergebnis kein Double, sondern halt ein neuer Treenode, in den die vom Stack genommenen "Operanden"-Nodes eingehängt sind.
      Auch kann ich hier kein so listiges Dictionary vorbereiten, sondern führe eine traditionelle Fallunterscheidung durch. Dieses, weil ich nun auch Parameter unterstütze - da mag ich nicht für jeden denkbaren ParameterNamen einen Dictionary-Eintrag anlegen.

      Gut, das ist jetzt ein Treeview, und ein Treeview sieht auch gut aus - aber rechnen kannernich :S
      Also weiterhin schachtelbare Expression-Klasse gesucht - ach guggemal System.Linq.Expressions - du lieber Himmel - da fährt vlt. mal abgedrehtes Zeugs herum!

      Binary-, Block-, CatchBlock-, Constant-, Goto-, Index-, Loop-, Parameter- ... - da ist ja eine vollständige Programmiersprache abgebildet, nicht nur son bischen Arithmetik wie bei mir!
      Na, entsprechend wüst ist das Zeugs auch zu benutzen.
      Ich zeige erstmal nur, wie ich die Signatur designed habe:

      VB.NET-Quellcode

      1. Public Function CreateExpressionTree(s As String) As Func(Of Double, Double, Double, Double, Double)
      Also man gibt einen String hinein und bekommt fixnfertig eine Func(Of Double, Double, Double, Double, Double) zurück, also eine Function, die aus 4 Double-Argumenten ein Double-Ergebnis berechnet.
      Aufrufbeispiel:

      VB.NET-Quellcode

      1. Dim myFunc As Func(Of Double, Double, Double, Double, Double) = CreateExpressionTree("( a + b ) * ( c + a - 2 )")
      2. Dim result As Double = myFunc(1, 2, 3, 0)
      Da der übergebene String "( a + b ) * ( c + a - 2 )" hier nur 3 Parameter verwendet, ist zum Ausrechnen das vierte Argument beliebig - nur weglassen kannmans nicht.
      Wolle gugge TestApp?


      Ein weiteres ist noch vorrauszuschicken, nämlich Expressions werden nicht mit Schlüsselwort New gebildet, sondern die Expression-Klasse bietet haufenweise statische Generator-Funktionen an für Operatoren (und für endlos annere Sachen).
      Also eine Potenzierung bekommt man durch Aufruf von Expression.Power(xp1 As Expression, xp2 As Expression)
      eine Division durch Expression.Divide(xp1 As Expression, xp2 As Expression)
      eine Multiplikation durch Expression.Multiply(xp1 As Expression, xp2 As Expression)
      eine Negation durch Expression.Negate(xp1 As Expression) (unärer Operator)
      eine Konstante durch Expression.Constant(value, Typ) (nullär - verlangt kein Expression-Argument)
      usw.

      So, jetzt aber CreateExpressionTree() komplett:

      VB.NET-Quellcode

      1. Public Function CreateExpressionTree(s As String) As Func(Of Double, Double, Double, Double, Double)
      2. Dim tokens = Infix2Postfix(Tokenize(s))
      3. Dim stk As New Stack(Of Expression)
      4. Dim binaryExpressions = New Dictionary(Of Char, Func(Of Expression, Expression, Expression)) From { _
      5. {"^"c, AddressOf Expression.Power}, _
      6. {"/"c, AddressOf Expression.Divide}, _
      7. {"*"c, AddressOf Expression.Multiply}, _
      8. {"+"c, AddressOf Expression.Add}, _
      9. {"-"c, AddressOf Expression.Subtract}}
      10. Dim params = "abcd".ConvertAll(Function(c) Expression.Parameter(GetType(Double), name:=c)).ToDictionary(Function(p) p.Name)
      11. For Each tk In tokens
      12. Select Case tk.OpCode
      13. Case "!"c
      14. stk.Push(Expression.Negate(stk.Pop))
      15. Case "+"c, "-"c, "*"c, "/"c, "^"c
      16. Dim right = stk.Pop, left = stk.Pop
      17. stk.Push(binaryExpressions(tk.OpCode)(left, right))
      18. Case "0"c
      19. stk.Push(Expression.Constant(tk.Number, GetType(Double)))
      20. Case Else
      21. stk.Push(params(tk.OpCode))
      22. End Select
      23. Next
      24. With params.Values.ToArray.GetEnumeratorX
      25. Return Expression.Lambda(Of Func(Of Double, Double, Double, Double, Double))(stk.Pop, .Yield, .Yield, .Yield, .Yield).Compile
      26. End With
      27. End Function
      Prinzipiell dasselbe wie mitte Treenodes, nur statt Stack(Of Treenode) isses jetzt ein Stack(Of Expression), der nach Postfix-Rechenregel "ausgerechnet" wird.
      Ich kann hier auch wieder eine Dictionary-Vorbereitung treffen, sodass ich im Select in der Schleife alle binären Operatoren über einen Kamm scheren kann.

      Es gibt sogar ein zweites Dictionary, nämlich noch eines für die Parameter-Expressions:

      VB.NET-Quellcode

      1. Dim params = "abcd".ConvertAll(Function(c) Expression.Parameter(GetType(Double), name:=c)) _
      2. .ToDictionary(Function(p) p.Name)
      Ich konvertiere alle Zeichen von "abcd" je in eine ParameterExpression dieses Namens. Daraus bau ich gleich ein Dictionary, welches als Key den Parameter-Namen verwendet.
      So, dann also die Schleife abfahren, und am Schluss bleibt eine Expression auffm Stack liegen, und da sind alle Sub-Expressions drin eingeschachtelt.
      Hieraus (#25) wird eine (kompilierbare) Lambda-Expression generiert, wobei ich die ParameterExpressions nochmal der Reihe nach angeben muß, damit der Lambda die richtige Argument-Reihenfolge generiert.
      Das noch .Compilen, und raus kommt der richtige Methoden-Rückgabewert vom Typ Func(Of Double, Double, Double, Double, Double), also eine anonyme Function, die auch tatsächlich so schnell rechnet, wie anonyme Methoden in .Net halt sind.
      :D
      Dateien
      • FormelRechner.zip

        (63,24 kB, 335 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von „ErfinderDesRades“ ()