Equation (expression) parser with precedence?

前端 未结 23 1505
遇见更好的自我
遇见更好的自我 2020-11-22 11:44

I\'ve developed an equation parser using a simple stack algorithm that will handle binary (+, -, |, &, *, /, etc) operators, unary (!) operators, and parenthesis.

<
相关标签:
23条回答
  • 2020-11-22 12:23

    Is there a language you want to use? ANTLR will let you do this from a Java perspective. Adrian Kuhn has an excellent writeup on how to write an executable grammar in Ruby; in fact, his example is almost exactly your arithmetic expression example.

    0 讨论(0)
  • 2020-11-22 12:24

    The hard way

    You want a recursive descent parser.

    To get precedence you need to think recursively, for example, using your sample string,

    1+11*5
    

    to do this manually, you would have to read the 1, then see the plus and start a whole new recursive parse "session" starting with 11... and make sure to parse the 11 * 5 into its own factor, yielding a parse tree with 1 + (11 * 5).

    This all feels so painful even to attempt to explain, especially with the added powerlessness of C. See, after parsing the 11, if the * was actually a + instead, you would have to abandon the attempt at making a term and instead parse the 11 itself as a factor. My head is already exploding. It's possible with the recursive decent strategy, but there is a better way...

    The easy (right) way

    If you use a GPL tool like Bison, you probably don't need to worry about licensing issues since the C code generated by bison is not covered by the GPL (IANAL but I'm pretty sure GPL tools don't force the GPL on generated code/binaries; for example Apple compiles code like say, Aperture with GCC and they sell it without having to GPL said code).

    Download Bison (or something equivalent, ANTLR, etc.).

    There is usually some sample code that you can just run bison on and get your desired C code that demonstrates this four function calculator:

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

    Look at the generated code, and see that this is not as easy as it sounds. Also, the advantages of using a tool like Bison are 1) you learn something (especially if you read the Dragon book and learn about grammars), 2) you avoid NIH trying to reinvent the wheel. With a real parser-generator tool, you actually have a hope at scaling up later, showing other people you know that parsers are the domain of parsing tools.


    Update:

    People here have offered much sound advice. My only warning against skipping the parsing tools or just using the Shunting Yard algorithm or a hand rolled recursive decent parser is that little toy languages1 may someday turn into big actual languages with functions (sin, cos, log) and variables, conditions and for loops.

    Flex/Bison may very well be overkill for a small, simple interpreter, but a one off parser+evaluator may cause trouble down the line when changes need to be made or features need to be added. Your situation will vary and you will need to use your judgement; just don't punish other people for your sins [2] and build a less than adequate tool.

    My favorite tool for parsing

    The best tool in the world for the job is the Parsec library (for recursive decent parsers) which comes with the programming language Haskell. It looks a lot like BNF, or like some specialized tool or domain specific language for parsing (sample code [3]), but it is in fact just a regular library in Haskell, meaning that it compiles in the same build step as the rest of your Haskell code, and you can write arbitrary Haskell code and call that within your parser, and you can mix and match other libraries all in the same code. (Embedding a parsing language like this in a language other than Haskell results in loads of syntactic cruft, by the way. I did this in C# and it works quite well but it is not so pretty and succinct.)

    Notes:

    1 Richard Stallman says, in Why you should not use Tcl

    The principal lesson of Emacs is that a language for extensions should not be a mere "extension language". It should be a real programming language, designed for writing and maintaining substantial programs. Because people will want to do that!

    [2] Yes, I am forever scarred from using that "language".

    Also note that when I submitted this entry, the preview was correct, but SO's less than adequate parser ate my close anchor tag on the first paragraph, proving that parsers are not something to be trifled with because if you use regexes and one off hacks you will probably get something subtle and small wrong.

    [3] Snippet of a Haskell parser using Parsec: a four function calculator extended with exponents, parentheses, whitespace for multiplication, and constants (like pi and 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
    
    0 讨论(0)
  • 2020-11-22 12:24

    Long time ago, I made up my own parsing algorithm, that I couldn't find in any books on parsing (like the Dragon Book). Looking at the pointers to the Shunting Yard algorithm, I do see the resemblance.

    About 2 years ago, I made a post about it, complete with Perl source code, on http://www.perlmonks.org/?node_id=554516. It's easy to port to other languages: the first implementation I did was in Z80 assembler.

    It's ideal for direct calculation with numbers, but you can use it to produce a parse tree if you must.

    Update Because more people can read (or run) Javascript, I've reimplemented my parser in Javascript, after the code has been reorganized. The whole parser is under 5k of Javascript code (about 100 lines for the parser, 15 lines for a wrapper function) including error reporting, and comments.

    You can find a live demo at 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;
        }    
    }
    
    0 讨论(0)
  • 2020-11-22 12:24

    I would suggest cheating and using the Shunting Yard Algorithm. It's an easy means of writing a simple calculator-type parser and takes precedence into account.

    If you want to properly tokenise things and have variables, etc. involved then I would go ahead and write a recursive descent parser as suggested by others here, however if you simply require a calculator-style parser then this algorithm should be sufficient :-)

    0 讨论(0)
  • 2020-11-22 12:27

    Have you thought about using Boost Spirit? It allows you to write EBNF-like grammars in C++ like this:

    group       = '(' >> expression >> ')';
    factor      = integer | group;
    term        = factor >> *(('*' >> factor) | ('/' >> factor));
    expression  = term >> *(('+' >> term) | ('-' >> term));
    
    0 讨论(0)
  • 2020-11-22 12:27

    I've implemented a recursive descent parser in Java in the MathEclipse Parser project. It could also be used in as a Google Web Toolkit module

    0 讨论(0)
提交回复
热议问题