首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

Java 動態公式引擎實作:中綴轉後綴表達式與遞迴變數解析

     編輯:MySQL 資料庫相關知識

專案背景

合約系統需支援「依年份變更的計算公式」,早期將公式硬編碼導致維護困難。需求:

  • 基礎變數(如 dddd=56)
  • 中間公式(如 eeee = #{dddd}*20)
  • 最終公式可巢狀引用中間公式(如 #{eeee}*-12+13-#{dddd}+24)
  • 公式以字串存於資料庫,執行期動態解析

技術選型演進

  1. Interpreter Pattern:優先級、括號處理複雜,開發難度高
  2. FreeMarker / ScriptEngine (JSR-223):需 Java 6+,引入外部依賴
  3. 中綴轉後綴表達式(Shunting-yard 演算法):無外部依賴、效能佳、支援完整運算子優先級與括號,最終採用

核心流程三步驟

  1. 遞迴展開變數:將 #{var} 替換為基礎數值或子公式(加括號)
  2. 中綴轉後綴:掃描字元,運算子進堆疊,依優先級輸出
  3. 後綴求值:運算元進堆疊,遇運算子彈出兩元素運算再推回

關鍵類別設計

FormulaParser:進入點與變數展開

public class FormulaParser {
    private static final Pattern VAR_PATTERN = Pattern.compile("\\#\\{(.+?)\\}");

    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 expanded = expandVariables(formula, formulas, values);
        return new Calculator().eval(expanded);
    }

    private static String expandVariables(String expr,
                                          Map<String, String> formulas,
                                          Map<String, BigDecimal> values) {
        Matcher m = VAR_PATTERN.matcher(expr);
        if (!m.find()) return expr;
        m.reset();
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String key = m.group(1);
            if (formulas.containsKey(key)) {
                m.appendReplacement(sb, "(" + formulas.get(key) + ")");
            } else if (values.containsKey(key)) {
                m.appendReplacement(sb, values.get(key).toPlainString());
            } else {
                throw new IllegalArgumentException("Unknown variable: " + key);
            }
        }
        m.appendTail(sb);
        // 遞迴處理巢狀公式
        return expandVariables(sb.toString(), formulas, values);
    }
}

Calculator:中綴轉後綴與求值

public class Calculator {
    private static final char SEP = ' ';

    public BigDecimal eval(String infix) {
        String postfix = infixToPostfix(infix);
        return evalPostfix(postfix);
    }

    // 中綴 → 後綴(Shunting-yard)
    public String infixToPostfix(String expr) {
        if (expr == null) return null;
        Deque<Character> opStack = new ArrayDeque<>();
        StringBuilder out = new StringBuilder();
        boolean needSep = false, signAllowed = true;

        for (char c : expr.toCharArray()) {
            if (c == ' ') continue;
            if (needSep) { out.append(SEP); needSep = false; }

            if ((c == '+' || c == '-') && signAllowed) { // 一元正負號
                out.append(c);
                continue;
            }
            if (Character.isDigit(c) || c == '.') {
                out.append(c);
                signAllowed = false;
            } else if (c == '(') {
                opStack.push(c);
                signAllowed = true;
            } else if (c == ')') {
                signAllowed = false;
                while (opStack.peek() != '(')
                    out.append(SEP).append(opStack.pop());
                opStack.pop(); // 丟掉 '('
            } else if (isOperator(c)) {
                needSep = true;
                signAllowed = true;
                int prec = Operator.priority(c);
                while (!opStack.isEmpty() && opStack.peek() != '(' &&
                       Operator.priority(opStack.peek()) >= prec)
                    out.append(SEP).append(opStack.pop());
                opStack.push(c);
            }
        }
        while (!opStack.isEmpty()) out.append(SEP).append(opStack.pop());
        return out.toString();
    }

    // 後綴求值
    private BigDecimal evalPostfix(String postfix) {
        String[] tokens = postfix.split("\\s+");
        Deque<BigDecimal> st = new ArrayDeque<>();
        for (String t : tokens) {
            if (!Operator.isOperator(t)) {
                st.push(new BigDecimal(t));
            } else {
                BigDecimal b = st.pop(), a = st.pop();
                st.push(Operator.get(t).eval(a, b));
            }
        }
        return st.pop();
    }

    private boolean isOperator(char c) {
        return Operator.get(c) != null;
    }
}

Operator:運算子註冊與優先級

public abstract class Operator {
    private final char sym; private final int prec;
    private static final Map<Character, Operator> REG = new HashMap<>();

    protected Operator(char sym, int prec) { this.sym = sym; this.prec = prec; REG.put(sym, this); }

    public static int priority(char c) { return REG.getOrDefault(c, new Operator(c,0){public BigDecimal eval(BigDecimal a,BigDecimal b){return null;}}).prec; }
    public static boolean isOperator(char c) { return REG.containsKey(c); }
    public static Operator get(char c) { return REG.get(c); }
    public static Operator get(String s) { return s.length()==1 ? get(s.charAt(0)) : null; }
    public abstract BigDecimal eval(BigDecimal a, BigDecimal b);

    // 內建運算子
    public static final Operator ADD = new Operator('+', 100) { public BigDecimal eval(BigDecimal a,BigDecimal b){ return a.add(b); }};
    public static final Operator SUB = new Operator('-', 100) { public BigDecimal eval(BigDecimal a,BigDecimal b){ return a.subtract(b); }};
    public static final Operator MUL = new Operator('*', 200) { public BigDecimal eval(BigDecimal a,BigDecimal b){ return a.multiply(b); }};
    public static final Operator DIV = new Operator('/', 200) { public BigDecimal eval(BigDecimal a,BigDecimal b){ return a.divide(b, 10, RoundingMode.HALF_UP); }};
    public static final Operator POW = new Operator('^', 300) { public BigDecimal eval(BigDecimal a,BigDecimal b){ return a.pow(b.intValueExact()); }};
}

單元測試驗證

@Test
public void testNestedFormula() {
    Map<String, BigDecimal> base = new HashMap<>();
    base.put("dddd", BigDecimal.valueOf(56));

    Map<String, String> mid = new HashMap<>();
    mid.put("eeee", "#{dddd}*20"); // 1120

    String expr = "#{eeee}*-12+13-#{dddd}+24"; // 1120*-12 +13 -56 +24 = -13459
    BigDecimal r = FormulaParser.parse(expr, mid, base);
    Assert.assertEquals(BigDecimal.valueOf(-13459), r);
}

效能與擴充建議

  • 表達式解析結果可快取(以原始字串為 key)
  • 支援函數呼叫(如 MAX(a,b))只需擴充 Operator 為函數物件
  • 大數值運算建議統一 BigDecimal,避免浮點誤差

結語

以「遞迴變數展開 → Shunting-yard 中綴轉後綴 → 堆疊求值」三階段管線,即可構建輕量、無依賴、支援巢狀公式的 Java 計算引擎,完美滿足合約系統動態公式需求。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved