具有优先级的方程式(表达式)解析器?


104

我已经开发了一种使用简单堆栈算法的方程式解析器,该算法将处理二进制(+,-,|,&,*,/等)运算符,一元(!)运算符和括号。

但是,使用此方法会使我拥有所有具有相同优先级的内容-尽管可以使用括号强制执行优先级,但无论运算符如何,它都是从左到右求值的。

因此,现在“ 1 + 11 * 5”返回60,而不是人们期望的56。

尽管这适用于当前项目,但我希望有一个通用例程,可以用于以后的项目。

为清楚起见进行了编辑:

解析具有优先级的方程的最佳算法是什么?

我对一些易于实现的东西感兴趣,并且了解我可以自己编写代码,以避免可用代码出现许可问题。

语法:

我不懂语法问题-我是用手写的。非常简单,我认为不需要YACC或Bison。我只需要使用诸如“ 2 + 3 *(42/13)”之类的方程式来计算字符串。

语言:

我正在用C进行此操作,但是我对算法感兴趣,而不对特定于语言的解决方案感兴趣。C足够低,可以根据需要轻松转换为另一种语言。

代码示例

我在上面发布了简单表达式解析器测试代码。项目需求发生了变化,因此我不需要针对性能或空间进行代码优化,因为它没有合并到项目中。它采用原始的冗长形式,应该易于理解。如果我在运算符优先级方面做进一步的事情,我可能会选择宏技巧,因为它可以简单地与程序的其余部分匹配。但是,如果我在实际项目中使用过它,那么我将寻求一个更紧凑/更快速的解析器。

相关问题

数学解析器的智能设计?

-亚当


我在博客上用C#写了一个表达式解析器。它在后院码算法中没有堆栈的情况下对后缀进行了中缀。它仅使用数组。
古格

据我了解,您只需要解析算术表达式。使用逆波兰式
mishadoff

Answers:


69

艰难的道路

您需要递归下降解析器

要获得优先权,您需要进行递归思考,例如,使用示例字符串,

1+11*5

要手动执行此操作,您必须先阅读1,然后查看加号并从...开始以全新的方式进行递归解析“会话”,11并确保将解析11 * 5为自己的因子,并生成带有的解析树1 + (11 * 5)

即使尝试解释,这一切都让人感到非常痛苦,尤其是在C变得更加无能为力的情况下。请参阅,在解析11之后,如果*实际上是一个+,则您将不得不放弃创建术语的尝试,而是解析 11本身就是一个因素。我的头已经爆炸了。递归的体面策略是可能的,但是有更好的方法...

简单(正确)的方式

如果您使用像Bison这样的GPL工具,那么您可能不必担心许可问题,因为bison生成的C代码未包含在GPL中(IANAL,但我敢肯定,GPL工具不会强制将GPL用于生成的代码/二进制文件;例如,Apple使用GCC编译了诸如Aperture之类的代码,而他们无需GPL所说的代码就可以出售它)。

下载野牛(或等效的工具,ANTLR等)。

通常有一些示例代码,您可以在其上运行bison并获得所需的C代码,以演示这四个功能的计算器:

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

查看生成的代码,发现这并不像听起来那样简单。同样,使用像Bison这样的工具的优点是:1)学到一些东西(特别是如果您阅读了Dragon书籍并学习了语法),2)避免了NIH尝试重新发明轮子。使用真正的解析器生成器工具,您实际上希望以后可以扩展,向其他人展示解析器是解析工具的领域。


更新:

这里的人们提供了很多合理的建议。关于避免跳过解析工具或仅使用Shunting Yard算法或手动递归递归体面解析器的唯一警告是,玩具语言1可能有一天会变成具有功能(正弦,余弦,对数)以及变量,条件和条件的大型实际语言。循环。

对于小型,简单的解释器而言,Flex / Bison可能会显得过大,但是当需要进行更改或需要添加功能时,脱离分析器+评估器可能会带来麻烦。您的情况会有所不同,您将需要运用自己的判断力;只是不要因为你的罪惩罚别人[2]并建立一个不够充分的工具。

