基于逆波兰表达式的公式解析器-算法和思路(一)

背景:

       最近项目需要自己完成Excel的公式解析和求值,在Java中可以使用POI解析Excel公式然后求值。但是项目需要JS端和Java后端均需要支持公式解析,所以就需要自己写一套了。其实公式解析器总体上并不复杂,原理使用逆波兰表达式就可了。

难点:

       1. 针对复杂的用户输入环境解析公式,需要注意公式书写不规范、大小写、空格等问题,甚至公式出错的判断。

       2. 需要解决函数扩展、函数执行等问题。

       3. 需要解决地址、地址范围取数,求值问题。

       4. 处理括号带来的优先级提升。

       5. 解决公式嵌套求知问题。

       6. 财务小数精度,解决采用IEEE 754标准出现的0.3 –0.2 != 0.1问题。

       7. 解决循环引用问题,也就是公式链成环问题。

 

理论和原理:

       1. 基于逆波兰表达式,将用户输入解析为后缀表达式。

       2. 抽象操作数和操作符,操作数(operand)操作符(operator)分别放入两个stack。

       3. 将函数抽象为operator,地址、地址范围抽象为operand。

 

       关于逆波兰表达式,在数据结构和编译原理中都已经讲烂了,简单介绍下:

       逆波兰表达式是波兰逻辑学家在1929年提出的一种表达式表达方式。我们传统的表达式操作符一般都是在两个数之间的(先仅限于二元操作符)。而逆波兰表达式的操作符在数字的后面。看下面一个简单例子就知道了:

转换前:1 + 2 – 3 * 4 
转换后:1 , 2 , + , 3 , 4 , * , -

       优点:逆波兰表达式非常适合机器执行,能屏蔽掉括号对运算符的优先级的提升。

 

一般算法:

       逆波兰表达式的一般解析算法是建立在简单算术表达式上的,它是我们进行公式解析和执行的基础:

       1. 构建两个栈Operand(操作数栈)和Operator(操作符栈)。

       2.扫描给定的字符串,如果得到一个数字,则提取(扫描是一位一位的,一定要提取一个完整的数字)数字(以下用Operand代替),然后把Operand压入Operand栈中。

       3. 如果获得一个运算符(比如+或者*,下面用B代替),则需要和Operator栈栈顶元素(用A替代)比较:

              1) 如果A不存在,则把B压入Operator栈中;

              2)如果B是一个左括号,则忽略A和B的优先级比较,把B压入Operator栈。

              3)如果B是一个右括号,则把Operator栈顺序出栈,然后把弹出的元素顺序压入Operand栈中,直到栈顶弹出的是左括号,括号不入Operand栈中。

              4)如果A是左括号,则把B直接压入Operator栈。

              5)如果B优先级比较A高,则把B直接压入Operator栈。

              6)如果B优先级低于或等于A的优先级,则把A出栈然后压入Operand栈,反复进行此步骤直到栈顶优先级高于B的优先级或者栈顶是一个括号。

       4.扫描完毕后,把Operator栈的元素依次出栈,然后依次压入Operand栈中。

 

算法特点:

       1.使用两个stack,用来构建后缀表达式,Operator栈忽略括号的情况下,始终是高优先级的Operator在栈顶。

       2.括号的优先级最低,优先级优自己定义。

       3.到最后我们只剩下一个Operator栈,从栈低依次运算即可。

       4.优先级比较是关键,优先级关系到出入栈顺序和最终结果。

 

公式解析的复杂性:

       相比传统的算术表达式的解析,Excel类公式的解析更加复杂:

       1. 需要支持的操作数和操作符众多:不仅仅是数字操作数和数学操作符,还会包含比较运算符、函数、逻辑操作数,字符串操作数,地址(如Excel的A1,A2)等。

       2. 公式的组成比较复杂,扫描困难:扫描时需要提取数字(有可能是科学计数法)、单词(比如函数、地址、地址范围),还可能出现一元操作符(比如取非!,预防需求的变化,需求是最坑程序员的)。

       3. 同一操作符代表的含义不同:操作符重载情况比较到,比如“-”,即可能是“减号”,也可能是“符号”;再比如执行函数功能时,函数需要多少个操作数,地址范围(A1:A10)如何执行求值。

       4. 计算精度,这是最麻烦的(Java和JS的那个坑爹的0.1问题),既要保证效率,又要保证精度(这个世界不公平啊)。


总结:

       使用逆波兰表达式解析公式,我们需要对整改算法做小小的改动,将操作数和操作符的范围提升,不仅仅局限在算术表达式的范围内。这就要求我们解析的时候书写正则表达式(或词法分析的办法)来提取完整的操作数、操作符,甚至根据上下文环境对操作符进行重载(比如-到底是减法还是符号)。今天到这儿,明天说下改进算法和代码。


PS:转载请注明出处。


你可能感兴趣的:(Java,JavaScript,总结,公式解析器)