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

时间:2008-08-26 14:52:05

标签: algorithm parsing equation

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

然而,使用这种方法会让我拥有相同优先级的所有内容 - 无论操作符如何,都会从左到右进行评估,尽管可以使用括号强制执行优先级。

所以现在“1 + 11 * 5”会返回60,而不是56,正如人们所期望的那样。

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

为清晰起见编辑:

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

我对实现起来很简单并且理解我可以自行编码以避免使用可用代码的许可问题感兴趣。

语法:

我不懂语法问题 - 我是手写的。这很简单,我认为不需要YACC或Bison。我只需要用“2 + 3 *(42/13)”等方程计算字符串。

语言

我在C中这样做,但我对算法感兴趣,而不是语言特定的解决方案。 C足够低,如果需要,很容易转换成另一种语言。

代码示例

我发布了我上面谈到的test code for the simple expression parser。项目要求发生了变化,因此我从不需要优化性能或空间代码,因为它没有包含在项目中。它是原始的详细形式,应该易于理解。如果我在运算符优先级方面做了更多的事情,我可能会选择the macro hack,因为它简单地匹配程序的其余部分。但是,如果我在一个真实的项目中使用它,我将寻求一个更紧凑/更快速的解析器。

相关问题

  

Smart design of a math parser?

- 亚当

24 个答案:

答案 0 :(得分:150)

shunting yard algorithm是正确的工具。维基百科对此非常困惑,但基本上算法的工作原理如下:

说,你要评估1 + 2 * 3 + 4.直观地说,你“知道”你必须先做2 * 3,但是你怎么得到这个结果?关键是要意识到当你从左到右扫描字符串时,当跟随的运算符具有较低(或等于)优先级时,您将评估运算符。在示例的上下文中,这是您想要做的:

  1. 看看:1 + 2,什么都不做。
  2. 现在看1 + 2 * 3,仍然没有做任何事情。
  3. 现在看1 + 2 * 3 + 4,现在你知道必须评估2 * 3,因为下一个运算符的优先级较低。
  4. 你是如何实现这个的?

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

    回到这个例子,它的工作原理如下:

    N = [] Ops = []

    • 阅读1. N = [1],Ops = []
    • 阅读+。 N = [1],Ops = [+]
    • 阅读2. N = [1 2],Ops = [+]
    • 阅读*。 N = [1 2],Ops = [+ *]
    • 阅读3. N = [1 2 3],Ops = [+ *]
    • 阅读+。 N = [1 2 3],Ops = [+ *]
      • 弹出3,然后执行2 * 3,并将结果推到N. N = [1 6],Ops = [+]
      • +是左关联的,所以你想要关闭1和6并执行+。 N = [7],Ops = []。
      • 最后将[+]推到操作员堆栈上。 N = [7],Ops = [+]。
    • 阅读4. N = [7 4]。 Ops = [+]。
    • 你的输入已用完,所以你想立即清空堆栈。您将获得结果11。

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

答案 1 :(得分:68)

艰难的方式

您需要recursive descent parser

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

1+11*5

要手动执行此操作,您必须阅读1,然后查看加号并从11开始一个全新的递归解析“会话”...并确保解析11 * 5进入自己的因素,产生一个1 + (11 * 5)的解析树。

即使尝试解释这一切也感到非常痛苦,特别是在C的额外无能为力的情况下。在解析11之后,如果*实际上是+而不是,你将不得不放弃尝试制定一个术语和而是将11本身解析为一个因素。我的脑袋已经爆炸了。这有可能采用递归的体面策略,但还有更好的方法......

简单(正确)方式

如果您使用像Bison这样的GPL工具,您可能不需要担心许可问题,因为Bison生成的C代码不在GPL中(IANAL,但我很确定GPL工具不强制关于生成的代码/二进制文件的GPL;例如,Apple编译代码,例如Aperture和GCC,并且无需使用GPL代码即可将其出售。

Download Bison(或等效的东西,ANTLR等)。

通常会有一些示例代码可以运行bison并获得所需的C代码,以演示这四个函数计算器:

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

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


<强>更新

这里的人提供了很多合理的建议。我反对跳过解析工具或仅使用Shunting Yard算法或手动滚动递归正确解析器的唯一警告是,有一天玩具语言 1 可能变成具有函数的大型实际语言(罪恶) ,cos,log)和变量,条件和for循环。

Flex / Bison对于一个简单的小型解释器来说可能会有点过分,但是当需要进行更改或需要添加功能时,一个解析器+评估器可能会导致线路故障。你的情况会有所不同,你需要用你的判断;只是不要punish other people for your sins [2] 并构建一个不够充分的工具。

我最喜欢的解析工具

世界上最适合这项工作的工具是Parsec库(用于递归代码解析器),它带有编程语言Haskell。它看起来很像BNF,或者像解析的一些专用工具或领域特定语言(示例代码[3]),但它实际上只是Haskell中的常规库,这意味着它在同一个构建中编译作为Haskell代码的其余部分,您可以编写任意Haskell代码并在解析器中调用它,并且可以在同一代码中混合和匹配其他库 。 (顺便说一句,在Haskell之外的语言中嵌入这样的解析语言会导致句法错误。我在C#中做到这一点并且效果很好,但它不是那么漂亮和简洁。)

备注:

1 Richard Stallman在Why you should not use Tcl

中说
  

Emacs的主要教训是   扩展语言不应该   仅仅是一种“扩展语言”。它   应该是一种真正的编程语言,   专为写作和维护而设计   实质性计划。因为人   我想这样做!

[2]是的,我永远不会使用那种“语言”。

另请注意,当我提交此条目时,预览是正确的,但 SO不足以解析我在第一段上的关闭锚标记,证明解析器不值得被嘲笑因为如果你使用正则表达式和一次攻击你可能会得到一些微妙和小错误

[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

答案 2 :(得分:24)

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

对不同方法的非常好的解释:

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

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

我喜欢'优先攀登'一个。

答案 3 :(得分:17)

关于将简单的递归下降解析器与运算符优先级解析相结合,有一篇很好的文章here。如果您最近一直在编写解析器,那么阅读它应该非常有趣和有益。

答案 4 :(得分:15)

很久以前,我编写了自己的解析算法,在任何有关解析的书籍中都找不到(比如Dragon Book)。看看Shunting Yard算法的指针,我确实看到了相似之处。

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

这对于使用数字进行直接计算是理想的,但如果必须,可以使用它来生成解析树。

更新因为更多人可以阅读(或运行)Javascript,所以在重新组织代码之后,我已经在Javascript中重新实现了我的解析器。整个解析器不到5k的Javascript代码(解析器约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;
    }    
}

答案 5 :(得分:10)

如果您能描述当前用于解析的语法,将会有所帮助。听起来问题可能就在那里!

编辑:

你不理解语法问题并且'你用手写的'这一事实很可能解释了为什么你会遇到'1 + 11 * 5'形式的问题(即,运算符优先级)。例如,谷歌搜索“算术表达式的语法”应该会产生一些好的指针。这样的语法不需要复杂:

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

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

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

可以做到这一点,并且可以通过简单的扩充来处理一些更复杂的表达式(包括函数,例如函数或函数......)。

我建议您查看this帖子,例如。

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

请注意,使用语法并不意味着使用特定工具( a la Yacc,Bison,...)。实际上,你肯定已经使用了以下语法:

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

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

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

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

答案 6 :(得分:9)

您是否考虑过使用Boost Spirit?它允许你用C ++编写类似EBNF的语法:

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

答案 7 :(得分:5)

当你提出问题时,不需要任何递归。答案是三件事:Postfix符号加Shunting Yard算法加Postfix表达式评估:

1)。 Postfix符号=发明以消除对显式优先级规范的需要。在网上阅读更多内容,但这里是它的要点:中缀表达式(1 + 2)* 3,同时人们很容易阅读和处理通过机器计算效率不高。什么是?简单的规则,即“通过优先级缓存重写表达式,然后始终从左到右处理”。因此,中缀(1 + 2)* 3成为后缀12 + 3 *。 POST因为操作符总是放在操作数之后。

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

