Gleichungs- (Ausdrucks-) Parser mit Vorrang?


103

Ich habe einen Gleichungsparser entwickelt, der einen einfachen Stapelalgorithmus verwendet, der binäre (+, -, |, &, *, / usw.) Operatoren, unäre (!) Operatoren und Klammern verarbeitet.

Wenn ich diese Methode verwende, habe ich jedoch alles mit der gleichen Priorität - sie wird unabhängig vom Operator von links nach rechts ausgewertet, obwohl die Priorität in Klammern erzwungen werden kann.

Im Moment gibt "1 + 11 * 5" also 60 zurück, nicht 56, wie man erwarten könnte.

Während dies für das aktuelle Projekt geeignet ist, möchte ich eine Allzweckroutine haben, die ich für spätere Projekte verwenden kann.

Aus Gründen der Übersichtlichkeit bearbeitet:

Was ist ein guter Algorithmus zum Parsen von Gleichungen mit Vorrang?

Ich interessiere mich für etwas Einfaches zu implementieren und zu verstehen, dass ich mich selbst codieren kann, um Lizenzprobleme mit verfügbarem Code zu vermeiden.

Grammatik:

Ich verstehe die Grammatikfrage nicht - ich habe sie von Hand geschrieben. Es ist einfach genug, dass ich keine Notwendigkeit für YACC oder Bison sehe. Ich muss lediglich Zeichenfolgen mit Gleichungen wie "2 + 3 * (42/13)" berechnen.

Sprache:

Ich mache das in C, aber ich interessiere mich für einen Algorithmus, nicht für eine sprachspezifische Lösung. C ist so niedrig, dass es bei Bedarf leicht in eine andere Sprache konvertiert werden kann.

Codebeispiel

Ich habe den Testcode für den Parser für einfache Ausdrücke gepostet, über den ich oben gesprochen habe. Die Projektanforderungen haben sich geändert, sodass ich den Code nie für Leistung oder Speicherplatz optimieren musste, da er nicht in das Projekt integriert war. Es ist in der ursprünglichen ausführlichen Form und sollte leicht verständlich sein. Wenn ich in Bezug auf die Operatorrangfolge etwas weiter damit mache, werde ich wahrscheinlich den Makro-Hack wählen, da er der Einfachheit halber mit dem Rest des Programms übereinstimmt. Wenn ich dies jedoch jemals in einem realen Projekt verwende, werde ich mich für einen kompakteren / schnelleren Parser entscheiden.

Verwandte Frage

Intelligentes Design eines Mathe-Parsers?

-Adam


Ich habe einen Ausdrucksparser in C # in meinem Blog geschrieben. Es wird ein Infix zum Postfixieren ohne den Stapel im Rangierplatzalgorithmus durchgeführt. Es wird nur ein Array verwendet.
Guge

Soweit ich weiß, müssen Sie nur den arithmetischen Ausdruck analysieren. Verwenden Sie Reverse Polish Notation
Mishadoff

Antworten:


69

Der harte Weg

Sie möchten einen Parser für rekursiven Abstieg .

Um Vorrang zu haben, müssen Sie rekursiv denken, z. B. mithilfe Ihrer Beispielzeichenfolge.

1+11*5

Um dies manuell zu tun, müssten Sie das lesen 1, dann das Plus sehen und eine ganz neue rekursive Analyse "Sitzung" starten, beginnend mit 11... und sicherstellen, dass Sie das 11 * 5in seinen eigenen Faktor analysieren, um einen Analysebaum mit zu erhalten 1 + (11 * 5).

Dies alles fühlt sich so schmerzhaft an, selbst zu versuchen, es zu erklären, insbesondere mit der zusätzlichen Ohnmacht von C. Sehen Sie, nach dem Parsen der 11, wenn das * tatsächlich ein + wäre, müssten Sie den Versuch, einen Begriff zu bilden, abbrechen und stattdessen das analysieren 11selbst als Faktor. Mein Kopf explodiert bereits. Es ist mit der rekursiven anständigen Strategie möglich, aber es gibt einen besseren Weg ...

Der einfache (richtige) Weg

Wenn Sie ein GPL-Tool wie Bison verwenden, müssen Sie sich wahrscheinlich keine Gedanken über Lizenzprobleme machen, da der von Bison generierte C-Code nicht von der GPL abgedeckt wird (IANAL, aber ich bin mir ziemlich sicher, dass GPL-Tools die GPL nicht erzwingen generierter Code / Binärdateien (zum Beispiel Apple kompiliert Code wie beispielsweise Aperture mit GCC und sie verkaufen ihn, ohne den Code GPL zu müssen).

Laden Sie Bison herunter (oder etwas Ähnliches, ANTLR usw.).

Normalerweise gibt es einen Beispielcode, mit dem Sie Bison einfach ausführen und den gewünschten C-Code erhalten können, der diesen Rechner mit vier Funktionen demonstriert:

http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html

Schauen Sie sich den generierten Code an und stellen Sie fest, dass dies nicht so einfach ist, wie es sich anhört. Die Vorteile eines Tools wie Bison sind außerdem: 1) Sie lernen etwas (insbesondere, wenn Sie das Drachenbuch lesen und etwas über Grammatiken lernen). 2) Sie vermeiden, dass NIH versucht, das Rad neu zu erfinden. Mit einem echten Parser-Generator-Tool können Sie später skalieren und anderen Personen zeigen, dass Parser die Domäne von Parsing-Tools sind.


Aktualisieren:

Die Leute hier haben viele gute Ratschläge gegeben. Meine einzige Warnung vor dem Überspringen der Parsing-Tools oder der Verwendung des Shunting Yard-Algorithmus oder eines handgerollten rekursiven anständigen Parsers ist, dass kleine Spielzeugsprachen 1 eines Tages zu großen tatsächlichen Sprachen mit Funktionen (sin, cos, log) und Variablen, Bedingungen und für werden können Schleifen.

Flex / Bison kann für einen kleinen, einfachen Interpreter durchaus übertrieben sein, aber ein einmaliger Parser + Evaluator kann später zu Problemen führen, wenn Änderungen vorgenommen oder Funktionen hinzugefügt werden müssen. Ihre Situation wird unterschiedlich sein und Sie müssen Ihr Urteilsvermögen einsetzen. Bestrafe einfach keine anderen Menschen für deine Sünden [2] und baue ein weniger als angemessenes Werkzeug.

Mein Lieblingswerkzeug zum Parsen

Das beste Werkzeug der Welt für diesen Job ist die Parsec- Bibliothek (für rekursive anständige Parser), die mit der Programmiersprache Haskell geliefert wird. Es ähnelt stark BNF oder einem speziellen Tool oder einer domänenspezifischen Sprache zum Parsen (Beispielcode [3]), ist jedoch nur eine reguläre Bibliothek in Haskell, was bedeutet, dass es im selben Erstellungsschritt wie die anderen kompiliert wird Sie können beliebigen Haskell-Code schreiben und diesen in Ihrem Parser aufrufen, und Sie können andere Bibliotheken im selben Code mischen und abgleichen . (Das Einbetten einer Parsing-Sprache wie dieser in eine andere Sprache als Haskell führt übrigens zu einer Menge syntaktischer Cruft. Ich habe dies in C # gemacht und es funktioniert ganz gut, aber es ist nicht so hübsch und prägnant.)

Anmerkungen:

1 Richard Stallman sagt in Warum Sie Tcl nicht verwenden sollten

Die wichtigste Lehre von Emacs ist, dass eine Sprache für Erweiterungen keine bloße "Erweiterungssprache" sein sollte. Es sollte eine echte Programmiersprache sein, die zum Schreiben und Verwalten umfangreicher Programme entwickelt wurde. Weil die Leute das wollen werden!

[2] Ja, ich habe für immer Angst davor, diese "Sprache" zu benutzen.

Beachten Sie auch , dass , wenn ich diesen Eintrag vorgelegt, die Vorschau war richtig, aber SO ist weniger als ausreichend Parser meiner Nähe Anker - Tag auf dem ersten Absatz aß , was beweist , dass Parser sind nicht etwas zu spaßen , denn wenn Sie reguläre Ausdrücke verwenden und eine off - Hacks Sie wird wahrscheinlich etwas subtiles und kleines falsch machen .

[3] Ausschnitt eines Haskell-Parsers mit Parsec: Ein Vierfunktionsrechner, der um Exponenten, Klammern, Leerzeichen für die Multiplikation und Konstanten (wie pi und e) erweitert wurde.

aexpr   =   expr `chainl1` toOp
expr    =   optChainl1 term addop (toScalar 0)
term    =   factor `chainl1` mulop
factor  =   sexpr  `chainr1` powop
sexpr   =   parens aexpr
        <|> scalar
        <|> ident

powop   =   sym "^" >>= return . (B Pow)
        <|> sym "^-" >>= return . (\x y -> B Pow x (B Sub (toScalar 0) y))

toOp    =   sym "->" >>= return . (B To)

mulop   =   sym "*" >>= return . (B Mul)
        <|> sym "/" >>= return . (B Div)
        <|> sym "%" >>= return . (B Mod)
        <|>             return . (B Mul)

addop   =   sym "+" >>= return . (B Add) 
        <|> sym "-" >>= return . (B Sub)

scalar = number >>= return . toScalar

ident  = literal >>= return . Lit

parens p = do
             lparen
             result <- p
             rparen
             return result

9
Um meinen Standpunkt zu betonen, beachten Sie, dass das Markup in meinem Beitrag nicht korrekt analysiert wird (und dies variiert zwischen dem statisch gerenderten Markup und dem in der WMD-Vorschau gerenderten Markup). Es gab mehrere Versuche, das Problem zu beheben, aber ich denke, DER PARSER IST FALSCH. Tun Sie allen einen Gefallen und machen Sie das Parsen richtig!
Jared Updike

155

Der Rangierbahnhof-Algorithmus ist dafür das richtige Werkzeug. Wikipedia ist diesbezüglich wirklich verwirrend, aber im Grunde funktioniert der Algorithmus folgendermaßen:

Angenommen, Sie möchten 1 + 2 * 3 + 4 bewerten. Intuitiv "wissen" Sie, dass Sie zuerst 2 * 3 ausführen müssen, aber wie erhalten Sie dieses Ergebnis? Der Schlüssel besteht darin, zu erkennen, dass Sie beim Scannen der Zeichenfolge von links nach rechts einen Operator auswerten, wenn der darauf folgende Operator eine niedrigere (oder gleichwertige) Priorität hat. Im Kontext des Beispiels möchten Sie Folgendes tun:

  1. Schau dir an: 1 + 2, mach nichts.
  2. Schauen Sie sich jetzt 1 + 2 * 3 an und tun Sie immer noch nichts.
  3. Schauen Sie sich nun 1 + 2 * 3 + 4 an, jetzt wissen Sie, dass 2 * 3 ausgewertet werden muss, da der nächste Operator eine niedrigere Priorität hat.

Wie setzen Sie das um?

Sie möchten zwei Stapel haben, einen für Zahlen und einen für Operatoren. Sie schieben die ganze Zeit Zahlen auf den Stapel. Sie vergleichen jeden neuen Operator mit dem am oberen Rand des Stapels. Wenn der am oberen Rand des Stapels eine höhere Priorität hat, entfernen Sie ihn vom Operatorstapel, entfernen die Operanden vom Zahlenstapel, wenden den Operator an und übertragen das Ergebnis auf den Zahlenstapel. Jetzt wiederholen Sie den Vergleich mit dem Top-of-Stack-Operator.

Zurück zum Beispiel: Es funktioniert folgendermaßen:

N = [] Ops = []

  • Lesen Sie 1. N = [1], Ops = []
  • Lesen Sie +. N = [1], Ops = [+]
  • Lesen Sie 2. N = [1 2], Ops = [+]
  • Lesen Sie *. N = [1 2], Ops = [+ *]
  • Lesen Sie 3. N = [1 2 3], Ops = [+ *]
  • Lesen Sie +. N = [1 2 3], Ops = [+ *]
    • Pop 3, 2 und führe 2 *3 aus und drücke das Ergebnis auf N. N = [1 6], Ops = [+]
    • +bleibt assoziativ, daher möchten Sie auch 1, 6 ausschalten und das + ausführen. N = [7], Ops = [].
    • Schieben Sie zum Schluss das [+] auf den Bedienerstapel. N = [7], Ops = [+].
  • Lesen Sie 4. N = [7 4]. Ops = [+].
  • Sie haben keine Eingabe mehr und möchten jetzt die Stapel leeren. Daraufhin erhalten Sie das Ergebnis 11.

Dort ist das nicht so schwierig, oder? Und es werden keine Grammatiken oder Parser-Generatoren aufgerufen.


6
Sie brauchen eigentlich keine zwei Stapel, solange Sie das zweite auf dem Stapel sehen können, ohne die Oberseite zu öffnen. Sie können stattdessen einen einzelnen Stapel verwenden, der Zahlen und Operatoren abwechselt. Dies entspricht genau dem, was ein LR-Parser-Generator (wie z. B. Bison) tut.
Chris Dodd

2
Wirklich schöne Erklärung des Algorithmus, den ich gerade implementiert habe. Außerdem konvertieren Sie es nicht in Postfix, was auch schön ist. Das Hinzufügen von Unterstützung für Klammern ist ebenfalls sehr einfach.
Giorgi

4
Eine vereinfachte Version für den Shunt -Yard-Algorithmus finden Sie hier: andreinc.net/2010/10/05/… (mit Implementierungen in Java und Python)
Andrei Ciobanu

1
Danke dafür, genau das, wonach ich suche!
Joe Green

Vielen Dank für die Erwähnung von left - assoziative. Ich blieb beim ternären Operator: Wie analysiere ich komplexe Ausdrücke mit verschachteltem "?:". Mir wurde klar, dass beide '?' und ':' müssen die gleiche Priorität haben. Und wenn wir '?' als rechtsassoziativ und ':' als linksassoziativ funktioniert dieser Algorithmus sehr gut mit ihnen. Außerdem können wir 2 Operatoren nur reduzieren, wenn beide übrig bleiben - assoziativ.
Vladislav

25

http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm

Sehr gute Erklärung verschiedener Ansätze:

  • Rekursive Abstiegserkennung
  • Der Rangierbahnhof-Algorithmus
  • Die klassische Lösung
  • Vorrang klettern

Geschrieben in einfacher Sprache und Pseudocode.

Ich mag 'Priority Climbing'.


Die Verbindung scheint unterbrochen zu sein. Eine bessere Antwort wäre gewesen, jede Methode so zu paraphrasieren, dass beim Verschwinden dieses Links einige dieser nützlichen Informationen hier erhalten geblieben wären.
Adam White

18

Es gibt einen schönen Artikel hier über einen einfachen rekursiven Abstieg Parser mit Operator-Vorrang - Analyse kombiniert. Wenn Sie kürzlich Parser geschrieben haben, sollte das Lesen sehr interessant und lehrreich sein.


16

Vor langer Zeit habe ich meinen eigenen Parsing-Algorithmus entwickelt, den ich in keinem Parsing-Buch (wie dem Dragon Book) finden konnte. Wenn ich mir die Zeiger auf den Shunting Yard-Algorithmus ansehe, sehe ich die Ähnlichkeit.

Vor ungefähr 2 Jahren habe ich auf http://www.perlmonks.org/?node_id=554516 einen Beitrag darüber mit Perl-Quellcode verfasst . Es ist einfach, in andere Sprachen zu portieren: Die erste Implementierung, die ich durchgeführt habe, war in Z80 Assembler.

Es ist ideal für die direkte Berechnung mit Zahlen, aber Sie können es verwenden, um einen Analysebaum zu erstellen, wenn Sie müssen.

Update Da mehr Benutzer Javascript lesen (oder ausführen) können, habe ich meinen Parser in Javascript neu implementiert, nachdem der Code neu organisiert wurde. Der gesamte Parser enthält weniger als 5.000 KB Javascript-Code (ca. 100 Zeilen für den Parser, 15 Zeilen für eine Wrapper-Funktion), einschließlich Fehlerberichterstattung und Kommentaren.

Eine Live-Demo finden Sie unter http://users.telenet.be/bartl/expressionParser/expressionParser.html .

// operator table
var ops = {
   '+'  : {op: '+', precedence: 10, assoc: 'L', exec: function(l,r) { return l+r; } },
   '-'  : {op: '-', precedence: 10, assoc: 'L', exec: function(l,r) { return l-r; } },
   '*'  : {op: '*', precedence: 20, assoc: 'L', exec: function(l,r) { return l*r; } },
   '/'  : {op: '/', precedence: 20, assoc: 'L', exec: function(l,r) { return l/r; } },
   '**' : {op: '**', precedence: 30, assoc: 'R', exec: function(l,r) { return Math.pow(l,r); } }
};

// constants or variables
var vars = { e: Math.exp(1), pi: Math.atan2(1,1)*4 };

// input for parsing
// var r = { string: '123.45+33*8', offset: 0 };
// r is passed by reference: any change in r.offset is returned to the caller
// functions return the parsed/calculated value
function parseVal(r) {
    var startOffset = r.offset;
    var value;
    var m;
    // floating point number
    // example of parsing ("lexing") without aid of regular expressions
    value = 0;
    while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    if(r.string.substr(r.offset, 1) == ".") {
        r.offset++;
        while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    }
    if(r.offset > startOffset) {  // did that work?
        // OK, so I'm lazy...
        return parseFloat(r.string.substr(startOffset, r.offset-startOffset));
    } else if(r.string.substr(r.offset, 1) == "+") {  // unary plus
        r.offset++;
        return parseVal(r);
    } else if(r.string.substr(r.offset, 1) == "-") {  // unary minus
        r.offset++;
        return negate(parseVal(r));
    } else if(r.string.substr(r.offset, 1) == "(") {  // expression in parens
        r.offset++;   // eat "("
        value = parseExpr(r);
        if(r.string.substr(r.offset, 1) == ")") {
            r.offset++;
            return value;
        }
        r.error = "Parsing error: ')' expected";
        throw 'parseError';
    } else if(m = /^[a-z_][a-z0-9_]*/i.exec(r.string.substr(r.offset))) {  // variable/constant name        
        // sorry for the regular expression, but I'm too lazy to manually build a varname lexer
        var name = m[0];  // matched string
        r.offset += name.length;
        if(name in vars) return vars[name];  // I know that thing!
        r.error = "Semantic error: unknown variable '" + name + "'";
        throw 'unknownVar';        
    } else {
        if(r.string.length == r.offset) {
            r.error = 'Parsing error at end of string: value expected';
            throw 'valueMissing';
        } else  {
            r.error = "Parsing error: unrecognized value";
            throw 'valueNotParsed';
        }
    }
}

function negate (value) {
    return -value;
}

function parseOp(r) {
    if(r.string.substr(r.offset,2) == '**') {
        r.offset += 2;
        return ops['**'];
    }
    if("+-*/".indexOf(r.string.substr(r.offset,1)) >= 0)
        return ops[r.string.substr(r.offset++, 1)];
    return null;
}

function parseExpr(r) {
    var stack = [{precedence: 0, assoc: 'L'}];
    var op;
    var value = parseVal(r);  // first value on the left
    for(;;){
        op = parseOp(r) || {precedence: 0, assoc: 'L'}; 
        while(op.precedence < stack[stack.length-1].precedence ||
              (op.precedence == stack[stack.length-1].precedence && op.assoc == 'L')) {  
            // precedence op is too low, calculate with what we've got on the left, first
            var tos = stack.pop();
            if(!tos.exec) return value;  // end  reached
            // do the calculation ("reduce"), producing a new value
            value = tos.exec(tos.value, value);
        }
        // store on stack and continue parsing ("shift")
        stack.push({op: op.op, precedence: op.precedence, assoc: op.assoc, exec: op.exec, value: value});
        value = parseVal(r);  // value on the right
    }
}

function parse (string) {   // wrapper
    var r = {string: string, offset: 0};
    try {
        var value = parseExpr(r);
        if(r.offset < r.string.length){
          r.error = 'Syntax error: junk found at offset ' + r.offset;
            throw 'trailingJunk';
        }
        return value;
    } catch(e) {
        alert(r.error + ' (' + e + '):\n' + r.string.substr(0, r.offset) + '<*>' + r.string.substr(r.offset));
        return;
    }    
}

11

Es wäre hilfreich, wenn Sie die Grammatik beschreiben könnten, die Sie derzeit zum Parsen verwenden. Klingt so, als ob das Problem dort liegen könnte!

Bearbeiten:

Die Tatsache, dass Sie die Grammatikfrage nicht verstehen und dass Sie dies von Hand geschrieben haben, erklärt sehr wahrscheinlich, warum Sie Probleme mit Ausdrücken der Form '1 + 11 * 5' haben (dh mit Operator-Vorrang). . Wenn Sie beispielsweise nach "Grammatik für arithmetische Ausdrücke" googeln, sollten Sie einige gute Hinweise erhalten. Eine solche Grammatik muss nicht kompliziert sein:

<Exp> ::= <Exp> + <Term> |
          <Exp> - <Term> |
          <Term>

<Term> ::= <Term> * <Factor> |
           <Term> / <Factor> |
           <Factor>

<Factor> ::= x | y | ... |
             ( <Exp> ) |
             - <Factor> |
             <Number>

würde zum Beispiel den Trick machen und kann trivial erweitert werden, um sich um einige kompliziertere Ausdrücke zu kümmern (einschließlich Funktionen zum Beispiel oder Potenzen, ...).

Ich schlage vor, Sie sehen sich zum Beispiel diesen Thread an.

Fast alle Einführungen in Grammatiken / Parsing behandeln arithmetische Ausdrücke als Beispiel.

Beachten Sie, dass die Verwendung einer Grammatik keinesfalls die Verwendung eines bestimmten Werkzeugs ( a la Yacc, Bison, ...) bedeutet. In der Tat verwenden Sie mit Sicherheit bereits die folgende Grammatik:

<Exp>  :: <Leaf> | <Exp> <Op> <Leaf>

<Op>   :: + | - | * | /

<Leaf> :: <Number> | (<Exp>)

(oder so etwas) ohne es zu wissen!


8

Haben Sie darüber nachgedacht, Boost Spirit zu verwenden ? Es ermöglicht Ihnen, EBNF-ähnliche Grammatiken in C ++ wie folgt zu schreiben:

group       = '(' >> expression >> ')';
factor      = integer | group;
term        = factor >> *(('*' >> factor) | ('/' >> factor));
expression  = term >> *(('+' >> term) | ('-' >> term));

1
+1 Und das Ergebnis ist, dass alles Teil von Boost ist. Die Grammatik für den Taschenrechner finden Sie hier: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Die Implementierung des Rechners ist hier: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Die Dokumentation finden Sie hier: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . Ich werde nie verstehen, warum die Leute dort immer noch eigene Mini-Parser implementieren.
stephan

5

Wenn Sie Ihre Frage stellen, besteht keinerlei Rekursionsbedarf. Die Antwort lautet drei Dinge: Postfix-Notation plus Shunting Yard-Algorithmus plus Bewertung des Postfix-Ausdrucks:

1). Postfix-Notation = erfunden, um die Notwendigkeit einer expliziten Prioritätsspezifikation zu beseitigen. Lesen Sie mehr im Internet, aber hier ist das Wesentliche: Infix-Ausdruck (1 + 2) * 3, während es für Menschen leicht zu lesen und zu verarbeiten ist, was für die maschinelle Berechnung nicht sehr effizient ist. Was ist? Einfache Regel, die besagt, dass "Ausdruck durch Caching vorrangig neu geschrieben und dann immer von links nach rechts verarbeitet wird". Das Infix (1 + 2) * 3 wird also zu einem Postfix 12 + 3 *. POST, da der Operator immer NACH den Operanden steht.

