Analizzatore di equazioni (espressioni) con precedenza?


104

Ho sviluppato un parser di equazioni utilizzando un semplice algoritmo di stack che gestirà operatori binari (+, -, |, &, *, /, ecc.), Operatori unari (!) E parentesi.

L'uso di questo metodo, tuttavia, mi lascia con tutto ciò che ha la stessa precedenza: viene valutato da sinistra a destra indipendentemente dall'operatore, sebbene la precedenza possa essere applicata utilizzando le parentesi.

Quindi in questo momento "1 + 11 * 5" restituisce 60, non 56 come ci si potrebbe aspettare.

Sebbene sia adatto per il progetto corrente, voglio avere una routine per scopi generali che posso usare per i progetti successivi.

Modificato per chiarezza:

Qual è un buon algoritmo per analizzare le equazioni con precedenza?

Sono interessato a qualcosa di semplice da implementare e capisco che posso programmare me stesso per evitare problemi di licenza con il codice disponibile.

Grammatica:

Non capisco la domanda grammaticale - l'ho scritta a mano. È abbastanza semplice da non vedere la necessità di YACC o Bison. Ho solo bisogno di calcolare stringhe con equazioni come "2 + 3 * (42/13)".

Linguaggio:

Lo sto facendo in C, ma sono interessato a un algoritmo, non a una soluzione specifica per il linguaggio. C è di livello sufficientemente basso da consentire la conversione in un'altra lingua in caso di necessità.

Esempio di codice

Ho pubblicato il codice di prova per il parser di espressioni semplici di cui parlavo sopra. I requisiti del progetto sono cambiati e quindi non ho mai avuto bisogno di ottimizzare il codice per le prestazioni o lo spazio in quanto non era incorporato nel progetto. È nella forma dettagliata originale e dovrebbe essere facilmente comprensibile. Se faccio qualcosa di più in termini di precedenza degli operatori, probabilmente sceglierò il macro hack perché corrisponde al resto del programma in semplicità. Se mai lo userò in un progetto reale, tuttavia, sceglierò un parser più compatto / veloce.

Domanda correlata

Progettazione intelligente di un parser matematico?

-Adamo


Ho scritto un parser di espressioni in C # sul mio blog. Fa da infisso a suffisso senza lo stack nell'algoritmo del cantiere di manovra. Utilizza solo un array.
Guge

Da quanto ho capito è necessario analizzare solo l'espressione aritmetica. Usa la notazione polacca inversa
mishadoff

Risposte:


69

Il modo più duro

Vuoi un parser di discesa ricorsivo .

Per avere la precedenza devi pensare in modo ricorsivo, ad esempio, usando la tua stringa di esempio,

1+11*5

per farlo manualmente, dovresti leggere il 1, quindi vedere il plus e avviare una nuova "sessione" di analisi ricorsiva iniziando con 11... e assicurarti di analizzare il 11 * 5in un proprio fattore, producendo un albero di analisi con 1 + (11 * 5).

Tutto ciò sembra così doloroso persino da tentare di spiegare, specialmente con l'ulteriore impotenza di C.Vedi, dopo aver analizzato l'11, se * fosse effettivamente un + invece, dovresti abbandonare il tentativo di creare un termine e invece analizzare il 11stesso come fattore. La mia testa sta già esplodendo. È possibile con la strategia decente ricorsiva, ma c'è un modo migliore ...

Il modo facile (giusto)

Se utilizzi uno strumento GPL come Bison, probabilmente non devi preoccuparti dei problemi di licenza poiché il codice C generato da bison non è coperto dalla GPL (IANAL ma sono abbastanza sicuro che gli strumenti GPL non forzino la GPL codice generato / binari; per esempio Apple compila codice come ad esempio Aperture con GCC e lo vendono senza dover GPL detto codice).

Scarica Bison (o qualcosa di equivalente, ANTLR, ecc.).

