天天看點

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     }      

最後把操作符類貼上

View Code

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);      

轉載于:https://www.cnblogs.com/lmtoo/archive/2013/04/26/formula.html