我最喜欢的解析工具

世界上最好的工具是Parsec库(用于递归的体面解析器),该库随附了编程语言Haskell。它看起来很像BNF,或者像某种专用工具或领域特定的语言进行解析(示例代码[3]),但实际上它只是Haskell中的一个常规库,这意味着它与其余代码在同一构建步骤中进行编译,您可以编写任意的Haskell代码并在解析器中调用它,也可以在同一代码中混合和匹配其他所有库。(顺便说一下,将这样的解析语言嵌入Haskell之外的语言中会导致大量的语法混乱。我在C#中做到了这一点,并且效果很好,但它并不那么简洁。)

笔记:

1 Richard Stallman在“ 为什么不应该使用Tcl”中说

Emacs的主要教训是扩展语言不应该仅仅是“扩展语言”。它应该是一种真正的编程语言,旨在编写和维护大量程序。因为人们会想这样做!

[2]是的,使用该“语言”让我永远感到害怕。

还要注意,当我提交此条目时,预览是正确的,但是SO不足以解析我在第一段中的紧密锚标签,从而证明解析器不是一件容易的事,因为如果您使用正则表达式和一些hacks,您可能会出现一些细微的错误

[3]使用Parsec的Haskell解析器的代码段:一个四函数计算器,扩展了指数,括号,用于乘法的空格和常量(如pi和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
为了强调我的观点,请注意我的帖子中的标记未正确解析(这在静态呈现的标记和在WMD预览中呈现的标记之间有所不同)。已经进行了几次尝试来修复它,但是我认为解析器是错误的。帮个忙,让解析正确!
Jared Updike 2009年

155

调度场算法是这样的工具。维基百科对此确实感到困惑,但是基本上该算法的工作原理如下:

假设您要评估1 + 2 * 3 +4。直觉上,您“知道”您必须首先进行2 * 3,但是如何获得此结果?关键是要认识到,当您从左到右扫描字符串时,当跟随它的运算符的优先级较低(或等于)时,您将评估一个运算符。在该示例的上下文中,这是您要执行的操作:

  1. 看一下:1 + 2,什么也不做。
  2. 现在看1 + 2 * 3,仍然什么也不做。
  3. 现在来看1 + 2 * 3 + 4,现在您知道必须评估2 * 3,因为下一个运算符的优先级较低。

您如何实施呢?

您希望有两个堆栈,一个用于数字,另一个用于运算符。您一直将数字推入堆栈。您将每个新运算符与堆栈顶部的运算符进行比较,如果堆栈顶部的运算符具有更高的优先级,则将其从运算符堆栈中弹出,从数字堆栈中弹出操作数,应用运算符并推送结果到数字堆栈上。现在,您使用堆栈运算符的顶部重复比较。

回到该示例,它的工作方式如下:

N = [] Ops = []

  • 读取1. N = [1],Ops = []
  • 阅读+。N = [1],操作数= [+]
  • 读取2。N = [1 2],Ops = [+]
  • 阅读*。N = [1 2],操作数= [+ *]
  • 读3. N = [1 2 3],Ops = [+ *]
  • 阅读+。N = [1 2 3],操作= [+ *]
    • 弹出3,2并执行2 *3,然后将结果压入N。N = [1 6],Ops = [+]
    • +保持关联状态,因此您也要同时弹出1、6并执行+。N = [7],操作数= []。
    • 最后,将[+]推入运算符堆栈。N = [7],操作数= [+]。
  • 读4. N = [7 4]。行动= [+]。
  • 您的输入用完了,所以现在要清空堆栈。在其上您将得到结果11。

在那里,不是那么困难,不是吗?而且,它不会调用任何语法或解析器生成器。


6
您实际上不需要两个堆栈,只要您可以在不弹出顶部的情况下看到堆栈上的第二个东西即可。相反,您可以使用一个交替使用数字和运算符的堆栈。实际上,这恰好对应于LR解析器生成器(例如bison)所做的事情。
克里斯·多德

2
我现在刚刚实现的算法的解释非常好。另外,您没有将其转换为后缀,这也很好。添加对括号的支持也非常容易。
Giorgi 2010年

4
可以在此处找到shunting -yard算法的简化版本:andreinc.net/2010/10/05/…(具有Java和python的实现)
Andrei Ciobanu 2010年

1
谢谢,这正是我的追求!
乔·格林

非常感谢您提及左-关联。我坚持使用三元运算符:如何使用嵌套的“?:”解析复杂的表达式。我意识到两者都是“?” 和':'必须具有相同的优先级。如果我们解释“?” 作为右-关联和':'作为左-关联,此算法与它们配合得很好。另外,只有当两个算子都处于关联状态时,我们才可以折叠它们。
弗拉迪斯拉夫

25

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

不同方法的很好解释:

  • 递归下降识别
  • 调车场算法
  • 经典解决方案
  • 优先攀登

用简单的语言和伪代码编写。

我喜欢“优先攀登”之一。


链接似乎已断开。最好的答案是改写每种方法,以便当该链接消失时,一些有用的信息将保留在此处。
亚当·怀特

18

这里有一篇不错的文章关于将简单的递归下降解析器与运算符优先级解析结合在一起。如果您最近一直在编写解析器,那么阅读它应该非常有趣且有启发性。


16

很久以前,我制定了自己的解析算法,这在任何有关解析的书(例如《龙书》)中都找不到。查看指向Shunting Yard算法的指针,我确实看到了相似之处。

大约2年前,我在http://www.perlmonks.org/?node_id=554516上发布了有关Perl源代码的文章。移植到其他语言很容易:我做的第一个实现是在Z80汇编器中。

它是使用数字直接计算的理想选择,但是如果需要,您可以使用它来生成解析树。

更新资料因为更多的人可以阅读(或运行)Javascript,所以在重新组织代码之后,我在Javascript中重新实现了解析器。整个解析器使用的Java代码不足5k(解析器大约100行,包装函数大约15行),包括错误报告和注释。

您可以在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

如果可以描述您当前正在使用的语法,这将有所帮助。听起来问题可能出在这里!

编辑:

您不理解语法问题,并且“您已经手工编写了”这一事实很可能解释了为什么您对格式为“ 1 + 11 * 5”的表达式(例如,运算符优先级)有疑问。例如,对“算术表达式的语法”进行谷歌搜索应该会产生一些好的指针。这样的语法不必太复杂:

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

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

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

例如,可以做到这一点,并且可以进行微不足道的扩展,以处理一些更复杂的表达式(例如,包括函数或幂,...)。

我建议您例如看一下线程。

几乎所有语法/解析介绍都以算术表达式为例。

请注意,使用语法根本不意味着使用特定工具(如la Yacc,Bison等)。实际上,您肯定已经在使用以下语法:

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

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

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

(或类似的东西)而不知道!


8

您是否考虑过使用Boost Spirit?它使您可以像这样在C ++中编写类似EBNF的语法:

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

1
+1结果是,一切都是Boost的一部分。计算器的语法在此处:spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/…。计算器的实现是在这里:spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/...。和文档是在这里:spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/...。我永远不会理解为什么人们仍然在那里实现自己的微型解析器。
斯蒂芬·

5

当您提出问题时,就无需进行递归。答案是三件事:Postfix表示法加上Shunting Yard算法加上Postfix表达式求值:

1)。后缀符号=发明是为了消除对显式优先级规范的需要。在网上阅读更多内容,但要点如下:中缀表达式(1 + 2)* 3虽然对于人类来说很容易阅读和处理,但对于通过计算机进行的计算不是很有效。什么是?简单的规则说:“通过优先缓存来重写表达式,然后总是从左到右处理它”。因此,中缀(1 + 2)* 3变为后缀12 + 3 *。POST,因为运算符始终放在操作数之后。

2)。评估后缀表达式。简单。从后缀字符串中读取数字。将它们推入堆栈,直到看到操作员为止。检查运算符类型-一元?二进制的?第三?根据需要从堆栈中弹出尽可能多的操作数以评估此运算符。评估。将结果推回堆栈!我们几乎完成了。继续这样做,直到堆栈只有一个条目=您正在寻找的值。

让我们做(1 + 2)* 3,它在后缀中是“ 12 + 3 *”。读取第一个数字=1。将其压入堆栈。继续阅读。Number =2。将其推入堆栈。继续阅读。操作员。哪一个?+。哪一种?Binary =需要两个操作数。弹出堆栈两次= argright为2,argleft为1。1+ 2为3。将3推回堆栈。从后缀字符串中读取下一个。它的数量。3.推。继续阅读。操作员。哪一个?*。哪一种?二进制=需要两个数字->两次弹出堆栈。首先弹出argright,第二次弹出argleft。评估操作-3乘3等于9.将9推入堆栈。阅读下一个后缀char。是空的 输入结束。弹出堆栈onec =这就是您的答案。

3)。Shunting Yard用于将人类(易于)阅读的中缀表达式转换为后缀表达(经过一些实践,人类也易于阅读)。易于手动编码。见上面的注释和净。


4

您想使用一种语言吗? ANTLR将使您从Java的角度进行操作。Adrian Kuhn 在如何用Ruby编写可执行语法方面有出色的著作。实际上,他的例子几乎就是您的算术表达式例子。


我必须承认,我在博客文章中给出的示例向左递归错误,即a-b-c的求值为(a-(b -c))而不是((a -b)-c)。实际上,这使我想起了一个应该修复博客文章的待办事项。
阿库恩

4

这取决于您希望它有多“通用”。

如果您希望它真的很通用,例如能够解析数学函数,例如sin(4 + 5)* cos(7 ^ 3),则可能需要一个 解析树。

其中,我认为不适合在此处粘贴完整的实现。我建议您看看其中一本臭名昭著的《龙书》》。

但是,如果您只是想要优先支持,则可以通过将表达式转换为后缀形式来实现,在这种形式中,可以从Google获得可以复制粘贴的算法,或者我认为您可以使用二进制代码自行编写树。

当您以postfix形式使用它时,从那时起这只是小菜一碟,因为您已经了解了堆栈如何提供帮助。


对于表达式评估器来说,这本龙书可能有点多余-只需一个简单的递归下降解析器,但是如果您想在编译器中做更多的事情,那是一本必读的书。

1
哇-很高兴得知《龙书》仍在讨论中。我记得30年前在大学学习过它,并通读了所有内容。
Schroedingers Cat

4

我建议作弊并使用分流场算法。这是编写简单的计算器类型的解析器的一种简便方法,并且考虑了优先级。

如果您想正确地标记事物并涉及变量等,那么我将继续按照此处其他人的建议编写递归下降解析器,但是,如果您只需要计算器样式的解析器,则此算法就足够了:-)


4

我在PIClist上找到有关Shunting Yard算法的信息

哈罗德写道:

我记得很久以前读过一种算法,该算法将代数表达式转换为RPN以便于评估。每个中缀值或运算符或括号均由轨道上的有轨电车表示。一种汽车分裂成另一条轨道,另一种继续向前行驶。我不记得详细信息(很明显!),但始终认为编写代码会很有趣。当我编写6800(不是68000)汇编代码时,这又回来了。

这就是“调车场算法”,这是大多数机器解析器使用的方法。请参阅Wikipedia中有关解析的文章。编码调车场算法的一种简单方法是使用两个堆栈。一个是“推”堆栈,另一个是“减少”或“结果”堆栈。例:

pstack =()//空的rstack =()输入:1 + 2 * 3优先级= 10 //最低reduce = 0 //不减少

