在改進一個關於合同的項目時,有個需求,就是由於合同中非數據項的計算公式會根據年份而進行變更,而之前是將公式硬編碼到系統中的,只要時間一變,系統就沒法使用了,因此要求合同中各個非基礎數據的項都能自定義公式,根據設置的公式來自動生成報表和合同中的數據。
顯然定義的公式都是以字符串來存儲到數據庫的,可是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);