2). Postfix-Ausdruck auswerten. Einfach. Lesen Sie die Zahlen aus der Postfix-Zeichenfolge. Schieben Sie sie auf einen Stapel, bis ein Bediener gesehen wird. Bedienertyp prüfen - unär? binär? Tertiär? Pop so viele Operanden vom Stapel wie nötig, um diesen Operator auszuwerten. Bewerten. Ergebnis wieder auf Stapel schieben! Und du bist fast fertig. Machen Sie so weiter, bis der Stapel nur noch einen Eintrag = Wert hat, den Sie suchen.

Machen wir (1 + 2) * 3, was im Postfix steht, ist "12 + 3 *". Lesen Sie die erste Zahl = 1. Schieben Sie sie auf den Stapel. Lesen Sie weiter. Nummer = 2. Schieben Sie es auf den Stapel. Lesen Sie weiter. Operator. Welcher? +. Welche Art? Binär = benötigt zwei Operanden. Pop-Stack zweimal = Argright ist 2 und Argleft ist 1. 1 + 2 ist 3. Schieben Sie 3 zurück auf den Stack. Lesen Sie als nächstes aus der Postfix-Zeichenfolge. Es ist eine Nummer. 3.Push. Lesen Sie weiter. Operator. Welcher? *. Welche Art? Binär = benötigt zwei Zahlen -> Pop-Stack zweimal. Erst in Argright, zweitens in Argleft. Betrieb auswerten - 3 mal 3 ist 9.Drücken Sie 9 auf den Stapel. Lesen Sie das nächste Postfix-Zeichen. Es ist null. Ende der Eingabe. Pop Stack onec = das ist deine Antwort.

3). Shunting Yard wird verwendet, um menschliche (leicht) lesbare Infix-Ausdrücke in Postfix-Ausdrücke umzuwandeln (auch menschliche, nach einiger Übung leicht lesbare). Einfach manuell zu codieren. Siehe Kommentare oben und net.


4

Gibt es eine Sprache, die Sie verwenden möchten? Mit ANTLR können Sie dies aus Java-Sicht tun. Adrian Kuhn hat eine ausgezeichnete Beschreibung, wie man eine ausführbare Grammatik in Ruby schreibt. Tatsächlich ist sein Beispiel fast genau Ihr Beispiel für einen arithmetischen Ausdruck.


Ich muss zugeben, dass meine Beispiele im Blog-Beitrag eine falsche Linksrekursion aufweisen, dh a - b - c ergibt (a - (b - c)) anstelle von ((a - b) - c). Eigentlich erinnert mich das daran, dass ich eine Aufgabe hinzugefügt habe, um die Blog-Beiträge zu reparieren.
Akuhn

4

Es hängt davon ab, wie "allgemein" es sein soll.

Wenn Sie möchten, dass es wirklich sehr allgemein ist, z. B. mathematische Funktionen wie sin (4 + 5) * cos (7 ^ 3) analysieren können, benötigen Sie wahrscheinlich einen Analysebaum.

In dem ich nicht denke, dass eine vollständige Implementierung angemessen ist, um hier eingefügt zu werden. Ich würde vorschlagen, dass Sie sich eines der berüchtigten " Drachenbücher " ansehen .

Wenn Sie jedoch nur Vorrangunterstützung wünschen , können Sie dies tun, indem Sie zuerst den Ausdruck in ein Postfix-Formular konvertieren, in dem ein Algorithmus zum Kopieren und Einfügen von Google verfügbar sein sollte, oder ich denke, Sie können ihn selbst mit einer Binärdatei codieren Baum.

Wenn Sie es in Postfix-Form haben, ist es von da an ein Kinderspiel, da Sie bereits verstehen, wie der Stapel hilft.


Das Drachenbuch ist für einen Ausdrucksbewerter möglicherweise etwas übertrieben - ein einfacher Parser für rekursiven Abstieg ist alles, was benötigt wird, aber es ist ein Muss, wenn Sie in Compilern etwas umfangreicheres tun möchten.
Eclipse

1
Wow - es ist schön zu wissen, dass das "Drachenbuch" noch diskutiert wird. Ich erinnere mich, dass ich es vor 30 Jahren an der Universität studiert und durchgelesen habe.
Schroedingers Cat

4

Ich würde vorschlagen, zu schummeln und den Shunting Yard-Algorithmus zu verwenden . Es ist ein einfaches Mittel zum Schreiben eines einfachen Parsers vom Typ eines Taschenrechners und berücksichtigt Vorrang.

Wenn Sie Dinge richtig tokenisieren und Variablen usw. einbeziehen möchten, würde ich einen rekursiven Abstiegsparser schreiben, wie von anderen hier vorgeschlagen. Wenn Sie jedoch einfach einen Parser im Taschenrechnerstil benötigen, sollte dieser Algorithmus ausreichen :-)


4

Ich habe dies auf der PIC-Liste über den Shunting Yard-Algorithmus gefunden :

Harold schreibt:

Ich erinnere mich, dass ich vor langer Zeit einen Algorithmus gelesen habe, der algebraische Ausdrücke zur einfachen Auswertung in RPN konvertierte. Jeder Infixwert oder Operator oder jede Klammer wurde durch einen Eisenbahnwagen auf einer Strecke dargestellt. Ein Autotyp spaltete sich auf eine andere Strecke ab und der andere fuhr geradeaus weiter. Ich erinnere mich nicht an die Details (offensichtlich!), Dachte aber immer, es wäre interessant zu codieren. Dies ist zurück, als ich 6800 (nicht 68000) Assembly-Code schrieb.

Dies ist der "Rangierhof-Algorithmus", den die meisten Maschinenparser verwenden. Siehe den Artikel zum Parsen in Wikipedia. Eine einfache Möglichkeit, den Rangierhof-Algorithmus zu codieren, besteht darin, zwei Stapel zu verwenden. Einer ist der "Push" -Stapel und der andere der "Reduzieren" - oder "Ergebnis" -Stapel. Beispiel:

pstack = () // leer rstack = () Eingabe: 1 + 2 * 3 Priorität = 10 // niedrigste Reduzierung = 0 // nicht reduzieren

start: token '1': isnumber, setze pstack (push) token '+': isoperator setze priorität = 2 wenn priorität <previous_operator_precedence dann reduziere () // siehe unten setze '+' in pstack (push) token '2' : isnumber, in pstack (push) token setzen '*': isoperator, setze Priorität = 1, setze pstack (push) // überprüfe die Priorität als // über Token '3': isnumber, setze in pstack (push) Ende von Eingabe, muss reduziert werden (Ziel ist leerer Stapel) reduzieren () // fertig

Um zu reduzieren, Pop-Elemente aus dem Push-Stapel und legen Sie sie in den Ergebnis-Stapel, tauschen Sie immer die Top 2 Elemente auf pstack, wenn sie die Form 'Operator' 'Nummer' haben:

pstack: '1' '+' '2' ' ' '3' rstack: () ... pstack: () rstack: '3' '2' ' ' '1' '+'

wenn der Ausdruck gewesen wäre:

1 * 2 + 3

dann wäre der Reduktionsauslöser das Lesen des Tokens '+' gewesen, das eine geringere Präzision hat als das bereits gepusste '*', also hätte es getan:

pstack: '1' ' ' '2' rstack: () ... pstack: () rstack: '1' '2' ' '

und dann '+' und dann '3' gedrückt und dann endlich reduziert:

pstack: '+' '3' rstack: '1' '2' ' ' ... pstack: () rstack: '1' '2' ' ' '3' '+'