start:令牌'1':isnumber,放入pstack(push)令牌'+':isoperator设置priority = 2,如果priority <previous_operator_precedence然后reduce()//参见下文将'+'放入pstack(push)令牌'2' :isnumber,放入pstack(push)令牌'*':isoperator,设置优先级= 1,放入pstack(push)//检查优先级// //在令牌'3'上方:isnumber,放在pstack(push)末尾输入,需要减少(目标为空pstack)reduce()//完成

为了减少从推送堆栈中弹出的元素并将它们放入结果堆栈中,如果它们的格式为'operator''number',请始终交换pstack上的前2个元素:

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

如果表达式是:

1 * 2 + 3

那么reduce触发器将是令牌'+'的读取,该令牌的优先级比已经推送的'*'低,因此它应该这样做:

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

然后先按“ +”再按“ 3”,最后减小:

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

因此,简短的版本是:推入数字,当推入运算符时检查前一个运算符的优先级。如果它高于现在要推的运算符,请先减小,然后再推当前运算符。要处理parens,只需保存“ previous”运算符的优先级,然后在pstack上标记一个标记,告诉reduce算法在解决paren对的内部时停止还原。结束括号与输入结束一样触发缩减,并且还从pstack中删除了断开括号标记,并恢复了“先前的操作”优先级,因此可以在关闭的结束处继续解析。这可以通过递归或不使用递归来完成(提示:遇到'('...)时,使用堆栈存储先前的优先级。通用的版本是使用解析器生成器实现的调车场算法f.ex。使用yacc或野牛或taccle(tacc的tcl类似物)。

彼得

-亚当


4

优先级解析的另一个资源是Wikipedia上的Operator-precedence解析器条目。涵盖了Dijkstra的调车场算法和树替代算法,但更值得注意的是涵盖了一个非常简单的宏替换算法,该算法可以在任何优先级无知解析器之前轻松实现:

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

调用为:

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

它的简单性很棒,而且非常容易理解。


3
那是一颗不错的小珍珠。但是扩展它(例如,使用函数应用程序,隐式乘法,前缀和后缀运算符,可选的类型注释等)将破坏整件事。换句话说,这是一个优雅的技巧。
Jared Updike,2009年

我不明白这一点。这一切都是将运算符优先级解析问题更改为括号优先级解析问题。
罗恩侯爵

@EJP可以肯定,但是问题中的解析器可以很好地处理括号,因此这是一个合理的解决方案。但是,如果您有一个解析器没有,那么您是正确的,这只会将问题移至另一个区域。
亚当·戴维斯

4

我已经发布了一个超紧凑型(1类,<10 KiB)Java Math Evaluator的源代码。这是一种递归下降解析器,其类型为引起接受答案的海报引起颅骨爆炸。

它支持完整的优先级,括号,命名变量和单参数函数。




2

我目前正在撰写有关构建正则表达式解析器作为设计模式和可读程序学习工具的系列文章。您可以看一下可读代码。本文提出了调车码算法的明确用法。


2

我用F#编写了一个表达式解析器,并在此处发布了博客。它使用了调车场算法,但是我没有从infix转换为RPN,而是添加了第二个堆栈来累积计算结果。它可以正确处理运算符优先级,但不支持一元运算符。我写这本书是为了学习F#,而不是为了学习表达式解析。


2

