合約系統需支援「依年份變更的計算公式」,早期將公式硬編碼導致維護困難。需求:
dddd=56)eeee = #{dddd}*20)#{eeee}*-12+13-#{dddd}+24)#{var} 替換為基礎數值或子公式(加括號)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);
}
}
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;
}
}
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);
}
MAX(a,b))只需擴充 Operator 為函數物件BigDecimal,避免浮點誤差以「遞迴變數展開 → Shunting-yard 中綴轉後綴 → 堆疊求值」三階段管線,即可構建輕量、無依賴、支援巢狀公式的 Java 計算引擎,完美滿足合約系統動態公式需求。