公式解析引擎(文本计算)-java实现

3,972 阅读3分钟

起因

有些时候要计算很复杂的场景,各种的加减乘除,甚至还有取模,取整,不止是要计算出结果,还的保留计算公式,所有就写了个公式计算引擎,真的不想用表达式引擎,觉得太重了!

特点

支持拓展

  • 加减乘除
  • 取模
  • 取整
  • 括号
  • 科学计

演示

计算字符串:"3 * (2 + 3) / 2" 运算结果:7.5

System.out.println(executeExpression("3 * (2 + 3) / 2"));

代码实现

/**
 * 公式解析引擎
 * <p>
 * Created by songzhaoying on 2020/8/11 14:44.
 *
 * @author songzhaoying@com.
 * @date 2020/8/11 14:44.
 */
public class CalculatorNewUtil {

    private static final Logger logger = LoggerFactory.getLogger(CalculatorNewUtil.class);

    /**
     * 表达式字符合法性校验正则模式
     */
    private static final String EXPRESSION_PATTERN_REGEX = "[0-9\\.\\+\\-\\*\\/\\\(\\)_%Ee\\s]+";

    /**
     * 判断数字正则
     */
    private static final String NUM_REGEX = "\d+(\\.)?\\d*((E|e|E\\+|e\\+|E-|e-)\\d+)?";

    /**
     * 操作符 需要加 空格
     * ((|)|+|-|*|/|_|%)
     */
    private static final String ADD_SPACE_REGEX = "((\\d+.?\\d*[Ee]([+\\-])?\\d+)|([()+\\-*/_%]))";

    /**
     * 空格
     */
    private static final String SPACE = "\\s";

    /**
     * 计算中保留小数位数
     */
    private static final int CAL_SCALE_SIZE = 10;

    /**
     * 计算中保留小数位数 处理原则
     */
    private static final int CAL_SCALE_ROUND = BigDecimal.ROUND_HALF_DOWN;

    /**
     * 保留小数
     *
     * @param calExpressionStr
     * @param scaleSize
     * @param bigRoundType
     * @return
     * @throws Exception
     */
    public static BigDecimal executeExpression(String calExpressionStr, int scaleSize, int bigRoundType) throws Exception {
        return executeExpression(calExpressionStr).setScale(scaleSize, bigRoundType);
    }

    /**
     * 计算值 10 位小数 0 去掉
     *
     * @param calExpressionStr
     * @return
     * @throws Exception
     */
    public static BigDecimal executeExpression(String calExpressionStr) throws Exception {
        return new BigDecimal(calculate(calExpressionStr).stripTrailingZeros().toPlainString());
    }

    /**
     * 计算 中序 字符串
     *
     * @param calExpressionStr
     * @return
     */
    public static BigDecimal calculate(String calExpressionStr) throws Exception {
        calExpressionStr = check2RepairExpression(calExpressionStr);

        List<String> inorderExpressionList = getInorderExpressionList(calExpressionStr);

        // 生成 逆波兰 表达式 list
        List<String> suffixExpressionList = getSuffixExpressionList(inorderExpressionList);

        if (logger.isDebugEnabled()) {
            logger.info("中序表达式:{}", JsonUtil.writeValueAsString(inorderExpressionList));
            logger.info("后缀表达式:{}", JsonUtil.writeValueAsString(suffixExpressionList));
        }

        return calculate(suffixExpressionList);
    }