Die Kurzversion lautet also: Push-Nummern, wenn Push-Operatoren die Priorität des vorherigen Operators überprüfen. Wenn es höher war als der Bediener, der jetzt gedrückt werden soll, reduzieren Sie zuerst und drücken Sie dann den aktuellen Bediener. Um mit Parens umzugehen, speichern Sie einfach die Priorität des 'vorherigen' Operators und setzen Sie eine Markierung auf den pstack, die den Reduktionsalgorithmus anweist, die Reduktion zu beenden, wenn das Innere eines Parenpaars gelöst wird. Der schließende Paren löst eine Reduzierung aus, ebenso wie das Ende der Eingabe. Außerdem wird die offene Paren-Markierung aus dem Stapel entfernt und die Priorität der vorherigen Operation wiederhergestellt, sodass das Parsen nach dem geschlossenen Paren fortgesetzt werden kann, wo es aufgehört hat. Dies kann mit oder ohne Rekursion erfolgen (Hinweis: Verwenden Sie einen Stapel, um die vorherige Priorität zu speichern, wenn Sie auf ein '(' ...) stoßen. Die verallgemeinerte Version davon besteht darin, einen Parser-Generator zu verwenden, der einen Rangierhof-Algorithmus implementiert hat, z. mit Yacc oder Bison oder Taccle (tcl Analogon von Yacc).

Peter

-Adam


4

Eine weitere Ressource für das Parsen von Vorrang ist der Eintrag für den Operator-Vorrang-Parser in Wikipedia. Behandelt den Shunt-Yard-Algorithmus von Dijkstra und einen alternativen Baumalgorithmus, behandelt jedoch insbesondere einen wirklich einfachen Makro-Ersetzungsalgorithmus, der trivial vor jedem ignoranten Parser mit Vorrang implementiert werden kann:

#include <stdio.h>
int main(int argc, char *argv[]){
  printf("((((");
  for(int i=1;i!=argc;i++){
    if(argv[i] && !argv[i][1]){
      switch(argv[i]){
      case '^': printf(")^("); continue;
      case '*': printf("))*(("); continue;
      case '/': printf("))/(("); continue;
      case '+': printf(")))+((("); continue;
      case '-': printf(")))-((("); continue;
      }
    }
    printf("%s", argv[i]);
  }
  printf("))))\n");
  return 0;
}

Rufen Sie es auf als:

$ cc -o parenthesise parenthesise.c
$ ./parenthesise a \* b + c ^ d / e
((((a))*((b)))+(((c)^(d))/((e))))

Das ist großartig in seiner Einfachheit und sehr verständlich.


3
Das ist eine hübsche kleine Perle. Aber eine Erweiterung (z. B. mit Funktionsanwendung, impliziter Multiplikation, Präfix- und Postfix-Operatoren, optionalen Typanmerkungen usw.) würde das Ganze zerstören. Mit anderen Worten, es ist ein eleganter Hack.
Jared Updike

Ich verstehe den Punkt nicht. Dies ändert lediglich ein Parsing-Problem mit Operator-Priorität in ein Parsing-Problem mit Klammer-Priorität.
Marquis von Lorne

@EJP sicher, aber der Parser in der Frage behandelt Klammern ganz gut, also ist dies eine vernünftige Lösung. Wenn Sie einen Parser haben, der dies jedoch nicht tut, haben Sie Recht, dass dies das Problem nur in einen anderen Bereich verschiebt.
Adam Davis

4

Ich habe auf meiner Website eine Quelle für einen ultrakompakten Java Math Evaluator (1 Klasse, <10 KiB) veröffentlicht . Dies ist ein rekursiver Abstiegsparser des Typs, der die Schädelexplosion für das Poster der akzeptierten Antwort verursacht hat.

Es unterstützt volle Priorität, Klammern, benannte Variablen und Funktionen mit nur einem Argument.




2

Ich arbeite derzeit an einer Reihe von Artikeln, die einen Parser für reguläre Ausdrücke als Lernwerkzeug für Entwurfsmuster und lesbare Programmierung erstellen. Sie können sich den lesbaren Code ansehen . Der Artikel beschreibt eine klare Verwendung des Rangierbahnhof-Algorithmus.


2

Ich habe einen Ausdrucksparser in F # geschrieben und hier darüber gebloggt . Es verwendet den Shunt-Yard-Algorithmus, aber anstatt von Infix zu RPN zu konvertieren, habe ich einen zweiten Stapel hinzugefügt, um die Ergebnisse der Berechnungen zu sammeln. Es behandelt die Priorität von Operatoren korrekt, unterstützt jedoch keine unären Operatoren. Ich habe dies geschrieben, um F # zu lernen, aber nicht um das Parsen von Ausdrücken zu lernen.


2