让后缀中的(1 + 2)* 3为“12 + 3 *”。读取第一个数字= 1.将其推入堆栈。读下一个。 Number = 2.将其推入堆栈。读下一个。操作员。哪一个? +。哪一种?二进制=需要两个操作数。弹出堆栈两次= argright为2,argleft为1. 1 + 2为3.将3推回堆栈。从postfix字符串开始阅读。它的数量。 3.Push。读下一个。操作员。哪一个? *。哪一种?二进制=需要两个数字 - &gt;弹出堆栈两次。首先进入argright,第二次进入argleft。评估操作 - 3次3是9.Push 9在堆栈上。阅读下一个postfix char。它是空的。输入结束。 Pop stack onec =那是你的答案。

3)。 Shunting Yard用于将人(易)可读的中缀表达式转换为后缀表达式(在某些练习后人类也很容易阅读)。易于手动编码。见上面的评论和网络。

答案 8 :(得分:4)

您想使用哪种语言? {J}允许您从Java角度执行此操作。关于如何在Ruby中编写可执行语法,Adrian Kuhn有一个很好的ANTLR;事实上,他的例子几乎就是你的算术表达式例子。

答案 9 :(得分:4)

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

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

答案 10 :(得分:4)

我在PIClist上找到了关于Shunting Yard algorithm

的内容
  哈罗德写道:

     
    

我记得很久以前读过一个转换过的算法     RPN的代数表达式,便于评估。每个中缀值或     操作员或括号由一辆铁路车代表     跟踪。一     汽车的类型分裂到另一个轨道,另一个继续直     先。我不记得细节(显然!),但总是这么想     代码会很有趣。这是我写6800时的回归(不是     68000)汇编代码。

  
     

这是“调车场algorythm”   它是大多数机器解析器   使用。请参阅有关解析的文章   维基百科。一种简单的编码方式   调车场algorythm是用两个   栈。一个是“推”堆栈和   另一个是“减少”或“结果”   堆。例如:

     

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

     

start:token'1':isnumber,put in   pstack(push)token'+':isoperator   如果优先级&lt;设置优先级= 2   previous_operator_precedence然后   reduce()//见下面把'+'放进去   pstack(push)token'2':isnumber,   放入pstack(推)令牌'*':   isoperator,set precedence = 1,put in   pstack(push)//检查优先级为   //上面标记'3':isnumber,放入   pstack(推)输入结束,需要   reduce(目标是空的pstack)reduce()   //完成

     

减少,推送弹出元素   堆叠并将它们放入结果中   堆栈,始终交换前2项   如果它们是形式的pstack   '运营商''号码':

     

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

     

如果表达式是:

     

1 * 2 + 3

     

然后减少触发器就会有   一直在阅读令牌'+'   其中的比例低于   '*'已被推,所以它会有   完成:

     

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

     

然后按'+'然后按'3'和   然后终于减少了:

     

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

     

所以简短的版本是:推送数字,   当推动操作员检查   前一个运算符的优先级。   如果它高于运营商的   首先,现在要推动   减少,然后推动电流   运营商。处理parens只需保存   'previous'的优先顺序   操作员,并在pstack上加上标记   告诉减少algorythm   解决内部时停止减少   一对父母。闭幕式   最终会触发减少   输入,并删除打开   来自pstack的paren mark,和   恢复'上一次操作'   优先级,因此解析可以继续   在它离开后关闭的paren之后   关闭。这可以通过递归来完成   或没有(提示:使用堆栈存储   以前的优先顺序   遇到'('...)。该   这是一般化的使用方法   一个解析器生成器实现   shunting yard algorythm,f.ex。运用   yacc或bison或taccle(tcl类似的   YACC)。

     

彼得

- 亚当

答案 11 :(得分:4)

我建议作弊并使用Shunting Yard Algorithm。这是编写简单的计算器类型解析器的简单方法,并且优先考虑。

如果你想要正确地标记事物并且有变量等,那么我会继续按照其他人的建议写一个递归下降解析器,但是如果你只需要一个计算器式解析器,那么这个算法就足够了: - )

答案 12 :(得分:4)

这取决于你想要它的“一般”程度。

如果你想要它真的非常通用,比如能够像sin(4 + 5)* cos(7 ^ 3)一样解析数学函数,你可能需要一个解析树。

其中,我认为完整的实施不适合粘贴在这里。我建议您查看一个臭名昭着的“Dragon book”。

