公司使用公式做各种业务已经很长时间了,但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>