Eine Python-Lösung mit Pyparsing finden Sie hier . Das Parsen der Infixnotation mit verschiedenen Operatoren mit Vorrang ist ziemlich häufig, und daher umfasst das Pyparsing auch den infixNotation(früheren operatorPrecedence) Ausdrucksgenerator. Mit ihm können Sie einfach boolesche Ausdrücke definieren, indem Sie beispielsweise "UND", "ODER", "NICHT" verwenden. Oder Sie können Ihre Arithmetik mit vier Funktionen erweitern, um andere Operatoren zu verwenden, z. für Fakultät oder '%' für Modul oder fügen Sie P- und C-Operatoren hinzu, um Permutationen und Kombinationen zu berechnen. Sie können einen Infix-Parser für die Matrixnotation schreiben, der die Behandlung von '-1'- oder' T'-Operatoren (für Inversion und Transponierung) umfasst. Das operatorPrecedence-Beispiel eines Parsers mit 4 Funktionen (mit '!'


1

Ich weiß, dass dies eine späte Antwort ist, aber ich habe gerade einen winzigen Parser geschrieben, mit dem alle Operatoren (Präfix, Postfix und Infix-links, Infix-rechts und nicht assoziativ) einen beliebigen Vorrang haben können.

Ich werde dies für eine Sprache mit willkürlicher DSL-Unterstützung erweitern, aber ich wollte nur darauf hinweisen, dass man keine benutzerdefinierten Parser für die Operatorpriorität benötigt, man kann einen generalisierten Parser verwenden, der überhaupt keine Tabellen benötigt, und sucht einfach nach der Priorität jedes Operators, wie er angezeigt wird. Die Leute haben benutzerdefinierte Pratt-Parser oder Rangier-Parser erwähnt, die illegale Eingaben akzeptieren können - diese müssen nicht angepasst werden und akzeptieren (sofern kein Fehler vorliegt) keine schlechten Eingaben. Es ist in gewissem Sinne nicht vollständig, es wurde geschrieben, um den Algorithmus zu testen, und seine Eingabe erfolgt in einer Form, die eine Vorverarbeitung erfordert, aber es gibt Kommentare, die dies klar machen.

Beachten Sie, dass einige gängige Arten von Operatoren fehlen, z. B. die Art des Operators, der zum Indizieren verwendet wird, z. B. Tabelle [Index] oder Aufrufen einer Funktionsfunktion (Parameterausdruck, ...). Ich werde diese hinzufügen, aber beide als Postfix betrachten Operatoren, bei denen das, was zwischen den Trennzeichen '[' und ']' oder '(' und ')' steht, mit einer anderen Instanz des Ausdrucksparsers analysiert wird. Es tut mir leid, dass ich das ausgelassen habe, aber der Postfix-Teil ist in - das Hinzufügen des Restes wird wahrscheinlich fast die doppelte Größe des Codes haben.

Da der Parser nur aus 100 Zeilen Schlägercode besteht, sollte ich ihn vielleicht hier einfügen. Ich hoffe, dass dies nicht länger ist, als es der Stackoverflow zulässt.

Einige Details zu willkürlichen Entscheidungen:

Wenn ein Postfix-Operator mit niedriger Priorität um dieselben Infixblöcke wie ein Präfix-Operator mit niedriger Priorität konkurriert, gewinnt der Präfix-Operator. Dies tritt in den meisten Sprachen nicht auf, da die meisten keine Postfix-Operatoren mit niedriger Priorität haben. - zum Beispiel: ((Daten a) (links 1 +) (vor 2 nicht) (Daten b) (nach 3!) (Links 1 +) (Daten c)) ist a + nicht b! + C, wo nicht a ist Präfixoperator und! ist ein Postfix-Operator und beide haben eine niedrigere Priorität als +, daher möchten sie auf inkompatible Weise entweder als (a + nicht b!) + c oder als + (nicht b! + c) gruppieren. In diesen Fällen gewinnt der Präfix-Operator immer Zweitens ist die Art und Weise, wie es analysiert

Nichtassoziative Infix-Operatoren sind wirklich vorhanden, sodass Sie nicht so tun müssen, als ob Operatoren, die andere Typen zurückgeben, als sie zusammen sinnvoll sind, aber ohne unterschiedliche Ausdruckstypen für jeden einen Kludge zu haben. Daher weigern sich nicht assoziative Operatoren in diesem Algorithmus, nicht nur sich selbst, sondern jedem Operator mit derselben Priorität zuzuordnen. Dies ist ein häufiger Fall, da << = ==> = usw. in den meisten Sprachen nicht miteinander assoziiert werden.

Die Frage, wie verschiedene Arten von Operatoren (links, Präfix usw.) die Rangfolge aufheben, sollte nicht auftauchen, da es nicht wirklich sinnvoll ist, Operatoren unterschiedlicher Typen dieselbe Priorität einzuräumen. Dieser Algorithmus macht in diesen Fällen etwas, aber ich mache mir nicht einmal die Mühe, genau herauszufinden, was, weil eine solche Grammatik überhaupt eine schlechte Idee ist.

#lang racket
;cool the algorithm fits in 100 lines!
(define MIN-PREC -10000)
;format (pre prec name) (left prec name) (right prec name) (nonassoc prec name) (post prec name) (data name) (grouped exp)
;for example "not a*-7+5 < b*b or c >= 4"
;which groups as: not ((((a*(-7))+5) < (b*b)) or (c >= 4))"
;is represented as '((pre 0 not)(data a)(left 4 *)(pre 5 -)(data 7)(left 3 +)(data 5)(nonassoc 2 <)(data b)(left 4 *)(data b)(right 1 or)(data c)(nonassoc 2 >=)(data 4)) 
;higher numbers are higher precedence
;"(a+b)*c" is represented as ((grouped (data a)(left 3 +)(data b))(left 4 *)(data c))

(struct prec-parse ([data-stack #:mutable #:auto]
                    [op-stack #:mutable #:auto])
  #:auto-value '())

(define (pop-data stacks)
  (let [(data (car (prec-parse-data-stack stacks)))]
    (set-prec-parse-data-stack! stacks (cdr (prec-parse-data-stack stacks)))
    data))

(define (pop-op stacks)
  (let [(op (car (prec-parse-op-stack stacks)))]
    (set-prec-parse-op-stack! stacks (cdr (prec-parse-op-stack stacks)))
    op))

(define (push-data! stacks data)
    (set-prec-parse-data-stack! stacks (cons data (prec-parse-data-stack stacks))))

(define (push-op! stacks op)
    (set-prec-parse-op-stack! stacks (cons op (prec-parse-op-stack stacks))))

(define (process-prec min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((>= (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-prec min-prec stacks))))))))

(define (process-nonassoc min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((> (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-nonassoc min-prec stacks))
                   ((= (cadr op) min-prec) (error "multiply applied non-associative operator"))
                   ))))))

(define (apply-op op stacks)
  (let [(op-type (car op))]
    (cond ((eq? op-type 'post)
           (push-data! stacks `(,op ,(pop-data stacks) )))
          (else ;assume infix
           (let [(tos (pop-data stacks))]
             (push-data! stacks `(,op ,(pop-data stacks) ,tos))))))) 

(define (finish input min-prec stacks)
  (process-prec min-prec stacks)
  input
  )