但是如果你只想要优先支持,那么你可以通过首先将表达式转换为postfix形式来实现这一点,在这种形式中你可以从{{3获得可以复制和粘贴的算法或者我认为你可以用二叉树自己编写代码。

如果你有后缀形式,那么从那时起它就是一块蛋糕,因为你已经了解了堆栈的帮助。

答案 13 :(得分:3)

优先解析的另一个资源是维基百科上的Operator-precedence parser条目。涵盖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))))

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

答案 14 :(得分:3)

我在Dijkstra's Shunting Yard的条款下发布了基于Apache License 2.0算法的表达式解析器:

http://projects.congrace.de/exp4j/index.html

答案 15 :(得分:2)

我在F#和blogged about it here中编写了一个表达式解析器。它使用了分流码算法,但不是从中缀转换为RPN,而是添加了第二个堆栈来累积计算结果。它正确处理运算符优先级,但不支持一元运算符。我写这篇文章是为了学习F#,不是为了学习表达式解析。

答案 16 :(得分:2)

我目前正在撰写一系列文章,构建一个正则表达式解析器,作为设计模式和可读编程的学习工具。你可以看看readablecode。本文明确使用了分流码算法。

答案 17 :(得分:2)

我在recursive descent parser项目中用Java实现了MathEclipse Parser。它也可以用作Google Web Toolkit模块

答案 18 :(得分:2)

可以找到使用pyparsing的Python解决方案here。使用具有优先级的各种运算符解析中缀表示法是相当常见的,因此pyparsing还包括infixNotation(以前的operatorPrecedence)表达式构建器。有了它,您可以使用“AND”,“OR”,“NOT”轻松定义布尔表达式。或者您可以扩展您的四函数算术以使用其他运算符,例如!对于阶乘,或'%'表示模数,或添加P和C运算符来计算排列和组合。你可以为矩阵表示法编写一个中缀解析器,包括处理'-1'或'T'运算符(用于反转和转置)。 4函数解析器的operatorPrecedence示例(带有'!'以获得乐趣)是here,而功能更全面的解析器和赋值器是here

答案 19 :(得分:1)

我知道这是一个迟到的答案,但我刚刚编写了一个小的解析器,允许所有运算符(前缀,后缀和中缀 - 左,中缀 - 右和非关联)具有任意优先级。

我将扩展这个支持任意DSL的语言,但我只是想指出一个不需要自定义解析器来实现运算符优先级,可以使用不需要表格的通用解析器全部,只查看每个运算符的优先级。人们一直在提到可以接受非法输入的自定义Pratt解析器或分流码解析器 - 这个不需要定制,并且(除非有错误)不接受错误的输入。它在某种意义上是不完整的,它是为了测试算法而编写的,它的输入采用的形式需要一些预处理,但是有一些注释可以说清楚。

注意缺少一些常见类型的运算符,例如用于索引的运算符类型,即table [index]或调用函数函数(parameter-expression,...) 我将添加这些,但将两者都视为后缀运算符,其中在分隔符'['和']'或'('和')'之间的内容使用表达式解析器的不同实例进行解析。很抱歉没有把它排除在外,但是后缀部分正在进行中 - 其余部分可能几乎是代码大小的两倍。

由于解析器只有100行的球拍代码,或许我应该把它粘贴到这里,我希望这不会超过stackoverflow允许的。

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

如果低优先级后缀运算符与低优先级前缀运算符竞争相同的中缀块,则前缀运算符将获胜。这在大多数语言中都没有出现,因为大多数语言都没有低优先级的后缀运算符。 - 例如:((数据a)(左1 +)(前2没有)(数据b)(后3!)(左1 +)(数据c)) 是+不是b!+ c其中not是前缀运算符而且!是后缀运算符,两者都有较低的 优先级高于+所以他们希望以不兼容的方式分组为     (a +不是b!)+ c 或者作为      a +(不是b!+ c) 在这些情况下,前缀运算符总是获胜,所以第二种是它解析的方式

非关联中缀运算符确实存在,因此您不必假装返回不同类型的运算符一起使用,但没有不同的表达类型,每个运算符都是kludge。因此,在该算法中,非关联运算符拒绝不仅与自身关联,而且与任何具有相同优先级的运算符关联。这是一个常见的情况,因为&lt; &lt; = ==&gt; =等在大多数语言中不相互关联。

不同类型的运算符(左,前缀等)如何打破优先级关系的问题是不应该出现的问题,因为给不同类型的运算符赋予相同的优先级并不是真的有意义。这种算法在这些情况下做了一些事情,但我甚至都不打算弄清楚究竟是什么,因为这样的语法首先是一个坏主意。

#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)

答案 20 :(得分: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;
}

}

答案 21 :(得分:0)

算法可以很容易地在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

尝试将number替换为sum "+" sum number "+" number[0..9]+ "+" [0..9]+又可以扩展为1+8,最终可以缩减为sum "+" sum这是正确的另外表达。

其他替换也会产生正确的表达:number "+" sum - &gt; number "+" sum "+" sum - &gt; number "+" sum "+" number - &gt; number "+" number "+" number - &gt; 12+3+5 - &gt; expression -> sum sum -> difference | difference "+" sum difference -> product | difference "-" product product -> fraction | fraction "*" product fraction -> term | fraction "/" term term -> "(" expression ")" | number number -> digit+

我们可以一点一点地表达生成规则 aka grammar 的集合,表达所有可能的代数表达式。

*

控制运算符优先级更改其生产规则相对于其他人的位置。查看上面的语法,并注意+的生成规则位于product下方,这将迫使sumexpression_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字符,则返回它。在生产规则中选择否则 - 评估符号之后返回{ {1}} 这是我们的生产规则中的正确选择,即term "*" product

答案 22 :(得分:0)

实际上,有一种方法可以不进行递归操作,这使您可以逐个字符地遍历整个表达式。对于时间和空间,这是O(n)。即使是中等大小的表达式,也要花5毫秒才能全部运行。

首先,您需要进行检查以确保您的parens平衡。我这里不是为了简单起见。另外,我的行为就像是一个计算器。除非您将表达式包装在括号中,否则计算器不会应用优先级。

我正在使用两个堆栈,一个用于操作数,另一个用于操作符。每当达到开头'(')括号时,我都会增加操作的优先级,而当达到结尾')'括号时,我会降低操作的优先级。我什至修改了代码,在小数点后添加数字。这是在c#中。

注意:这不适用于带负号的带符号数字。可能只是一个简单的修订。

  internal double Compute(string sequence)
    {
        int priority = 0;
        int sequenceCount = sequence.Length;            
        for (int i = 0; i < sequenceCount; i++) {
            char s = sequence[i];                
            if (Char.IsDigit(s)) {
                double value = ParseNextNumber(sequence, i);
                numberStack.Push(value);
                i = i + value.ToString().Length - 1;
            } else if (s == '+' || s == '-' || s == '*' || s == '/')  {                
               Operator op = ParseNextOperator(sequence, i, priority);
                CollapseTop(op, numberStack, operatorStack);
                operatorStack.Push(op);
            } if (s == '(') { priority++; ; continue; }
            else if (s == ')') { priority--; continue; }
        }
        if (priority != 0) { throw new ApplicationException("Parens not balanced"); }
        CollapseTop(new Operator(' ', 0), numberStack, operatorStack);
        if (numberStack.Count == 1 && operatorStack.Count == 0) {
            return numberStack.Pop();
        }
        return 0;
    }    

然后进行测试:

Calculator c = new Calculator();
double value = c.Compute("89.8+((9*3)+8)+(9*2)+1");
Console.WriteLine(string.Format("The sum of the expression is: {0}", (float)value));
//prints out The sum of the expression is: 143.8

答案 23 :(得分:0)

纯javascript,不需要依赖

我非常喜欢bart's answer

并且我做了一些修改以使其更容易阅读,并且还添加了一些功能的支持(并且易于扩展)

function Parse(str) {
  try {
    return parseExpr(str.replaceAll(" ", "")) // Implement? See full code.
  } catch (e) {
    alert(e.message)
  }
}

Parse("123.45+3*22*4")

它可以支持如下

const testArray = [
  // ? Basic Test
  ["(3+5)*4", ""],
  ["123.45+3*22*4", ""],
  ["8%2", ""],
  ["8%3", ""],
  ["7/3", ""],
  ["2*pi*e", 2 * Math.atan2(0, -1) * Math.exp(1)],
  ["2**3", ""],

  // ? unary Test
  ["3+(-5)", ""],
  ["3+(+5)", ""],

  // ? Function Test
  ["pow{2,3}*2", 16],
  ["4*sqrt{16}", 16],
  ["round{3.4}", 3],
  ["round{3.5}", 4],
  ["((1+e)*3/round{3.5})%2", ((1 + Math.exp(1)) * 3 / Math.round(3.5)) % 2],
  ["round{3.5}+pow{2,3}", Math.round(3.5)+Math.pow(2,3)],
]