这里可以找到使用pyparsing的Python解决方案。优先使用各种运算符来解析中缀表示法是相当普遍的,因此pyparsing还包括infixNotation(以前是operatorPrecedence)表达式生成器。使用它,您可以轻松地使用例如“ AND”,“ OR”,“ NOT”来定义布尔表达式。或者,您可以扩展四功能算术以使用其他运算符,例如!对于阶乘,对于模数为“%”,或者添加P和C运算符以计算排列和组合。您可以编写一个用于矩阵符号的中缀解析器,其中包括对“ -1”或“ T”运算符的处理(用于反转和转置)。四功能解析器的operatorPrecedence示例(带有“!”


1

我知道这是一个较晚的答案,但我刚刚编写了一个微型解析器,该解析器允许所有运算符(前缀,后缀和infix-left,infix-right和非关联的)具有任意优先级。

我打算将其扩展为一种具有任意DSL支持的语言,但我只想指出,一个操作符优先级不需要自定义解析器,一个可以使用完全不需要表的通用解析器,并且只是查找每个运算符的优先级。人们一直在提到可以接受非法输入的定制Pratt解析器或调车场解析器-不需要自定义该输入,并且(除非有错误)不会接受错误的输入。从某种意义上说,它不是完整的,它是为测试算法而编写的,其输入形式需要进行一些预处理,但是有一些注释可以使之清楚。

注意缺少一些常见的运算符,例如用于索引的运算符,例如table [index]或调用函数function(parameter-expression,...),我将添加它们,但都将它们视为后缀用表达式解析器的另一个实例解析运算符'['和']'或'('和')'之间的运算符。抱歉,省略了该内容,但后缀部分加入了-添加其余部分可能会使代码大小几乎增加一倍。

由于解析器仅是100行球拍代码,也许我应该将其粘贴在这里,所以我希望它的长度不超过stackoverflow允许的范围。

有关任意决定的一些细节:

如果低优先级后缀运算符与低优先级前缀运算符竞争相同的中缀块,则前缀运算符获胜。由于大多数语言没有低优先级的后缀运算符,因此在大多数语言中都不会出现这种情况。-例如:((data a)(left 1 +)(pre 2 not)(data b)(post 3!)(left 1 +)(data c))是a + not b!+ c其中not是a前缀运算符和!是后缀运算符,并且两者的优先级都低于+,因此它们希望以不兼容的方式分组为(a + not b!)+ c或a +(not b!+ c),在这种情况下,前缀运算符始终会获胜,因此第二是解析方式

非关联的infix运算符确实存在,因此您不必假装返回不同类型的运算符在一起就有意义,但是每种表达式都没有不同的表达式类型,这是很麻烦的。这样,在该算法中,非关联运算符不仅拒绝与自己关联,而且拒绝与具有相同优先级的任何运算符关联。这是常见的情况,因为<<= ==> =等在大多数语言中都不相互关联。

不同类型的运算符(左,前缀等)如何断开优先级关系是一个不应该提出的问题,因为为不同类型的运算符赋予相同的优先级实际上是没有意义的。在这种情况下,该算法会执行某些操作,但是我什至不必去弄清楚到底是什么,因为这种语法首先是一个坏主意。

#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

这是一个用Java编写的简单案例递归解决方案。请注意,它不处理负数,但是如果您愿意,可以添加:

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

算法可以很容易地在C中编码为递归下降解析器。

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

下一个库可能有用: yupana-严格的算术运算; tinyexpr-算术运算+ C数学函数+用户提供的一种; MPC-解析器组合器

说明

让我们捕获代表代数表达式的符号序列。第一个是数字,是重复一次或多次的十进制数字。 我们将这种表示法称为生产规则。

number -> [0..9]+

加法运算符及其操作数是另一条规则。它是number代表sum "*" sum序列的任何符号。

sum -> number | sum "+" sum

尝试将其替换numbersum "+" sum将被number "+" number扩展为[0..9]+ "+" [0..9]+最终可被缩减1+8为正确的加法表达式的那个。

其他替换也将产生正确的表达式:sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

我们可以一点一点地类似于表达所有可能的代数表达式的一组生产规则(又称为语法)

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

为了控制操作员优先级,可以更改其生产规则相对于其他规则的位置。看上面的语法,请注意,将生产规则*放在下面,+将强制product评估之前sum。实施仅将模式识别与评估结合在一起,从而紧密反映了生产规则。

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

在这里,我们term首先评估,如果没有*字符,则返回它,否则将在我们的生产规则中将留给选择 -评估符号后,在我们的生产规则中将其返回term.value * product.value 为正确的选择,即term "*" product

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.