校验公式是否合法

公司使用公式做各种业务已经很长时间了,但tm前辈们都不写公式校验…
最近测试提bug,有很多都是由于公式错误导致的,原本是想让前端去写这个逻辑,但看她样子实在是…算了,就自己写吧…
话不多说,直接上代码 (部分stream语法需要在jdk17才有,jdk8的童鞋们就自行修改吧)

@Getter
@AllArgsConstructor
public enum SignTypeEnum {
    RELATION("关系运算符"),
    LOGIC("逻辑连结符"),
    OPERATOR("数值计算操作符"),
    BRACKETS("括号");

    private final String name;
}
@Getter
@AllArgsConstructor
public enum SignEnum {
    GT(">", "大于", "<=", SignTypeEnum.RELATION),
    GE(">=", "大于等于", "<", SignTypeEnum.RELATION),
    LT("<", "小于", ">=", SignTypeEnum.RELATION),
    LE("<=", "小于等于", ">", SignTypeEnum.RELATION),
    EQ("=", "等于", "=", SignTypeEnum.RELATION),
    DEQ("==", "等于", "==", SignTypeEnum.RELATION),
    AND("&", "与", "|", SignTypeEnum.LOGIC),
    DAND("&&", "短路与", "||", SignTypeEnum.LOGIC),
    OR("|", "或", "&", SignTypeEnum.LOGIC),
    DOR("||", "短路或", "&&", SignTypeEnum.LOGIC),
    PLUS("+", "加", "-", SignTypeEnum.OPERATOR),
    MINUS("-", "减", "+", SignTypeEnum.OPERATOR),
    MULTIPLY("*", "乘", "/", SignTypeEnum.OPERATOR),
    DIVIDE("/", "除", "*", SignTypeEnum.OPERATOR),
    LB("(", "左侧小括号", ")", SignTypeEnum.BRACKETS),
    RB(")", "右侧小括号", "(", SignTypeEnum.BRACKETS);

    private final String val;
    private final String name;
    private final String inverseSign;
    private final SignTypeEnum type;

    public static SignEnum match(String val) {
        return Arrays.stream(SignEnum.values())
                .filter(item -> Objects.equals(item.getVal(), val))
                .findFirst()
                .orElse(null);
    }

    public static List<SignEnum> noBracketsSigns() {
        return Arrays.stream(SignEnum.values())
                .filter(SignEnum::isNotBrackets)
                .collect(Collectors.toList());
    }

    public boolean isNotBrackets() {
        return type != SignTypeEnum.BRACKETS;
    }

    /** 获取逆向符号 */
    public SignEnum getInverseSign() {
        return match(inverseSign);
    }

    public List<SignEnum> getSubSigns() {
        return switch (this) {
            case GE -> Arrays.asList(GE, GT, EQ);
            case LE -> Arrays.asList(LE, LT, EQ);
            default -> List.of(this);
        };
    }

    public List<String> getSubSignStr() {
        List<SignEnum> subSigns = getSubSigns();
        return subSigns.stream().map(SignEnum::getVal).collect(Collectors.toList());
    }

    public boolean compare(String v1, String v2) {
        Double t1 = Double.parseDouble(v1);
        Double t2 = Double.parseDouble(v2);
        return compare(t1, t2);
    }

    public boolean compare(Double v1, Double v2) {
        BigDecimal t1 = BigDecimal.valueOf(v1);
        BigDecimal t2 = BigDecimal.valueOf(v2);
        return compare(t1, t2);
    }

    public boolean compare(BigDecimal v1, BigDecimal v2) {
        int res = v1.compareTo(v2);
        return switch (this) {
            case GT -> res > 0;
            case GE -> res >= 0;
            case LT -> res < 0;
            case LE -> res <= 0;
            case EQ, DEQ -> res == 0;
            default -> false;
        };
    }

    public static SignEnum combineSign(SignEnum signA, SignEnum signB) {
        String combineSign = signA.getVal() + signB.getVal();
        return match(combineSign);
    }
}
import com.googlecode.aviator.AviatorEvaluator;
import com.googlecode.aviator.Expression;
import com.thing.common.core.enumeration.SignEnum;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.util.CollectionUtils;

import java.util.*;

/**
 * @author siyang
 * @date 2024-03-01
 * @description 涉及公式的工具类
 */
public class FormulaUtil {

    /**
     * 验证输入的公式是否合法
     *
     * @param formula 公式表达式
     * @return 是否合法
     */
    public static boolean validateFormula(String formula) {
        // 记录每个符号的索引,以及上一个符号的位置
        List<Triple<Integer, Integer, SignEnum>> signIndexList = new ArrayList<>();
        // 判断括号是否对称出现,记录位置,递归处理括号内部的子公式
        Stack<Pair<Integer, SignEnum>> stack = new Stack<>();
        for (int i = 0; i < formula.length(); i++) {
            String ch = String.valueOf(formula.charAt(i));
            SignEnum sign = SignEnum.match(ch);
            if (Objects.isNull(sign)) {
                continue;
            }
            if (sign == SignEnum.LB) {
                stack.push(Pair.of(i, SignEnum.LB));
            } else if (sign == SignEnum.RB) {
                if (stack.isEmpty()) {
                    return false;
                }
                Pair<Integer, SignEnum> lbPair = stack.pop();
                if (lbPair.getLeft() >= (i - 1)) {
                    return false;
                }
                String subFormula = formula.substring(lbPair.getLeft() + 1, i);
                boolean subFormulaValid = validateFormula(subFormula);
                if (!subFormulaValid) {
                    return false;
                }
            } else {
                Integer lastSignIndex =
                        signIndexList.isEmpty()
                                ? null
                                : signIndexList.get(signIndexList.size() - 1).getMiddle();
                signIndexList.add(Triple.of(lastSignIndex, i, sign));
            }
        }
        if (!stack.isEmpty()) {
            return false;
        }

        // 没有括号:1. 不能以运算符作为开头和结尾, 2. 运算符不能连续出现
        List<SignEnum> signEnums = SignEnum.noBracketsSigns();
        for (SignEnum signEnum : signEnums) {
            if (formula.startsWith(signEnum.getVal()) || formula.endsWith(signEnum.getVal())) {
                return false;
            }
        }

        // 连续符号列表
        List<Triple<Integer, Integer, SignEnum>> continuousSignList =
                signIndexList.stream()
                        .filter(
                                t ->
                                        Objects.nonNull(t.getLeft())
                                                && (t.getMiddle() - t.getLeft() == 1))
                        .toList();

        for (Triple<Integer, Integer, SignEnum> t : continuousSignList) {
            String nextChar = String.valueOf(formula.charAt(t.getMiddle() + 1));
            SignEnum nextSign = SignEnum.match(nextChar);
            if (Objects.nonNull(nextSign) && nextSign.isNotBrackets()) {
                return false;
            }
            // 前一个符号与当前符号的组合是否是合法的符号
            String frontChar = String.valueOf(formula.charAt(t.getLeft()));
            SignEnum frontSign = SignEnum.match(frontChar);
            SignEnum combineSign = SignEnum.combineSign(frontSign, t.getRight());
            if (Objects.isNull(combineSign)) {
                return false;
            }
        }

        return true;
    }

	/**
     * 获取公式中的变量
     *
     * @param formula 公式
     * @return 公式中的变量
     */
    public static Set<String> getFormulaVariables(String formula) {
        Expression compiledExp = AviatorEvaluator.compile(formula);
        List<String> variableNames = compiledExp.getVariableNames();
        return new HashSet<>(variableNames);
    }

    /**
     * 执行计算公式
     *
     * @param formula 公式表达式
     * @param variableMap 变量-变量值map
     *    注意:变量值是类型敏感的,举例如下
     *         String formula = "A + B";
     *         Map<,> variableMap = new HashMap<>();
     *         variableMap.put("A", "1");  |  variableMap.put("A", 1);
     *         variableMap.put("B", "2");  |  variableMap.put("B", 2);
     *         ====> 结果为 "12"            |  ====> 结果为 3
     * @return 计算结果
     */
    public static Object executeFormula(String formula, Map<String, Object> variableMap) {
        Expression compiledExp = AviatorEvaluator.compile(formula);
        try {
            return compiledExp.execute(variableMap);
        } catch (Exception e) {
            log.error(
                    "====计算失败====\n公式: {}\n参数: {}\n错误信息: {}",
                    formula,
                    variableMap,
                    e.getMessage());
        }
        return null;
    }


}

公式运算需要添加如下依赖

<dependency>
	<groupId>com.googlecode.aviator</groupId>
    <artifactId>aviator</artifactId>
    <version>5.4.1</version>
</dependency>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值