java处理字符串公式运算
在改进一个关于合同的项目时,有个需求,就是由于合同中非数据项的计算公式会根据年份而进行变更,而之前是将公式硬编码到系统中的,只要时间一变,系统就没法使用了,因此要求合同中各个非基础数据的项都能自定义公式,根据设置的公式来自动生成报表和合同中的数据。
显然定义的公式都是以字符串来存储到数据库的,可是java中没有这种执行字符串公式的工具或者类,而且是公式可以嵌套一个中间公式。比如:基础数据dddd是56,而一个公式是依赖dddd的,eeee=dddd*20,而最终的公式可能是这样:eeee*-12+13-dddd+24。可知eeee是一个中间公式,所以一个公式的计算需要知道中间公式和基础数据。
这好像可以使用一个解释器模式来解决,但是我没有成功,因为括号的优先级是一个棘手的问题,后来又想到可以使用freemarker类似的模板引擎或者java6之后提供的ScriptEngine 脚本引擎,做了个实验,脚本引擎可以解决,但是这限制了必须使用java6及以上的版本。最终功夫不负有心人,终于找到了完美解决方案,即后缀表达式。我们平时写的公式称作中缀表达式,计算机处理起来比较困难,所以需要先将中缀表达式转换成计算机处理起来比较容易的后缀表达式。
将中缀表达式转换为后缀表达式具体算法规则:见后缀表达式
a.若为 '(',入栈;
b.若为 ')',则依次把栈中的的运算符加入后缀表达式中,直到出现'(',从栈中删除'(' ;
c.若为 除括号外的其他运算符 ,当其优先级高于栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止。
·当扫描的中缀表达式结束时,栈中的的所有运算符出栈;
我们提出的要求设想是这样的:
1 public class FormulaTest { 2 @Test 3 public void testFormula() { 4 //基础数据 5 Map<String, BigDecimal> values = new HashMap<String, BigDecimal>(); 6 values.put("dddd", BigDecimal.valueOf(56d)); 7 8 //需要依赖的其他公式 9 Map<String, String> formulas = new HashMap<String, String>(); 10 formulas.put("eeee", "#{dddd}*20"); 11 12 //需要计算的公式 13 String expression = "#{eeee}*-12+13-#{dddd}+24"; 14 15 BigDecimal result = FormulaParser.parse(expression, formulas, values); 16 Assert.assertEquals(result, BigDecimal.valueOf(-13459.0)); 17 } 18 }
以下就是解决问题的步骤:
1、首先将所有中间变量都替换成基础数据
FormulaParser的finalExpression方法会将所有的中间变量都替换成基础数据,就是一个递归的做法
1 public class FormulaParser { 2 /** 3 * 匹配变量占位符的正则表达式 4 */ 5 private static Pattern pattern = Pattern.compile("\\#\\{(.+?)\\}"); 6 7 /** 8 * 解析公式,并执行公式计算 9 * 10 * @param formula 11 * @param formulas 12 * @param values 13 * @return 14 */ 15 public static BigDecimal parse(String formula, Map<String, String> formulas, Map<String, BigDecimal> values) { 16 if (formulas == null)formulas = Collections.emptyMap(); 17 if (values == null)values = Collections.emptyMap(); 18 String expression = finalExpression(formula, formulas, values); 19 return new Calculator().eval(expression); 20 } 21 22 /** 23 * 解析公式,并执行公式计算 24 * 25 * @param formula 26 * @param values 27 * @return 28 */ 29 public static BigDecimal parse(String formula, Map<String, BigDecimal> values) { 30 if (values == null)values = Collections.emptyMap(); 31 return parse(formula, Collections.<String, String> emptyMap(), values); 32 } 33 34 /** 35 * 解析公式,并执行公式计算 36 * 37 * @param formula 38 * @return 39 */ 40 public static BigDecimal parse(String formula) { 41 return parse(formula, Collections.<String, String> emptyMap(), Collections.<String, BigDecimal> emptyMap()); 42 } 43 44 /** 45 * 将所有中间变量都替换成基础数据 46 * 47 * @param expression 48 * @param formulas 49 * @param values 50 * @return 51 */ 52 private static String finalExpression(String expression, Map<String, String> formulas, Map<String, BigDecimal> values) { 53 Matcher m = pattern.matcher(expression); 54 if (!m.find())return expression; 55 56 m.reset(); 57 58 StringBuffer buffer = new StringBuffer(); 59 while (m.find()) { 60 String group = m.group(1); 61 if (formulas != null && formulas.containsKey(group)) { 62 String formula = formulas.get(group); 63 m.appendReplacement(buffer, '(' + formula + ')'); 64 } else if (values != null && values.containsKey(group)) { 65 BigDecimal value = values.get(group); 66 m.appendReplacement(buffer,value.toPlainString()); 67 }else{ 68 throw new IllegalArgumentException("expression '"+expression+"' has a illegal variable:"+m.group()+",cause veriable '"+group+"' not being found in formulas or in values."); 69 } 70 } 71 m.appendTail(buffer); 72 return finalExpression(buffer.toString(), formulas, values); 73 } 74 }
2、将中缀表达式转换为后缀表达式
Calculator的infix2Suffix将中缀表达式转换成了后缀表达式
3、计算后缀表达式
Calculator的evalInfix计算后缀表达式
1 public class Calculator{ 2 private static Log logger = LogFactory.getLog(Calculator.class); 3 4 /** 5 * 左括号 6 */ 7 public final static char LEFT_BRACKET = '('; 8 9 /** 10 * 右括号 11 */ 12 public final static char RIGHT_BRACKET = ')'; 13 14 /** 15 * 中缀表达式中的空格,需要要忽略 16 */ 17 public final static char BLANK = ' '; 18 19 /** 20 * 小数点符号 21 */ 22 public final static char DECIMAL_POINT = '.'; 23 24 /** 25 * 负号 26 */ 27 public final static char NEGATIVE_SIGN = '-'; 28 29 /** 30 * 正号 31 */ 32 public final static char POSITIVE_SIGN = '+'; 33 34 /** 35 * 后缀表达式的各段的分隔符 36 */ 37 public final static char SEPARATOR = ' '; 38 39 /** 40 * 解析并计算表达式 41 * 42 * @param expression 43 * @return 44 */ 45 public BigDecimal eval(String expression) { 46 String str = infix2Suffix(expression); 47 logger.info("Infix Expression: " + expression); 48 logger.info("Suffix Expression: " + str); 49 if (str == null) { 50 throw new IllegalArgumentException("Infix Expression is null!"); 51 } 52 return evalInfix(str); 53 } 54 55 /** 56 * 对后缀表达式进行计算 57 * 58 * @param expression 59 * @return 60 */ 61 private BigDecimal evalInfix(String expression) { 62 String[] strs = expression.split("\\s+"); 63 Stack<String> stack = new Stack<String>(); 64 for (int i = 0; i < strs.length; i++) { 65 if (!Operator.isOperator(strs[i])) { 66 stack.push(strs[i]); 67 } else { 68 Operator op = Operator.getInstance(strs[i]); 69 BigDecimal right =new BigDecimal(stack.pop()); 70 BigDecimal left =new BigDecimal(stack.pop()); 71 BigDecimal result = op.eval(left, right); 72 stack.push(String.valueOf(result)); 73 } 74 } 75 return new BigDecimal(stack.pop()); 76 } 77 78 /** 79 * 将中缀表达式转换为后缀表达式<br> 80 * 具体算法规则 81 * 1)计算机实现转换: 将中缀表达式转换为后缀表达式的算法思想: 82 * 开始扫描; 83 * 数字时,加入后缀表达式; 84 * 运算符: 85 * a.若为 '(',入栈; 86 * b.若为 ')',则依次把栈中的的运算符加入后缀表达式中,直到出现'(',从栈中删除'(' ; 87 * c.若为 除括号外的其他运算符 ,当其优先级高于栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止。 88 * ·当扫描的中缀表达式结束时,栈中的的所有运算符出栈; 89 * 90 * @param expression 91 * @return 92 */ 93 public String infix2Suffix(String expression) { 94 if (expression == null) return null; 95 96 Stack<Character> stack = new Stack<Character>(); 97 98 char[] chs = expression.toCharArray(); 99 StringBuilder sb = new StringBuilder(chs.length); 100 101 boolean appendSeparator = false; 102 boolean sign = true; 103 for (int i = 0; i < chs.length; i++) { 104 char c = chs[i]; 105 106 // 空白则跳过 107 if (c == BLANK)continue; 108 109 // Next line is used output stack information. 110 // System.out.printf("%-20s %s%n", stack, sb.toString()); 111 112 // 添加后缀表达式分隔符 113 if (appendSeparator) { 114 sb.append(SEPARATOR); 115 appendSeparator = false; 116 } 117 118 if (isSign(c) && sign) { 119 sb.append(c); 120 } else if (isNumber(c)) { 121 sign = false;// 数字后面不是正号或负号,而是操作符+- 122 sb.append(c); 123 } else if (isLeftBracket(c)) { 124 stack.push(c); 125 } else if (isRightBracket(c)) { 126 sign = false; 127 128 // 如果为),则弹出(上面的所有操作符,并添加到后缀表达式中,并弹出( 129 while (stack.peek() != LEFT_BRACKET) { 130 sb.append(SEPARATOR).append(stack.pop()); 131 } 132 stack.pop(); 133 } else { 134 appendSeparator = true; 135 if (Operator.isOperator(c)) { 136 sign = true; 137 138 // 若为(则入栈 139 if (stack.isEmpty() || stack.peek() == LEFT_BRACKET) { 140 stack.push(c); 141 continue; 142 } 143 int precedence = Operator.getPrority(c); 144 while (!stack.isEmpty() && Operator.getPrority(stack.peek()) >= precedence) { 145 sb.append(SEPARATOR).append(stack.pop()); 146 } 147 stack.push(c); 148 } 149 } 150 } 151 while (!stack.isEmpty()) { 152 sb.append(SEPARATOR).append(stack.pop()); 153 } 154 return sb.toString(); 155 } 156 157 /** 158 * 判断某个字符是否是正号或者负号 159 * 160 * @param c 161 * @return 162 */ 163 private boolean isSign(char c) { 164 return (c == NEGATIVE_SIGN || c == POSITIVE_SIGN); 165 } 166 167 /** 168 * 判断某个字符是否为数字或者小数点 169 * 170 * @param c 171 * @return 172 */ 173 private boolean isNumber(char c) { 174 return ((c >= '0' && c <= '9') || c == DECIMAL_POINT); 175 } 176 177 /** 178 * 判断某个字符是否为左括号 179 * 180 * @param c 181 * @return 182 */ 183 private boolean isLeftBracket(char c) { 184 return c == LEFT_BRACKET; 185 } 186 187 /** 188 * 判断某个字符是否为右括号 189 * 190 * @param c 191 * @return 192 */ 193 private boolean isRightBracket(char c) { 194 return c == RIGHT_BRACKET; 195 }
最后把操作符类贴上
1 public abstract class Operator { 2 /** 3 * 运算符 4 */ 5 private char operator; 6 7 /** 8 * 运算符的优先级别,数字越大,优先级别越高 9 */ 10 private int priority; 11 12 private static Map<Character, Operator> operators = new HashMap<Character, Operator>(); 13 14 private Operator(char operator, int priority) { 15 setOperator(operator); 16 setPriority(priority); 17 register(this); 18 } 19 20 private void register(Operator operator) { 21 operators.put(operator.getOperator(), operator); 22 } 23 24 /** 25 * 加法运算 26 */ 27 public final static Operator ADITION = new Operator('+', 100) { 28 public BigDecimal eval(BigDecimal left, BigDecimal right) { 29 return left.add(right); 30 } 31 }; 32 33 /** 34 * 减法运算 35 */ 36 public final static Operator SUBTRATION = new Operator('-', 100) { 37 public BigDecimal eval(BigDecimal left, BigDecimal right) { 38 return left.subtract(right); 39 } 40 }; 41 42 /** 43 * 乘法运算 44 */ 45 public final static Operator MULTIPLICATION = new Operator('*', 200) { 46 public BigDecimal eval(BigDecimal left, BigDecimal right) { 47 return left.multiply(right); 48 } 49 }; 50 51 /** 52 * 除法运算 53 */ 54 public final static Operator DIVITION = new Operator('/', 200) { 55 public BigDecimal eval(BigDecimal left, BigDecimal right) { 56 return left.divide(right); 57 } 58 }; 59 60 /** 61 * 冪运算 62 */ 63 public final static Operator EXPONENT = new Operator('^', 300) { 64 public BigDecimal eval(BigDecimal left, BigDecimal right) { 65 return left.pow(right.intValue()); 66 } 67 }; 68 69 public char getOperator() { 70 return operator; 71 } 72 73 private void setOperator(char operator) { 74 this.operator = operator; 75 } 76 77 public int getPriority() { 78 return priority; 79 } 80 81 private void setPriority(int priority) { 82 this.priority = priority; 83 } 84 85 /** 86 * 根据某个运算符获得该运算符的优先级别 87 * 88 * @param c 89 * @return 运算符的优先级别 90 */ 91 public static int getPrority(char c) { 92 Operator op = operators.get(c); 93 return op != null ? op.getPriority() : 0; 94 } 95 96 /** 97 * 工具方法,判断某个字符是否是运算符 98 * 99 * @param c 100 * @return 是运算符返回 true,否则返回 false 101 */ 102 public static boolean isOperator(char c) { 103 return getInstance(c) != null; 104 } 105 106 public static boolean isOperator(String str) { 107 return str.length() > 1 ? false : isOperator(str.charAt(0)); 108 } 109 110 /** 111 * 根据运算符获得 Operator 实例 112 * 113 * @param c 114 * @return 从注册中的 Operator 返回实例,尚未注册返回 null 115 */ 116 public static Operator getInstance(char c) { 117 return operators.get(c); 118 } 119 120 public static Operator getInstance(String str) { 121 return str.length() > 1 ? null : getInstance(str.charAt(0)); 122 } 123 124 /** 125 * 根据操作数进行计算 126 * 127 * @param left 128 * 左操作数 129 * @param right 130 * 右操作数 131 * @return 计算结果 132 */ 133 public abstract BigDecimal eval(BigDecimal left, BigDecimal right);