(define (post input min-prec stacks)
  (if (null? input) (finish input min-prec stacks)
      (let* [(cur (car input))
             (input-type (car cur))]
        (cond ((eq? input-type 'post)
               (cond ((< (cadr cur) min-prec)
                      (finish input min-prec stacks))
                     (else 
                      (process-prec (cadr cur)stacks)
                      (push-data! stacks (cons cur (list (pop-data stacks))))
                      (post (cdr input) min-prec stacks))))
              (else (let [(handle-infix (lambda (proc-fn inc)
                                          (cond ((< (cadr cur) min-prec)
                                                 (finish input min-prec stacks))
                                                (else 
                                                 (proc-fn (+ inc (cadr cur)) stacks)
                                                 (push-op! stacks cur)
                                                 (start (cdr input) min-prec stacks)))))]
                      (cond ((eq? input-type 'left) (handle-infix process-prec 0))
                            ((eq? input-type 'right) (handle-infix process-prec 1))
                            ((eq? input-type 'nonassoc) (handle-infix process-nonassoc 0))
                            (else error "post op, infix op or end of expression expected here"))))))))

;alters the stacks and returns the input
(define (start input min-prec stacks)
  (if (null? input) (error "expression expected")
      (let* [(cur (car input))
             (input-type (car cur))]
        (set! input (cdr input))
        ;pre could clearly work with new stacks, but could it reuse the current one?
        (cond ((eq? input-type 'pre)
               (let [(new-stack (prec-parse))]
                 (set! input (start input (cadr cur) new-stack))
                 (push-data! stacks 
                             (cons cur (list (pop-data new-stack))))
                 ;we might want to assert here that the cdr of the new stack is null
                 (post input min-prec stacks)))
              ((eq? input-type 'data)
               (push-data! stacks cur)
               (post input min-prec stacks))
              ((eq? input-type 'grouped)
               (let [(new-stack (prec-parse))]
                 (start (cdr cur) MIN-PREC new-stack)
                 (push-data! stacks (pop-data new-stack)))
               ;we might want to assert here that the cdr of the new stack is null
               (post input min-prec stacks))
              (else (error "bad input"))))))

(define (op-parse input)
  (let [(stacks (prec-parse))]
    (start input MIN-PREC stacks)
    (pop-data stacks)))

(define (main)
  (op-parse (read)))

(main)

1

Hier ist eine einfache rekursive Falllösung, die in Java geschrieben wurde. Beachten Sie, dass keine negativen Zahlen verarbeitet werden. Sie können dies jedoch hinzufügen, wenn Sie möchten:

public class ExpressionParser {

public double eval(String exp){
    int bracketCounter = 0;
    int operatorIndex = -1;

    for(int i=0; i<exp.length(); i++){
        char c = exp.charAt(i);
        if(c == '(') bracketCounter++;
        else if(c == ')') bracketCounter--;
        else if((c == '+' || c == '-') && bracketCounter == 0){
            operatorIndex = i;
            break;
        }
        else if((c == '*' || c == '/') && bracketCounter == 0 && operatorIndex < 0){
            operatorIndex = i;
        }
    }
    if(operatorIndex < 0){
        exp = exp.trim();
        if(exp.charAt(0) == '(' && exp.charAt(exp.length()-1) == ')')
            return eval(exp.substring(1, exp.length()-1));
        else
            return Double.parseDouble(exp);
    }
    else{
        switch(exp.charAt(operatorIndex)){
            case '+':
                return eval(exp.substring(0, operatorIndex)) + eval(exp.substring(operatorIndex+1));
            case '-':
                return eval(exp.substring(0, operatorIndex)) - eval(exp.substring(operatorIndex+1));
            case '*':
                return eval(exp.substring(0, operatorIndex)) * eval(exp.substring(operatorIndex+1));
            case '/':
                return eval(exp.substring(0, operatorIndex)) / eval(exp.substring(operatorIndex+1));
        }
    }
    return 0;
}

}}


1

Der Algorithmus könnte leicht in C als rekursiver Abstiegsparser codiert werden.

#include <stdio.h>
#include <ctype.h>

/*
 *  expression -> sum
 *  sum -> product | product "+" sum
 *  product -> term | term "*" product
 *  term -> number | expression
 *  number -> [0..9]+
 */

typedef struct {
    int value;
    const char* context;
} expression_t;

expression_t expression(int value, const char* context) {
    return (expression_t) { value, context };
}

/* begin: parsers */

expression_t eval_expression(const char* symbols);

expression_t eval_number(const char* symbols) {
    // number -> [0..9]+
    double number = 0;        
    while (isdigit(*symbols)) {
        number = 10 * number + (*symbols - '0');
        symbols++;
    }
    return expression(number, symbols);
}

expression_t eval_term(const char* symbols) {
    // term -> number | expression
    expression_t number = eval_number(symbols);
    return number.context != symbols ? number : eval_expression(symbols);
}

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

expression_t eval_sum(const char* symbols) {
    // sum -> product | product "+" sum
    expression_t product = eval_product(symbols);
    if (*product.context != '+')
        return product;

    expression_t sum = eval_sum(product.context + 1);
    return expression(product.value + sum.value, sum.context);
}

expression_t eval_expression(const char* symbols) {
    // expression -> sum
    return eval_sum(symbols);
}

/* end: parsers */

int main() {
    const char* expression = "1+11*5";
    printf("eval(\"%s\") == %d\n", expression, eval_expression(expression).value);

    return 0;
}

Die nächsten Bibliotheken könnten nützlich sein: yupana - streng arithmetische Operationen; tinyexpr - arithmetische Operationen + C mathematische Funktionen + eine vom Benutzer bereitgestellte; mpc - Parser-Kombinatoren

Erläuterung

Lassen Sie uns eine Folge von Symbolen erfassen, die den algebraischen Ausdruck darstellen. Die erste ist eine Zahl, dh eine Dezimalstelle, die ein- oder mehrmals wiederholt wird. Wir werden diese Notation als Produktionsregel bezeichnen.

number -> [0..9]+

Der Additionsoperator mit seinen Operanden ist eine weitere Regel. Es sind eines numberoder mehrere Symbole, die die sum "*" sumSequenz darstellen.

sum -> number | sum "+" sum

Versuchen Ersatz numberin sum "+" sumdem sein wird , number "+" numberwas wiederum in ausgeweitet werden könnte , [0..9]+ "+" [0..9]+dass schließlich reduziert werden könnte , 1+8die die Expression korrekte Zugabe ist.

Andere Substitutionen erzeugen ebenfalls den richtigen Ausdruck: sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

Stück für Stück könnten wir einer Reihe von Produktionsregeln, auch Grammatik genannt , ähneln , die alle möglichen algebraischen Ausdrücke ausdrücken.

expression -> sum
sum -> difference | difference "+" sum
difference -> product | difference "-" product
product -> fraction | fraction "*" product
fraction -> term | fraction "/" term
term -> "(" expression ")" | number
number -> digit+                                                                    

Um die Priorität des Bedieners zu kontrollieren, ändern Sie die Position seiner Produktionsregel gegenüber anderen. Schauen Sie sich die Grammatik oben an und beachten Sie, dass die Produktionsregel für *darunter platziert +wird, um die productAuswertung vorher zu erzwingen sum. Die Implementierung kombiniert lediglich die Mustererkennung mit der Bewertung und spiegelt somit die Produktionsregeln genau wider.

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

Hier bewerten wir termzuerst und geben es zurück, wenn es kein *Zeichen gibt, nachdem es in unserer Produktionsregel ausgewählt wurde. Andernfalls bewerten Sie die Symbole danach und geben term.value * product.value dies in unserer Produktionsregel zurück, d. H.term "*" product

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.