Di solito c'è un codice di esempio su cui puoi semplicemente eseguire bison e ottenere il codice C desiderato che dimostra questo calcolatore a quattro funzioni:

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

Guarda il codice generato e vedi che non è così facile come sembra. Inoltre, i vantaggi dell'utilizzo di uno strumento come Bison sono 1) impari qualcosa (soprattutto se leggi il libro del drago e impari la grammatica), 2) eviti che NIH cerchi di reinventare la ruota. Con un vero strumento generatore di parser, hai effettivamente una speranza di ridimensionare in seguito, mostrando ad altre persone che sai che i parser sono il dominio degli strumenti di analisi.


Aggiornare:

Le persone qui hanno offerto molti buoni consigli. Il mio unico avvertimento contro il saltare gli strumenti di analisi o semplicemente usare l'algoritmo di Shunting Yard o un parser decente ricorsivo a mano è che i linguaggi giocattolo 1 un giorno potrebbero trasformarsi in grandi linguaggi reali con funzioni (sin, cos, log) e variabili, condizioni e per loop.

Flex / Bison può benissimo essere eccessivo per un piccolo, semplice interprete, ma un parser + valutatore una tantum può causare problemi su tutta la linea quando è necessario apportare modifiche o è necessario aggiungere funzionalità. La tua situazione varierà e dovrai usare il tuo giudizio; semplicemente non punire altre persone per i tuoi peccati [2] e costruisci uno strumento meno che adeguato.

Il mio strumento preferito per l'analisi

Il miglior strumento al mondo per il lavoro è la libreria Parsec (per parser decenti ricorsivi) che viene fornita con il linguaggio di programmazione Haskell. Assomiglia molto a BNF , o come uno strumento specializzato o un linguaggio specifico per il dominio per l'analisi (codice di esempio [3]), ma in realtà è solo una normale libreria in Haskell, il che significa che si compila nella stessa fase di compilazione del resto del tuo codice Haskell e puoi scrivere codice Haskell arbitrario e chiamarlo all'interno del tuo parser, e puoi mescolare e abbinare altre librerie tutte nello stesso codice . (Incorporare un linguaggio di analisi come questo in un linguaggio diverso da Haskell si traduce in un sacco di cruft sintattico, a proposito. L'ho fatto in C # e funziona abbastanza bene ma non è così carino e succinto.)

Appunti:

1 Richard Stallman dice, in Perché non dovresti usare Tcl

La lezione principale di Emacs è che un linguaggio per estensioni non dovrebbe essere un semplice "linguaggio di estensione". Dovrebbe essere un vero linguaggio di programmazione, progettato per scrivere e mantenere programmi sostanziali. Perché le persone vorranno farlo!

[2] Sì, sono per sempre sfregiato dall'usare quel "linguaggio".

Nota anche che quando ho inviato questa voce, l'anteprima era corretta, ma il parser meno che adeguato di SO ha mangiato il mio tag di ancoraggio di chiusura nel primo paragrafo , dimostrando che i parser non sono qualcosa con cui scherzare perché se usi espressioni regolari e una tantum ti hackera probabilmente otterrà qualcosa di sottile e piccolo di sbagliato .

[3] Frammento di un parser Haskell che utilizza Parsec: un calcolatore a quattro funzioni esteso con esponenti, parentesi, spazi bianchi per la moltiplicazione e costanti (come pi ed e).

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
Per sottolineare il mio punto, nota che il markup nel mio post non viene analizzato correttamente (e questo varia tra il markup reso staticamente e quello reso nell'anteprima WMD). Ci sono stati diversi tentativi per risolverlo, ma penso che IL PARSER SIA SBAGLIATO. Fai un favore a tutti e analizza bene!
Jared Updike

155

L' algoritmo di smistamento è lo strumento giusto per questo. Wikipedia è davvero confusa su questo, ma fondamentalmente l'algoritmo funziona in questo modo:

Diciamo che vuoi valutare 1 + 2 * 3 + 4. Intuitivamente, "sai" che devi prima fare il 2 * 3, ma come ottieni questo risultato? La chiave è rendersi conto che quando si esegue la scansione della stringa da sinistra a destra, si valuta un operatore quando l'operatore che lo segue ha una precedenza inferiore (o uguale a). Nel contesto dell'esempio, ecco cosa vuoi fare:

  1. Guarda: 1 + 2, non fare nulla.
  2. Ora guarda 1 + 2 * 3, ancora non fare nulla.
  3. Ora guarda 1 + 2 * 3 + 4, ora sai che 2 * 3 deve essere valutato perché l'operatore successivo ha precedenza inferiore.

Come lo implementate?

Vuoi avere due pile, una per i numeri e un'altra per gli operatori. Metti i numeri in pila tutto il tempo. Confronta ogni nuovo operatore con quello in cima allo stack, se quello in cima allo stack ha una priorità più alta, lo estrai dallo stack degli operatori, estrai gli operandi dallo stack dei numeri, applica l'operatore e sposta il risultato sulla pila dei numeri. Ora ripeti il ​​confronto con l'operatore in cima allo stack.

Tornando all'esempio, funziona così:

N = [] Ops = []

  • Leggi 1. N = [1], Ops = []
  • Leggi +. N = [1], Ops = [+]
  • Leggi 2. N = [1 2], Ops = [+]
  • Leggi *. N = [1 2], Ops = [+ *]
  • Leggi 3. N = [1 2 3], Ops = [+ *]
  • Leggi +. N = [1 2 3], Ops = [+ *]
    • Apri 3, 2 ed esegui 2 *3 e inserisci il risultato su N. N = [1 6], Ops = [+]
    • +è associativo a sinistra, quindi si desidera estrarre anche 1, 6 ed eseguire il +. N = [7], Ops = [].
    • Infine spingere il [+] sulla pila dell'operatore. N = [7], Ops = [+].
  • Leggi 4. N = [7 4]. Ops = [+].
  • Sei a corto di input, quindi vuoi svuotare le pile ora. In base al quale otterrai il risultato 11.

Ecco, non è così difficile, vero? E non fa invocazioni a grammatiche o generatori di parser.


6
In realtà non hai bisogno di due pile, fintanto che puoi vedere la seconda cosa in pila senza far saltare la cima. Puoi invece utilizzare un unico stack che alterna numeri e operatori. Questo in effetti corrisponde esattamente a ciò che fa un generatore di parser LR (come bison).
Chris Dodd,

2
Veramente bella spiegazione dell'algoritmo che ho appena implementato in questo momento. Inoltre non lo stai convertendo in suffisso, il che è anche bello. Anche aggiungere il supporto per le parentesi è molto semplice.
Giorgi

4
Una versione semplificata per l'algoritmo dello scalo di manovra può essere trovata qui: andreinc.net/2010/10/05/… (con implementazioni in Java e python)
Andrei Ciobanu

1
Grazie per questo, esattamente quello che sto cercando!
Joe Green

Grazie mille per aver menzionato la sinistra - associativa. Mi sono bloccato con l'operatore ternario: come analizzare espressioni complesse con "?:" Annidato. Mi sono reso conto che entrambi "?" e ':' devono avere la stessa priorità. E se interpretiamo "?" come destro - associativo e ':' come sinistro - associativo, questo algoritmo funziona molto bene con loro. Inoltre, possiamo comprimere 2 operatori solo quando entrambi sono a sinistra - associativi.
Vladislav

25

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

Ottima spiegazione dei diversi approcci:

  • Riconoscimento della discesa ricorsiva
  • L'algoritmo della stazione di manovra
  • La soluzione classica
  • Precedenza arrampicata

Scritto in un linguaggio semplice e pseudo-codice.

Mi piace quello "precedence climbing".


Il collegamento sembra essere interrotto. Ciò che avrebbe reso una risposta migliore sarebbe stata parafrasare ogni metodo in modo che quando quel collegamento fosse scomparso, alcune di quelle informazioni utili sarebbero state conservate qui.
Adam White

18

C'è un bell'articolo qui sulla combinazione di un semplice parser a discesa ricorsiva con l'analisi della precedenza degli operatori. Se hai recentemente scritto parser, dovrebbe essere molto interessante e istruttivo da leggere.


16

Molto tempo fa, ho creato il mio algoritmo di analisi, che non sono riuscito a trovare in nessun libro sull'analisi (come il Libro del drago). Guardando i puntatori all'algoritmo di Shunting Yard, vedo la somiglianza.

Circa 2 anni fa, ho scritto un post a riguardo, completo di codice sorgente Perl, su http://www.perlmonks.org/?node_id=554516 . È facile eseguire il port in altre lingue: la prima implementazione che ho fatto è stata nell'assembler Z80.

È ideale per il calcolo diretto con i numeri, ma puoi usarlo per produrre un albero di analisi, se necessario.

Aggiorna Poiché più persone possono leggere (o eseguire) Javascript, ho reimplementato il mio parser in Javascript, dopo che il codice è stato riorganizzato. L'intero parser è inferiore a 5k di codice Javascript (circa 100 righe per il parser, 15 righe per una funzione wrapper) inclusi la segnalazione degli errori e i commenti.

Puoi trovare una demo live su 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

Sarebbe utile se potessi descrivere la grammatica che stai attualmente utilizzando per analizzare. Sembra che il problema sia lì!

Modificare:

Il fatto che tu non capisca la domanda grammaticale e che "l'hai scritta a mano" molto probabilmente spiega perché hai problemi con le espressioni della forma "1 + 11 * 5" (cioè con la precedenza dell'operatore) . Ad esempio, cercare su Google "grammatica per espressioni aritmetiche" dovrebbe fornire alcuni buoni suggerimenti. Una tale grammatica non deve essere complicata:

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

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

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

farebbe il trucco per esempio, e può essere banalmente aumentato per prendersi cura di alcune espressioni più complicate (incluse funzioni per esempio, o poteri, ...).

Ti suggerisco di dare un'occhiata a questo thread, per esempio.

Quasi tutte le introduzioni alle grammatiche / analisi considerano le espressioni aritmetiche come un esempio.

Nota che usare una grammatica non implica affatto usare uno strumento specifico ( a la Yacc, Bison, ...). In effetti, stai sicuramente già utilizzando la seguente grammatica:

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

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

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

(o qualcosa del genere) senza saperlo!


8

Hai pensato di usare Boost Spirit ? Ti consente di scrivere grammatiche simili a EBNF in C ++ in questo modo:

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

1
+1 E il risultato è che tutto fa parte di Boost. La grammatica per la calcolatrice è qui: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . L'implementazione della calcolatrice è qui: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . E la documentazione è qui: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . Non capirò mai perché le persone implementano ancora i propri mini-parser.
stephan

5

Mentre poni la tua domanda, non è necessaria alcuna ricorsione. La risposta è tre cose: notazione Postfix più algoritmo Shunting Yard più valutazione dell'espressione Postfix:

1). Notazione con suffisso = inventata per eliminare la necessità di una specifica esplicita della precedenza. Leggi di più in rete, ma ecco il succo: espressione infissa (1 + 2) * 3 mentre è facile da leggere per gli esseri umani ed elaborare non molto efficiente per il calcolo tramite macchina. Cosa è? Regola semplice che dice "riscrivi l'espressione memorizzando nella cache in precedenza, quindi elaborala sempre da sinistra a destra". Quindi infisso (1 + 2) * 3 diventa un suffisso 12 + 3 *. POST perché l'operatore viene posto sempre DOPO gli operandi.

2). Valutazione dell'espressione postfissa. Facile. Legge i numeri dalla stringa suffisso. Spingerli su una pila finché non si vede un operatore. Controllare il tipo di operatore - unario? binario? terziario? Estrai tutti gli operandi dallo stack necessari per valutare questo operatore. Valutare. Rimetti in pila il risultato! E hai quasi finito. Continua a farlo finché lo stack non ha una sola voce = valore che stai cercando.

Facciamo (1 + 2) * 3 che nel suffisso è "12 + 3 *". Leggi il primo numero = 1. Mettilo in pila. Continua a leggere. Numero = 2. Mettilo in pila. Continua a leggere. Operatore. Quale? +. Che tipo? Binary = necessita di due operandi. Pop stack due volte = argright è 2 e argleft è 1. 1 + 2 è 3. Spingi 3 di nuovo in pila. Leggi il successivo dalla stringa del suffisso. È un numero. 3.Premere. Continua a leggere. Operatore. Quale? *. Che tipo? Binario = richiede due numeri -> pop stack due volte. Primo pop in argright, seconda volta in argleft. Valuta l'operazione: 3 volte 3 fa 9, spingi 9 in pila. Leggi il prossimo carattere postfisso. È nullo. Fine dell'input. Pop stack onec = questa è la tua risposta.

3). Shunting Yard viene utilizzato per trasformare l'espressione infissa umana (facilmente) leggibile in espressione postfissa (anche umana facilmente leggibile dopo un po 'di pratica). Facile da codificare manualmente. Vedi i commenti sopra e in rete.


4

C'è una lingua che vuoi usare? ANTLR ti consentirà di farlo da una prospettiva Java. Adrian Kuhn ha un eccellente articolo su come scrivere una grammatica eseguibile in Ruby; in effetti, il suo esempio è quasi esattamente il tuo esempio di espressione aritmetica.


Devo ammettere che i miei esempi forniti nel post del blog stanno ottenendo una ricorsione a sinistra sbagliata, cioè a - b - c restituisce (a - (b -c)) invece di ((a -b) - c). In realtà, questo mi ricorda di aggiungere un todo che dovrei correggere i post del blog.
akuhn

4

Dipende da quanto "generale" vuoi che sia.

Se vuoi che sia davvero molto generale, ad esempio essere in grado di analizzare funzioni matematiche come sin (4 + 5) * cos (7 ^ 3), probabilmente avrai bisogno di un albero di analisi.

In cui, non credo sia corretto incollare qui un'implementazione completa. Ti suggerisco di dare un'occhiata a uno dei famigerati " Dragon book ".

Ma se vuoi solo il supporto per la precedenza , puoi farlo convertendo prima l'espressione in forma di suffisso in cui un algoritmo che puoi copiare e incollare dovrebbe essere disponibile da Google o penso che tu possa codificarlo da solo con un binario albero.

Quando lo hai in forma di suffisso, da quel momento in poi è un gioco da ragazzi dato che sai già come la pila aiuta.


Il libro dei draghi potrebbe essere un po 'eccessivo per un analizzatore di espressioni: un semplice parser di discesa ricorsiva è tutto ciò che serve, ma è assolutamente da leggere se vuoi fare qualcosa di più ampio nei compilatori.
Eclipse

1
Wow, è bello sapere che il "libro del drago" è ancora discusso. Ricordo di averlo studiato - e di averlo letto tutto - all'università, 30 anni fa.
Gatto Schroedingers

4

Suggerirei di barare e di usare l' algoritmo di Shunting Yard . È un mezzo facile per scrivere un semplice parser di tipo calcolatrice e tiene conto della precedenza.

Se vuoi tokenizzare correttamente le cose e coinvolgere variabili, ecc., Allora vorrei andare avanti e scrivere un parser di discesa ricorsivo come suggerito da altri qui, tuttavia se hai semplicemente bisogno di un parser in stile calcolatrice, questo algoritmo dovrebbe essere sufficiente :-)


4

Ho trovato questo nella PIClist sull'algoritmo Shunting Yard :

Harold scrive:

Ricordo di aver letto, molto tempo fa, un algoritmo che converte le espressioni algebriche in RPN per una facile valutazione. Ogni valore infisso o operatore o parentesi era rappresentato da un vagone ferroviario su un binario. Un tipo di macchina si è separato su un altro binario e l'altro ha proseguito dritto. Non ricordo i dettagli (ovviamente!), Ma ho sempre pensato che sarebbe stato interessante programmare. Questo è tornato quando stavo scrivendo 6800 (non 68000) codice assembly.