完整代码

// ? Main
(() => {
  window.onload = () => {
    const nativeConsoleLogFunc = window.console.error
    window.console.error = (...data) => { // Override native function, just for test.
      const range = document.createRange()
      const frag = range.createContextualFragment(`<div>${data}</div>`)
      document.querySelector("body").append(frag)
      nativeConsoleLogFunc(...data)
    }

    // Add Enter event
    document.querySelector(`input`).onkeyup = (keyboardEvent) => {
      if (keyboardEvent.key === "Enter") {
        const result = Parse(document.getElementById('expr').value)
        if (result !== undefined) {
          alert(result)
        }
      }
    }

    const testArray = [
      // ? Basic Test
      ["(3+5)*4", ""],
      ["123.45+3*22*4", ""],
      ["8%2", ""],
      ["8%3", ""],
      ["7/3", ""],
      ["2*pi*e", 2 * Math.atan2(0, -1) * Math.exp(1)],
      ["2**3", ""],

      // ? unary
      ["3+(-5)", ""],
      ["3+(+5)", ""],

      // ? Function Test
      ["pow{2,3}*2", 16],
      ["4*sqrt{16}", 16],
      ["round{3.4}", 3],
      ["round{3.5}", 4],
      ["((1+e)*3/round{3.5})%2", ((1 + Math.exp(1)) * 3 / Math.round(3.5)) % 2],
      ["round{3.5}+pow{2,3}", Math.round(3.5) + Math.pow(2, 3)],

      // ? error test
      ["21+", ValueMissingError],
      ["21+*", ParseError],
      ["(1+2", ParseError], // miss ")"
      ["round(3.12)", MissingParaError], // should be round{3.12}
      ["help", UnknownVarError],
    ]

    for (let [testString, expected] of testArray) {
      if (expected === "") {
        expected = eval(testString) // Why don't you use eval instead of writing the function yourself? Because the browser may disable eval due to policy considerations. [CSP](https://content-security-policy.com/)
      }
      const actual = Parse(testString, false)

      if (actual !== expected) {
        if (actual instanceof Error && actual instanceof expected) {
          continue
        }
        console.error(`${testString} = ${actual}, value <code>${expected}</code> expected`)
      }
    }
  }
})()

// ? Script
class UnknownVarError extends Error {
}

class ValueMissingError extends Error {
}

class ParseError extends Error {
}

class MissingParaError extends Error {
}

/**
 * @description Operator
 * @param {string} sign "+", "-", "*", "/", ...
 * @param {number} precedence
 * @param {"L"|"R"} assoc associativity  left or right
 * @param {function} exec
 * */
function Op(sign, precedence, assoc, exec = undefined) {
  this.sign = sign
  this.precedence = precedence
  this.assoc = assoc
  this.exec = exec
}

const OpArray = [
  new Op("+", 10, "L", (l, r) => l + r),
  new Op("-", 10, "L", (l, r) => l - r),
  new Op("*", 20, "L", (l, r) => l * r),
  new Op("/", 20, "L", (l, r) => l / r),
  new Op("%", 20, "L", (l, r) => l % r),
  new Op("**", 30, "R", (l, r) => Math.pow(l, r))
]

const VarTable = {
  e: Math.exp(1),
  pi: Math.atan2(0, -1), // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2
  pow: (x, y) => Math.pow(x, y),
  sqrt: (x) => Math.sqrt(x),
  round: (x) => Math.round(x),
}

/**
 * @param {Op} op
 * @param {Number} value
 * */
function Item(op, value = undefined) {
  this.op = op
  this.value = value
}

class Stack extends Array {
  constructor(...items) {
    super(...items)
    this.push(new Item(new Op("", 0, "L")))
  }

  GetLastItem() {
    return this[this.length - 1] // fast then pop // https://stackoverflow.com/a/61839489/9935654
  }
}

function Cursor(str, pos) {
  this.str = str
  this.pos = pos
  this.MoveRight = (step = 1) => {
    this.pos += step
  }
  this.PeekRightChar = (step = 1) => {
    return this.str.substring(this.pos, this.pos + step)
  }

  /**
   * @return {Op}
   * */
  this.MoveToNextOp = () => {
    const opArray = OpArray.sort((a, b) => b.precedence - a.precedence)
    for (const op of opArray) {
      const sign = this.PeekRightChar(op.sign.length)
      if (op.sign === sign) {
        this.MoveRight(op.sign.length)
        return op
      }
    }
    return null
  }
}

/**
 * @param {Cursor} cursor
 * */
function parseVal(cursor) {
  let startOffset = cursor.pos

  const regex = /^(?<OpOrVar>[^\d.])?(?<Num>[\d.]*)/g
  const m = regex.exec(cursor.str.substr(startOffset))
  if (m) {
    const {groups: {OpOrVar, Num}} = m
    if (OpOrVar === undefined && Num) {
      cursor.pos = startOffset + Num.length

      if (cursor.pos > startOffset) {
        return parseFloat(cursor.str.substring(startOffset, startOffset + cursor.pos - startOffset)) // do not use string.substr() // It will be removed in the future. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Deprecated_and_obsolete_features#string_methods
      }
    }

    if ("+-(".indexOf(OpOrVar) !== -1) {
      cursor.pos++
      switch (OpOrVar) {
        case "+": // unary plus, for example: (+5)
          return parseVal(cursor)
        case "-":
          return -(parseVal(cursor))
        case "(":
          const value = parseExpr(cursor)
          if (cursor.PeekRightChar() === ")") {
            cursor.MoveRight()
            return value
          }
          throw new ParseError("Parsing error: ')' expected")
      }
    }
  }


  // ? below is for Variable or Function
  const match = cursor.str.substring(cursor.pos).match(/^[a-z_][a-z0-9_]*/i) // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match

  if (match) {
    // ? Variable
    const varName = match[0]
    cursor.MoveRight(varName.length)
    const bracket = cursor.PeekRightChar(1)
    if (bracket !== "{") {
      if (varName in VarTable) {
        const val = VarTable[varName]
        if (typeof val === "function") {
          throw new MissingParaError(`${varName} is a function, it needs big curly brackets`)
        }
        return val
      }
    }

    // ? is function
    const regex = /{(?<Para>[^{]*)}/gm
    const m = regex.exec(cursor.str.substring(cursor.pos))
    if (m && m.groups.Para !== undefined) {
      const paraString = m.groups.Para
      const para = paraString.split(',')
      cursor.MoveRight(paraString.length + 2) // 2 = { + }
      return VarTable[varName](...para)
    }
    throw new UnknownVarError(`unknown variable ${varName}`)
  }

  // ? Handle Error
  if (cursor.str.length === cursor.pos) { // example: 1+2+
    throw new ValueMissingError(`Parsing error at end of string: value expected.`)
  } else { // example: 1+2+*
    throw new ParseError("Parsing error: unrecognized value")
  }
}

/**
 * @param {string|Cursor} expr
 * */
function parseExpr(expr) {
  const stack = new Stack()
  const cursor = (expr instanceof Cursor) ? expr : new Cursor(expr, 0)
  while (1) {
    let rightValue = parseVal(cursor)
    const op = cursor.MoveToNextOp() ?? new Op("", 0, "L")

    while (
      op.precedence < stack.GetLastItem().op.precedence ||
      (op.precedence === stack.GetLastItem().op.precedence && op.assoc === 'L')) {
      const lastItem = stack.pop()
      if (!lastItem.op.exec) { // end reached
        return rightValue
      }
      rightValue = lastItem.op.exec(lastItem.value, rightValue)
    }

    stack.push(new Item(op, rightValue))
  }
}

function Parse(str, alertError = true) {
  try {
    return parseExpr(str.replaceAll(" ", ""))
  } catch (e) {
    if (alertError) {
      alert(e.message)
      return undefined
    }
    return e
  }
}
<input type="text" id="expr" name="expr" placeholder="123.45+3*22*4">
<button onclick="const x = Parse(document.getElementById('expr').value); if(x != null) alert(x);">
  Calculate!
</button>