在改进一个关于合同的项目时,有个需求,就是由于合同中非数据项的计算公式会根据年份而进行变更,而之前是将公式硬编码到系统中的,只要时间一变,系统就没法使用了,因此要求合同中各个非基础数据的项都能自定义公式,根据设置的公式来自动生成报表和合同中的数据。
显然定义的公式都是以字符串来存储到数据库的,可是java中没有这种执行字符串公式的工具或者类,而且是公式可以嵌套一个中间公式。比如:基础数据dddd是56,而一个公式是依赖dddd的,eeee=dddd*20,而最终的公式可能是这样:eeee*-12+13-dddd+24。可知eeee是一个中间公式,所以一个公式的计算需要知道中间公式和基础数据。
这好像可以使用一个解释器模式来解决,但是我没有成功,因为括号的优先级是一个棘手的问题,后来又想到可以使用freemarker类似的模板引擎或者java6之后提供的ScriptEngine 脚本引擎,做了个实验,脚本引擎可以解决,但是这限制了必须使用java6及以上的版本。最终功夫不负有心人,终于找到了完美解决方案,即后缀表达式。我们平时写的公式称作中缀表达式,计算机处理起来比较困难,所以需要先将中缀表达式转换成计算机处理起来比较容易的后缀表达式。
将中缀表达式转换为后缀表达式具体算法规则:见后缀表达式
a.若为 '(',入栈;
b.若为 ')',则依次把栈中的的运算符加入后缀表达式中,直到出现'(',从栈中删除'(' ;
c.若为 除括号外的其他运算符 ,当其优先级高于栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止。
·当扫描的中缀表达式结束时,栈中的的所有运算符出栈;
我们提出的要求设想是这样的:
复制代码 代码如下:
public class FormulaTest {
@Test
public void testFormula() {
//基础数据
Map<String, BigDecimal> values = new HashMap<String, BigDecimal>();
values.put("dddd", BigDecimal.valueOf(56d));
//需要依赖的其他公式
Map<String, String> formulas = new HashMap<String, String>();
formulas.put("eeee", "#{dddd}*20");
//需要计算的公式
String expression = "#{eeee}*-12+13-#{dddd}+24";
BigDecimal result = FormulaParser.parse(expression, formulas, values);
Assert.assertEquals(result, BigDecimal.valueOf(-13459.0));
}
}
以下就是解决问题的步骤:
1、首先将所有中间变量都替换成基础数据
FormulaParser的finalExpression方法会将所有的中间变量都替换成基础数据,就是一个递归的做法
复制代码 代码如下:
public class FormulaParser {
/**
* 匹配变量占位符的正则表达式
*/
private static Pattern pattern = Pattern.compile("\\#\\{(.+?)\\}");
/**
* 解析公式,并执行公式计算
*
* @param formula
* @param formulas
* @param values
* @return
*/
public static BigDecimal parse(String formula, Map<String, String> formulas, Map<String, BigDecimal> values) {
if (formulas == null)formulas = Collections.emptyMap();
if (values == null)values = Collections.emptyMap();
String expression = finalExpression(formula, formulas, values);
return new Calculator().eval(expression);
}
/**
* 解析公式,并执行公式计算
*
* @param formula
* @param values
* @return
*/
public static BigDecimal parse(String formula, Map<String, BigDecimal> values) {
if (values == null)values = Collections.emptyMap();
return parse(formula, Collections.<String, String> emptyMap(), values);
}
/**
* 解析公式,并执行公式计算
*
* @param formula
* @return
*/
public static BigDecimal parse(String formula) {
return parse(formula, Collections.<String, String> emptyMap(), Collections.<String, BigDecimal> emptyMap());
}
/**
* 将所有中间变量都替换成基础数据
*
* @param expression
* @param formulas
* @param values
* @return
*/
private static String finalExpression(String expression, Map<String, String> formulas, Map<String, BigDecimal> values) {
Matcher m = pattern.matcher(expression);
if (!m.find())return expression;
m.reset();
StringBuffer buffer = new StringBuffer();
while (m.find()) {
String group = m.group(1);
if (formulas != null && formulas.containsKey(group)) {
String formula = formulas.get(group);
m.appendReplacement(buffer, '(' + formula + ')');
} else if (values != null && values.containsKey(group)) {
BigDecimal value = values.get(group);
m.appendReplacement(buffer,value.toPlainString());
}else{
throw new IllegalArgumentException("expression '"+expression+"' has a illegal variable:"+m.group()+",cause veriable '"+group+"' not being found in formulas or in values.");
}
}
m.appendTail(buffer);
return finalExpression(buffer.toString(), formulas, values);
}
}
2、将中缀表达式转换为后缀表达式
Calculator的infix2Suffix将中缀表达式转换成了后缀表达式
3、计算后缀表达式
Calculator的evalInfix计算后缀表达式
复制代码 代码如下: