diff --git a/xhpc-modules/xhpc-order/src/main/java/com/xhpc/order/service/impl/Calc.java b/xhpc-modules/xhpc-order/src/main/java/com/xhpc/order/service/impl/Calc.java new file mode 100644 index 00000000..3f309482 --- /dev/null +++ b/xhpc-modules/xhpc-order/src/main/java/com/xhpc/order/service/impl/Calc.java @@ -0,0 +1,371 @@ +package com.xhpc.order.service.impl; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.io.IOException; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.io.StringReader; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +public class Calc { + + public static void main(String[] argv) { + + test("-1 + 2", 1); + test("2 * (3 * (4 + 5)) / 9 / 6", 1); + test("max(1, 2 + 3)", 5); + test("min(3 - 1, 1)", 1); + + Map map = new HashMap<>(); + map.put("totalPower", new BigDecimal("400000.000000")); + StandardEvaluationContext stdContext = new StandardEvaluationContext(); + stdContext.setVariables(map); + String ruleExpression = "#totalPower >= 500000 ? 'e1' : 'e2'"; + // Evaluate the SpEL expression + ExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression(ruleExpression); + String rs = expression.getValue(stdContext, String.class); + System.out.println(rs); + } + + private static void test(String s, double x) { + + double v = eval(s); + System.out.println((v == x) + ": " + s + " = " + v); + } + + private static double eval(String s) { + + Parser p = Parser.parse(s); + double v = p.value(); + + if (p.isValid()) { + return v; + } else { + throw new ArithmeticException("error check: " + p.error() + ": " + s); + } + } + +} + +/** + * Parse via recursive descent using the following LL(1) grammar: + * + * expr = [addop] term {(addop) term} end + * term = factor {(mulop) factor} end + * factor = word | number | "(" expr ")" end + * word = name ["(" expr] ["," expr] ")"] end + * addop = "+" | "-" + * mulop = "*" | "/" + * + * + * @param s the string to be evaluated. + * @see + * Recursive descent parser. + */ +class Parser extends Object { + + private final StreamTokenizer tokens; + private int token; + private final double value; + private String error; + + private Parser(String s) { + + Reader reader = new StringReader(s); + tokens = new StreamTokenizer(reader); + tokens.ordinaryChar(Symbol.SLASH.toChar()); + + getToken(); + value = expr(); + if (!tokenIs(Symbol.END)) + putError("syntax error"); + } + + public static Parser parse(String s) { + + return new Parser(s); + } + + public boolean isValid() { + + return error == null; + } + + public double value() { + + return value; + } + + public String error() { + + return error; + } + + // expr = [addop] term {(addop) term} end + private double expr() { + + BigDecimal sign = BigDecimal.ONE; + accept(Symbol.PLUS); + if (accept(Symbol.MINUS)) { + sign = BigDecimal.valueOf(-1); + } + BigDecimal value = sign.multiply(term()); + while (Symbol.isAddOp(token)) { + if (accept(Symbol.PLUS)) { + value = value.add(term()); + } + if (accept(Symbol.MINUS)) { + value = value.subtract(term()); + } + } + return value.doubleValue(); + } + + // term = factor {(mulop) factor} end + private BigDecimal term() { + + BigDecimal value = factor(); + while (Symbol.isMulOp(token)) { + if (accept(Symbol.STAR)) { + value = value.multiply(factor()); + } + if (accept(Symbol.SLASH)) { + value = value.divide(factor()); + } + } + return value; + } + + // factor = word | number | "(" expr ")" end + private BigDecimal factor() { + + BigDecimal value = BigDecimal.ZERO; + if (tokenIs(Symbol.WORD)) { + value = word(); + } else if (tokenIs(Symbol.NUMBER)) { + value = new BigDecimal(tokens.nval); + getToken(); + } else if (accept(Symbol.OPEN)) { + value = BigDecimal.valueOf(expr()); + expect(Symbol.CLOSE); + } else { + putError("factor error"); + getToken(); + } + return value; + } + + // word = name ["(" expr] ["," expr] ")"] end + private BigDecimal word() { + + BigDecimal value = BigDecimal.ZERO; + String name = tokens.sval; + FunctionAdapter fa = Function.lookup(name); + getToken(); + if (fa != null) { + int count = fa.getCount(); + if (count == 0) { + value = BigDecimal.valueOf(fa.eval()); + } else if (accept(Symbol.OPEN)) { + double[] args = new double[count]; + for (int i = 0; i < count; i++) { + args[i] = expr(); + if (i < count - 1) + expect(Symbol.COMMA); + } + value = BigDecimal.valueOf(fa.eval(args)); + expect(Symbol.CLOSE); + } else putError("missing " + Symbol.OPEN.toChar()); + } else putError("undefined " + name); + return value; + } + + /** + * Fetch the next token in the stream. + */ + private void getToken() { + + try { + token = tokens.nextToken(); + } catch (IOException e) { + putError("i/o error " + e.getMessage()); + } + } + + /** + * Return true if the current token matches the given symbol. + */ + private boolean tokenIs(Symbol symbol) { + + return token == symbol.token(); + } + + /** + * Require a matching symbol; gerate an error if it's unexpected. + */ + private void expect(Symbol symbol) { + + if (accept(symbol)) return; + putError("missing " + symbol.toChar()); + } + + /** + * Advance if the current token matches the given symbol. + */ + private boolean accept(Symbol symbol) { + + if (tokenIs(symbol)) { + getToken(); + return true; + } + return false; + } + + /** + * Generate an error; ignore line numbers. + */ + private void putError(String s) { + + if (error == null) + error = s + " at " + tokens.toString().replaceAll(",.*$", ""); + } + +} + +/** + * Enumerate the terminal symbols recognized by the Parser. + * Each symbol's token field is initialized with a value that + * matches one returned by StreamTokenizer's nextToken() method. + */ +enum Symbol { + PLUS('+'), MINUS('-'), STAR('*'), SLASH('/'), + OPEN('('), CLOSE(')'), COMMA(','), + END(StreamTokenizer.TT_EOF), + WORD(StreamTokenizer.TT_WORD), + NUMBER(StreamTokenizer.TT_NUMBER); + + private final int token; + + Symbol(int token) { + + this.token = token; + } + + /** + * Return this symbol's token. + */ + public int token() { + + return this.token; + } + + /** + * If printable, return character for this symbol's token. + */ + public char toChar() { + + if (this.token < 32) return '\ufffd'; + else return (char) this.token; + } + + /** + * Return if the given token matches PLUS or MINUS. + */ + public static boolean isAddOp(int token) { + + return token == PLUS.token || token == MINUS.token; + } + + /** + * Return if the given token matches STAR or SLASH. + */ + public static boolean isMulOp(int token) { + + return token == STAR.token || token == SLASH.token; + } +} + +/** + * Enumerate the available functions. Although an enum may define + * constant-specific methods, an included adpter or interface + * is perhaps easier to read, initialize and maintain. + */ +enum Function { + MAX(new Max()), + MIN(new Min()); + + private final FunctionAdapter fa; + + /** + * Construct a Function with the specified adapter. + **/ + Function(FunctionAdapter fa) { + + this.fa = fa; + } + + /** + * Return a Function's adapter by name; null if unknown. + */ + public static FunctionAdapter lookup(String name) { + + Function f; + try { + f = Enum.valueOf(Function.class, name.toUpperCase()); + } catch (RuntimeException e) { + return null; + } + return f.fa; + } +} + +/** + * Adapt to functions with a variable number of arguments. + * Concrete implementations should override getCount() to indicate + * the expected number of arguments. The default is one. + */ +abstract class FunctionAdapter { + + public int getCount() { + + return 1; + } + + abstract double eval(double... args); + +} + +class Max extends FunctionAdapter { + + public int getCount() { + + return 2; + } + + public double eval(double... args) { + + return Math.max(args[0], args[1]); + } + +} + +class Min extends FunctionAdapter { + + public int getCount() { + + return 2; + } + + public double eval(double... args) { + + return Math.min(args[0], args[1]); + } + +}