最近项目需要自己完成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:转载请注明出处。