Questo è l '"algoritmo di smistamento" ed è quello che usa la maggior parte dei parser delle macchine. Vedi l'articolo sull'analisi in Wikipedia. Un modo semplice per codificare l'algoritmo del cantiere di manovra è utilizzare due pile. Uno è lo stack "push" e l'altro lo stack "reduce" o "result". Esempio:

pstack = () // empty rstack = () input: 1 + 2 * 3 precedenza = 10 // riduzione più bassa = 0 // non ridurre

start: token '1': isnumber, put in pstack (push) token '+': isoperator set precedence = 2 if precedence <previous_operator_precedence then reduce () // see below put '+' in pstack (push) token '2' : isnumber, put in pstack (push) token '*': isoperator, set precedence = 1, put in pstack (push) // check precedence as // above token '3': isnumber, put in pstack (push) end of input, need to reduce (goal is empty pstack) reduce () // fatto

per ridurre, estrai gli elementi dalla pila di push e inseriscili nella pila dei risultati, scambia sempre i primi 2 elementi su pstack se sono della forma "operatore" "numero":

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

se l'espressione sarebbe stata:

1 * 2 + 3

quindi il trigger di riduzione sarebbe stata la lettura del token '+' che ha una precendenza inferiore rispetto al '*' già premuto, quindi avrebbe fatto:

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

e poi premuto "+" e poi "3" e infine ridotto:

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

Quindi la versione breve è: premi i numeri, quando spingi gli operatori controllano la precedenza dell'operatore precedente. Se era più alto di quello dell'operatore che deve essere spinto ora, prima ridurre, quindi spingere l'operatore corrente. Per gestire le parentesi, salva semplicemente la precedenza dell'operatore "precedente" e metti un segno sul pacco che dice all'algoritmo di riduzione di smettere di ridurre quando risolve l'interno di una coppia parentale. La parentesi di chiusura attiva una riduzione così come la fine dell'input, e rimuove anche il segno di parentesi aperto dal pstack e ripristina la precedenza dell '"operazione precedente" in modo che l'analisi possa continuare dopo la parentesi chiusa da dove era stata interrotta. Questo può essere fatto con la ricorsione o senza (suggerimento: usa uno stack per memorizzare la precedenza precedente quando incontri un '(' ...). La versione generalizzata di questo è quella di utilizzare un generatore di parser implementato algoritmo di smistamento cantiere, ad es. usando yacc o bison o taccle (analogo di tcl di yacc).

Peter

-Adamo


4

Un'altra risorsa per l'analisi della precedenza è la voce del parser di precedenza dell'operatore su Wikipedia. Copre l'algoritmo di smistamento di Dijkstra e un algoritmo alternativo ad albero, ma più in particolare copre un algoritmo di sostituzione macro molto semplice che può essere banalmente implementato di fronte a qualsiasi parser ignorante di precedenza:

#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;
}

Invocalo come:

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

Il che è fantastico nella sua semplicità e molto comprensibile.


3
È una bella piccola perla. Ma estenderlo (ad esempio, con applicazione di funzioni, moltiplicazione implicita, operatori di prefisso e suffisso, annotazioni di tipo opzionali, qualsiasi cosa) spezzerebbe l'intera cosa. In altre parole, è un trucco elegante.
Jared Updike

Non vedo il punto. Tutto ciò che fa è cambiare un problema di analisi della precedenza degli operatori in un problema di analisi della precedenza delle parentesi.
Marchese di Lorne

@EJP certo, ma il parser nella domanda gestisce bene le parentesi, quindi questa è una soluzione ragionevole. Se hai un parser che non lo fa, allora hai ragione che questo sposta semplicemente il problema in un'altra area.
Adam Davis

4

Ho pubblicato il codice sorgente per un Java Math Evaluator ultra compatto (1 classe, <10 KiB) sul mio sito web. Questo è un analizzatore di discesa ricorsivo del tipo che ha causato l'esplosione cranica per il poster della risposta accettata.

Supporta la precedenza completa, le parentesi, le variabili con nome e le funzioni con un singolo argomento.




2

Attualmente sto lavorando a una serie di articoli che creano un parser di espressioni regolari come strumento di apprendimento per modelli di progettazione e programmazione leggibile. Puoi dare un'occhiata a readablecode . L'articolo presenta un chiaro utilizzo dell'algoritmo dei cantieri di manovra.


2

Ho scritto un parser di espressioni in F # e ne ho scritto sul blog qui . Utilizza l'algoritmo del cantiere di smistamento, ma invece di convertire da infisso a RPN, ho aggiunto un secondo stack per accumulare i risultati dei calcoli. Gestisce correttamente la precedenza degli operatori, ma non supporta gli operatori unari. L'ho scritto per imparare F #, non per imparare l'analisi delle espressioni, però.


2

Una soluzione Python che utilizza il pyparsing può essere trovata qui . L'analisi della notazione di infisso con vari operatori con precedenza è abbastanza comune, quindi il pyparsing include anche il infixNotation(precedentemente operatorPrecedence) generatore di espressioni. Con esso puoi facilmente definire espressioni booleane usando "AND", "OR", "NOT", per esempio. Oppure puoi espandere la tua aritmetica a quattro funzioni per utilizzare altri operatori, come! per fattoriale, o '%' per modulo, oppure aggiungi operatori P e C per calcolare permutazioni e combinazioni. Potresti scrivere un parser infisso per la notazione di matrice, che include la gestione degli operatori "-1" o "T" (per inversione e trasposizione). L'esempio operatorPrecedence di un parser a 4 funzioni (con '!'.


1

So che questa è una risposta tardiva, ma ho appena scritto un minuscolo parser che consente a tutti gli operatori (prefisso, suffisso e infisso sinistro, infisso destro e non associativo) di avere la precedenza arbitraria.

Lo amplierò per una lingua con supporto DSL arbitrario, ma volevo solo sottolineare che non è necessario un parser personalizzato per la precedenza degli operatori, è possibile utilizzare un parser generalizzato che non necessita affatto di tabelle e cerca solo la precedenza di ogni operatore come appare. Le persone hanno menzionato parser Pratt personalizzati o parser di smistamento che possono accettare input illegali: questo non ha bisogno di essere personalizzato e (a meno che non ci sia un bug) non accetterà input errati. Non è completo in un certo senso, è stato scritto per testare l'algoritmo e il suo input è in una forma che richiederà un po 'di pre-elaborazione, ma ci sono commenti che lo rendono chiaro.

Nota che mancano alcuni tipi comuni di operatori, ad esempio il tipo di operatore utilizzato per l'indicizzazione, ad esempio tabella [indice] o per chiamare una funzione funzione (espressione-parametro, ...) Li aggiungerò, ma pensa a entrambi come suffisso operatori in cui ciò che si trova tra i delimitatori "[" e "]" o "(" e ")" viene analizzato con un'istanza diversa del parser di espressioni. Mi dispiace averlo tralasciato, ma la parte postfissa è inserita - l'aggiunta del resto probabilmente quasi raddoppierà la dimensione del codice.

Dato che il parser è solo 100 righe di codice racket, forse dovrei semplicemente incollarlo qui, spero che non sia più lungo di quanto consentito da stackoverflow.

Alcuni dettagli sulle decisioni arbitrarie:

Se un operatore postfisso con precedenza bassa è in competizione per gli stessi blocchi infissi di un operatore prefisso con precedenza bassa, l'operatore prefisso vince. Questo non si verifica nella maggior parte delle lingue poiché la maggior parte non ha operatori postfissi a bassa precedenza. - per esempio: ((data a) (left 1 +) (pre 2 not) (data b) (post 3!) (left 1 +) (data c)) is a + not b! + c where not is a operatore prefisso e! è un operatore postfisso ed entrambi hanno precedenza inferiore a + quindi vogliono raggruppare in modi incompatibili come (a + non b!) + c o come a + (non b! + c) in questi casi l'operatore prefisso vince sempre, quindi il secondo è il modo in cui analizza

Gli operatori infissi non associativi sono davvero lì in modo da non dover fingere che gli operatori che restituiscono tipi diversi da quelli che ritengono abbiano senso insieme, ma senza avere tipi di espressione diversi per ciascuno è un kludge. Pertanto, in questo algoritmo, gli operatori non associativi rifiutano di associarsi non solo a se stessi ma a qualsiasi operatore con la stessa precedenza. Questo è un caso comune poiché <<= ==> = ecc. Non si associano tra loro nella maggior parte delle lingue.

La questione di come diversi tipi di operatori (sinistra, prefisso, ecc.) Interrompono i legami sulla precedenza è una questione che non dovrebbe sorgere, perché non ha davvero senso dare a operatori di tipi diversi la stessa precedenza. Questo algoritmo fa qualcosa in quei casi, ma non mi preoccupo nemmeno di capire esattamente cosa perché una grammatica del genere è una cattiva idea in primo luogo.

#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

Ecco una semplice soluzione ricorsiva del caso scritta in Java. Nota che non gestisce numeri negativi, ma puoi aggiungerlo se vuoi:

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

L'algoritmo potrebbe essere facilmente codificato in C come parser discendente ricorsivo.

#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;
}

le prossime librerie potrebbero essere utili: yupana - operazioni strettamente aritmetiche; tinyexpr - operazioni aritmetiche + funzioni matematiche C + una fornita dall'utente; mpc - combinatori di parser

Spiegazione

Catturiamo la sequenza di simboli che rappresentano l'espressione algebrica. Il primo è un numero, ovvero una cifra decimale ripetuta una o più volte. Faremo riferimento a tale notazione come regola di produzione.

number -> [0..9]+

L'operatore di addizione con i suoi operandi è un'altra regola. È uno numbero qualsiasi dei simboli che rappresentano la sum "*" sumsequenza.

sum -> number | sum "+" sum

Prova a sostituire numberin sum "+" sumquella sarà number "+" numberche a sua volta potrebbe essere espansa in [0..9]+ "+" [0..9]+quella che alla fine potrebbe essere ridotta a 1+8quale è l'espressione di addizione corretta.

Altre sostituzioni produrranno anche un'espressione corretta: sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

A poco a poco potremmo assomigliare a un insieme di regole di produzione alias grammatica che esprimono tutte le possibili espressioni algebriche.

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

Per controllare la precedenza degli operatori altera la posizione della sua regola di produzione rispetto ad altri. Guarda la grammatica sopra e nota che la regola di produzione per *è posizionata sotto +questo forzerà la productvalutazione prima sum. L'implementazione combina semplicemente il riconoscimento del modello con la valutazione e quindi rispecchia da vicino le regole di produzione.

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);
}

Qui valutiamo termprima e restituiamo se non c'è alcun *carattere dopo di esso, questa è la scelta a sinistra nella nostra regola di produzione altrimenti - valuta i simboli dopo e restituiamo term.value * product.value questa è la scelta giusta nella nostra regola di produzione cioèterm "*" product

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.