    /**
     * 生成中序表达式 list
     *
     * @param calExpressionStr
     * @return
     */
    private static List<String> getInorderExpressionList(String calExpressionStr) {
        // 生成 中序表达式 list
        return Arrays.stream(calExpressionStr.split(SPACE))
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toList());
    }

    /**
     * 计算 逆波兰 / 后序表达式 List
     *
     * @param suffixExpressionList
     * @return
     */
    private static BigDecimal calculate(List<String> suffixExpressionList) throws Exception {
        if (CollectionUtils.isEmpty(suffixExpressionList)) {
            return BigDecimal.ZERO;
        }

        Stack<BigDecimal> numStack = new Stack<>();

        for (String op2num : suffixExpressionList) {
            if (StringUtils.isBlank(op2num)) {
                continue;
            }
            // 使用正则表达式取出
            if (isNumber(op2num)) {
                // 数字
                numStack.push(new BigDecimal(op2num));
            } else {
                // 运算符
                OptEnum opEnum = OptEnum.getEnum(op2num);
                if (opEnum.getCalFunction() == null) {
                    throw new RuntimeException("操作符不支持!");
                }

                BigDecimal bigDecimal2 = numStack.pop();
                BigDecimal bigDecimal1 = numStack.pop();

                BigDecimal resultBigDecimal = opEnum.getCalFunction().apply(bigDecimal1, bigDecimal2);

                // 结果入栈
                numStack.push(resultBigDecimal);
            }
        }

        return numStack.pop();
    }

    /**
     * 生成逆波兰 / 后续表达式 list
     *
     * @param inorderExpressionList
     * @return
     */
    private static List<String> getSuffixExpressionList(List<String> inorderExpressionList) {
        Stack<String> opStack = new Stack<>();

        // 不通过中间栈,在进行逆序处理,直接输出到list中,就是需要的逆波兰表达式
        List<String> resList = new ArrayList<>(inorderExpressionList.size());
        for (String op2num : inorderExpressionList) {
            if (isNumber(op2num)) {
                // 数字
                resList.add(op2num);
            } else if (Objects.equals(op2num, OptEnum.OP_LEFT_BRACKET.getOpt())) {
                // (
                opStack.push(op2num);
            } else if (Objects.equals(op2num, OptEnum.OP_RIGHT_BRACKET.getOpt())) {
                // )
                while (!opStack.peek().equals(OptEnum.OP_LEFT_BRACKET.getOpt())) {
                    resList.add(opStack.pop());
                }
                // 去掉 (
                opStack.pop();
            } else {
                // 操作符 优先级
                while (!CollectionUtils.isEmpty(opStack)
                        && OptEnum.getEnum(opStack.peek()).getOptPriority() >= OptEnum.getEnum(op2num).getOptPriority()) {
                    resList.add(opStack.pop());
                }
                // 将 操作符 最后加入
                opStack.push(op2num);
            }
        }

        // 处理剩余的操作符
        while (!CollectionUtils.isEmpty(opStack)) {
            resList.add(opStack.pop());
        }

        return resList;
    }

    /**
     * 判断是否数字
     *
     * @param numberStr
     * @return
     */
    public static boolean isNumber(String numberStr) {
        if (numberStr == null) {
            return false;
        }
        return numberStr.matches(NUM_REGEX);
    }

    /**
     * 运算符 枚举
     */
    public enum OptEnum {
        /**
         * 运算符
         */
        OP_LEFT_BRACKET("(", "左括号", 0, null),
        OP_RIGHT_BRACKET(")", "右括号", 7, null),

        OP_ADD("+", "加", 2, (p1, p2) -> p1.add(p2)),
        OP_SUB("-", "减", 2, (p1, p2) -> p1.subtract(p2)),
        OP_MULTIPLY("*", "乘", 3, (p1, p2) -> p1.multiply(p2)),
        OP_DIVIDE("/", "除", 3, (p1, p2) -> p1.divide(p2, CAL_SCALE_SIZE, CAL_SCALE_ROUND)),

        OP_FLOOR("_", "取整", 3, (p1, p2) -> p1.divide(p2, 0, BigDecimal.ROUND_DOWN)),
        OP_MODE("%", "取模", 3, (p1, p2) -> p1.divideAndRemainder(p2)[1]),

        ;

        /**
         * 运算符
         */
        private String opt;

        /**
         * 运算说明
         */
        private String optName;

        /**
         * 运算级别
         */
        private Integer optPriority;

        /**
         * 运算 函数
         */
        private BiFunction<BigDecimal, BigDecimal, BigDecimal> calFunction;

        /**
         * @param opt
         * @param optName
         * @param optPriority
         * @param biFunction
         */
        OptEnum(String opt, String optName, Integer optPriority
                , BiFunction<BigDecimal, BigDecimal, BigDecimal> biFunction) {
            this.opt = opt;
            this.optName = optName;
            this.optPriority = optPriority;
            this.calFunction = biFunction;
        }

        /**
         * 取枚举
         *
         * @param opt
         * @return
         */
        protected static OptEnum getEnum(String opt) {
            return Stream.of(OptEnum.values())
                    .filter(t -> Objects.equals(opt, t.getOpt()))
                    .findFirst().orElseThrow(() -> new RuntimeException("运算符不支持"));
        }

        public String getOpt() {
            return opt;
        }

        public String getOptName() {
            return optName;
        }

        public Integer getOptPriority() {
            return optPriority;
        }

        public BiFunction<BigDecimal, BigDecimal, BigDecimal> getCalFunction() {
            return calFunction;
        }
    }

    /**
     * 校验 & 整理字符串
     * 科学计数处理
     *
     * @param calExpressionStr
     * @return
     */
    private static String check2RepairExpression(String calExpressionStr) {
        // 非空校验
        if (StringUtils.isBlank(calExpressionStr)) {
            throw new IllegalArgumentException("表达式不能为空!");
        }

        // 表达式字符合法性校验
        if (!calExpressionStr.matches(EXPRESSION_PATTERN_REGEX)) {
            throw new IllegalArgumentException("表达式含有非法字符!" + calExpressionStr);
        }

        // 整理字符串
        calExpressionStr = calExpressionStr.replaceAll(SPACE, "");

        // (- 替换为 (0-
        calExpressionStr = calExpressionStr.replace(OptEnum.OP_LEFT_BRACKET.getOpt() + OptEnum.OP_SUB.getOpt()
                , OptEnum.OP_LEFT_BRACKET.getOpt() + BigDecimal.ZERO + OptEnum.OP_SUB.getOpt());

        // - 开始 前缀 0-
        if (calExpressionStr.startsWith(OptEnum.OP_SUB.getOpt())) {
            calExpressionStr = BigDecimal.ZERO + calExpressionStr;
        }

        calExpressionStr = calExpressionStr.replaceAll(ADD_SPACE_REGEX, " $1 ").trim();

        if (logger.isDebugEnabled()) {
            logger.info("整理后的运算串:{}", calExpressionStr);
        }

        return calExpressionStr;
    }


    public static void main(String[] args) throws Exception {
        String str = "3.E+3 \n* 2";
        boolean number = isNumber("3.E+3");

        BigDecimal bigDecimal = executeExpression(str);

        System.out.println(bigDecimal);

        System.out.println(executeExpression("-1 + (-1) + 2"));
        System.out.println(executeExpression(" 2 + 13/3 * 3 - 4 *(2 + 5 -2*4/2+9) + 3 + (2*1)-3"));
        System.out.println(executeExpression("330000000000*121000000000000000000000000000"
                , 4, BigDecimal.ROUND_HALF_UP));

        System.out.println(executeExpression("9.2 *(20-1)-1+199 / 13"
                , 4, BigDecimal.ROUND_HALF_UP));
        System.out.println(executeExpression("9.2 *(20-1)-1+199 / 13"));
        System.out.println(executeExpression("11"));
    }
}