业务需要,做一个用户自定义的公式计算。
公式由前端自定义,生成字符串发给后端,由后端完成计算。
公式需要支持四则运算,支持常用函数,支持函数的嵌套运算。
字符串类似这种
“1-2.5+SUM(3*4,5,SUM(IF(“皮卡”=“皮卡丘”,5,DAYS(“2022-10-19”,“2022-10-29”)),6))”
字符串四则运算,首先下意识就想到后缀表达式,思路就是基于后缀表达式添加函数的兼容。
中缀转后缀之前,首先把字符串分割成列表,这里规定函数中用到字符串为参数,比如日期,字符串,需要用""
包起来。
/**
* 将表达式转为list
*/
private static List<String> expressionToList(String text) {
int index = 0;
List<String> list = new ArrayList<>();
try {
String opStr = replaceFunctionStr(text);
do {
char ch = opStr.charAt(index);
if (ch < 48 || ch > 57) {
if (ch == '"') {
//是字符参数,截取到下一个引号为止
String str = "";
index += 1;
while (index < opStr.length() && opStr.charAt(index) != '"') {
str += opStr.charAt(index);
index++;
}
list.add(str);
index++;
} else {
//是操作符,判断多位操作符的情况 例如不等,大于等于
if (ch == '>' || ch == '<') {
String str = "";
while (index < opStr.length() && ((opStr.charAt(index) < 48 || opStr.charAt(index) > 57) && '(' != opStr.charAt(index) && '"' != opStr
.charAt(index))) {
str += opStr.charAt(index);
index++;
}
list.add(str);
} else {
//是操作符,直接添加至list中
index++;
list.add(ch + "");
}
}
} else if (ch >= 48 && ch <= 57) {
//是数字,判断多位数的情况
String str = "";
while (index < opStr.length() && ((opStr.charAt(index) >= 48 && opStr.charAt(index) <= 57) || '.' == opStr.charAt(index))) {
str += opStr.charAt(index);
index++;
}
list.add(str);
}
} while (index < opStr.length());
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_01);
}
return list;
}
执行结果:
*[1, -, 2.5, +, S, (, 3, , 4, , 5, , S, (, I, (, 皮卡, =, 皮卡丘, , 5, , D, (, 2022-10-19, , 2022-10-29, ), ), , 6, ), )]
再将中缀表达式List转为后缀表达式List
/**
* 中缀转后缀
*/
private static List<String> parseToSuffixExpression(List<String> expressionList) {
Stack<String> opStack = new Stack<>();
List<String> suffixList = new ArrayList<>();
for (String item : expressionList) {
if (isOperator(item)) {
//是操作符 判断操作符栈是否为空
if (opStack.isEmpty() || "(".equals(opStack.peek()) || priority(item) > priority(opStack.peek()) || ",".equals(opStack.peek())) {
//为空或者栈顶元素为左括号或者当前操作符大于栈顶操作符直接压栈
opStack.push(item);
} else if (",".equals(item)) {
//参数间隔符,说明聚合公式还没结束
if (isOperator(opStack.peek())) {
suffixList.add(opStack.pop());
opStack.push(item);
}
} else {
//否则将栈中元素出栈入队,直到遇到大于当前操作符或者遇到左括号时
while (!opStack.isEmpty() && !"(".equals(opStack.peek())) {
if (priority(item) <= priority(opStack.peek())) {
suffixList.add(opStack.pop());
} else {
break;
}
}
//当前操作符压栈
opStack.push(item);
}
} else if (isNumber(item)) {
suffixList.add(item);
} else if (isString(item)) {
suffixList.add(item);
} else if ("(".equals(item)) {
//是左括号,压栈
opStack.push(item);
} else if (")".equals(item)) {
int opParamNumber = 0;
//是右括号 ,将栈中元素弹出入队,直到遇到左括号,左括号出栈,但不入队
while (!opStack.isEmpty()) {
if ("(".equals(opStack.peek())) {
opStack.pop();
if ("S".equals(opStack.peek())) {
//左括号的下一个是S,那么S加上参数个数,出栈 入队
String op = opStack.pop();
opParamNumber += 1;
suffixList.add(op + opParamNumber);
}
if ("D".equals(opStack.peek())) {
//左括号的下一个是D,出栈 入队
suffixList.add(opStack.pop());
}
break;
} else {
String pop = opStack.pop();
if (",".equals(pop)) {
opParamNumber++;
} else {
suffixList.add(pop);
}
}
}
} else {
throw new RuntimeException("有非法字符!");
}
}
//循环完毕,如果操作符栈中元素不为空,将栈中元素出栈入队
while (!opStack.isEmpty()) {
suffixList.add(opStack.pop());
}
return suffixList;
}
执行结果:
中缀:
[1, -, 2.5, +, S, (, 3, *, 4, , 5, , S, (, I, (, 皮卡, =, 皮卡丘, , 5, , D, (, 2022-10-19, , 2022-10-29, ), ), , 6, ), )]
后缀:
[1, 2.5, -, 3, 4, *, 5, 皮卡, 皮卡丘, =, 5, 2022-10-19, 2022-10-29, D, I, 6, S2, S3, +]
这里把SUM函数的参数个数拼在操作符后面了,方便后续计算
最后计算
后缀表达式列表循环,如果是数字,入栈,如果是操作符,弹栈,计算
/**
* 计算
*/
private static Object calculate(List<String> suffixList) {
Stack<Object> stack = new Stack<>();
//遍历ls
for (String item : suffixList) {
if ((isNumber(item) || isString(item)) && !isOperator(item)) {
//如果是参数(数字、字符串)
//入栈
stack.push(item);
} else if (isSimpleOp(item)) {
//加减乘除
stack.push(doSimple(stack, item));
} else if (isFlagOp(item)) {
//等式运算
stack.push(doJudge(stack, item));
} else if (item.startsWith("S")) {
//sum
BigDecimal result = doSum(stack, item);
stack.push(result);
} else if (item.startsWith("I")) {
//if
String falseRes = stack.pop().toString();
String trueRes = stack.pop().toString();
String flag = stack.pop().toString();
stack.push(doIf(flag, trueRes, falseRes));
} else if (item.startsWith("D")) {
//days
Integer days = doDays(stack);
stack.push(days);
}
}
//将最后的stack中的数弹出
return stack.pop();
}
执行
public static void main(String[] args) {
String expression = "1-2.5+SUM(3*4,5,SUM(IF(\"皮卡\"=\"皮卡丘\",5,DAYS(\"2022-10-19\",\"2022-10-29\")),6))";
List<String> expressionList = expressionToList(expression);
System.out.println("中缀:" + expressionList);
List<String> suffixList = parseToSuffixExpression(expressionList);
System.out.println("后缀:" + suffixList);
System.out.println("结果:" + calculate(suffixList));
}
执行结果:
中缀:[1, -, 2.5, +, S, (, 3, *, 4, , 5, , S, (, I, (, 皮卡, =, 皮卡丘, , 5, , D, (, 2022-10-19, , 2022-10-29, ), ), , 6, ), )]
后缀:[1, 2.5, -, 3, 4, *, 5, 皮卡, 皮卡丘, =, 5, 2022-10-19, 2022-10-29, D, I, 6, S2, S3, +]
结果:31.5
这个实现只能说勉强能用,可维护性比较差,如果要添加新的规则比较复杂的函数,需要去上面改改逻辑,想办法兼容。
后面可能会想别的办法优化。
这里是 可拓展自定义函数的字符串公式计算优化方案
下面是上面三个方法中用到的一些非主要逻辑代码
具体计算
/**
* 计算天数函数
*/
private static Integer doDays(Stack<Object> stack) {
long result;
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate endDay = LocalDate.parse(stack.pop().toString(), formatter);
LocalDate startDay = LocalDate.parse(stack.pop().toString(), formatter);
result = startDay.until(endDay, ChronoUnit.DAYS);
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_06);
}
return Integer.parseInt(Long.toString(result));
}
/**
* 加减乘除运算
*/
private static Object doSimple(Stack<Object> stack, String op) {
//pop出两个数,并运算
BigDecimal num2 = BigDecimal.ZERO;
BigDecimal num1 = BigDecimal.ZERO;
try {
num2 = new BigDecimal(stack.pop().toString());
num1 = new BigDecimal(stack.pop().toString());
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_02);
}
BigDecimal res = BigDecimal.ZERO;
if (op.equals("+")) {
res = num1.add(num2);
} else if (op.equals("-")) {
res = num1.subtract(num2);
} else if (op.equals("*")) {
res = num1.multiply(num2);
} else if (op.equals("/")) {
if (num2.compareTo(BigDecimal.ZERO) == 0) {
throw new EqException(EqErrorEnum.E_03);
}
res = num1.divide(num2);
} else {
throw new EqException(EqErrorEnum.E_04);
}
return res;
}
/**
* SUM函数
*/
private static BigDecimal doSum(Stack<Object> stack, String op) {
int paramNum = Integer.parseInt(op.substring(1));
BigDecimal result = BigDecimal.ZERO;
for (int i = 0; i < paramNum; i++) {
try {
BigDecimal param = new BigDecimal(stack.pop().toString());
result = result.add(param);
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_06);
}
}
return result;
}
/**
* if函数
*/
private static String doIf(String flag, String trueRes, String falseRes) {
Boolean aBoolean;
try {
aBoolean = Boolean.valueOf(flag);
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_06);
}
return aBoolean ? trueRes : falseRes;
}
/**
* 不等式
*/
private static Boolean doJudge(Stack<Object> stack, String item) {
Object num2 = stack.pop();
Object num1 = stack.pop();
boolean res = false;
if (item.equals("=")) {
res = num1.equals(num2);
} else if (item.equals(">")) {
try {
BigDecimal param2 = new BigDecimal(num2.toString());
BigDecimal param1 = new BigDecimal(num1.toString());
res = param1.compareTo(param2) > 0;
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_02);
}
} else if (item.equals("<")) {
try {
BigDecimal param2 = new BigDecimal(num2.toString());
BigDecimal param1 = new BigDecimal(num1.toString());
res = param1.compareTo(param2) < 0;
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_02);
}
} else if (item.equals(">=")) {
try {
BigDecimal param2 = new BigDecimal(num2.toString());
BigDecimal param1 = new BigDecimal(num1.toString());
res = param1.compareTo(param2) >= 0;
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_02);
}
} else if (item.equals("<=")) {
try {
BigDecimal param2 = new BigDecimal(num2.toString());
BigDecimal param1 = new BigDecimal(num1.toString());
res = param1.compareTo(param2) < 0;
} catch (Throwable e) {
throw new EqException(EqErrorEnum.E_02);
}
} else if (item.equals("<>")) {
res = !num1.equals(num2);
} else {
throw new EqException(EqErrorEnum.E_04);
}
return res;
}
一些判断
private static String replaceFunctionStr(String text) {
return text.replaceAll("SUM", "S").replaceAll("IF", "I").replaceAll("DAYS", "D");
}
/**
* 判断字符串是否为操作符
*/
public static boolean isOperator(String op) {
return isSimpleOp(op) || isFlagOp(op) || isExpressOp(op) || isOtherOp(op);
}
/**
* 判断字符串是否为加减乘除
*/
private static boolean isSimpleOp(String op) {
return op.equals("+") || op.equals("-") || op.equals("*") || op.equals("/");
}
/**
* 判断字符串是否为等式操作符
*/
private static boolean isFlagOp(String op) {
return op.equals(">") || op.equals("<") || op.equals("=") || op.equals(">=") || op.equals("<=") || op.equals("<>");
}
/**
* 判断字符串是否为公式操作符
*/
private static boolean isExpressOp(String op) {
return op.startsWith("S") || op.equals("I") || op.equals("D");
}
/**
* 判断字符串是否为其他操作符
*/
private static boolean isOtherOp(String op) {
return op.equals(",");
}
/**
* 判断是否为数字
*/
public static boolean isNumber(String num) {
return num.matches("\\d+(\\.\\d+)?");
}
/**
* 判断是否为合法字符串参数 允许由汉字 数字 大小写英文字母 和 - 组成的字符串
*/
private static boolean isString(String num) {
return num.matches("[\\u4e00-\\u9fa50-9a-zA-Z-]{1,}$");
}
/**
* 获取操作符的优先级
*/
private static int priority(String op) {
if (op.equals("*") || op.equals("/") || isFlagOp(op) || isExpressOp(op)) {
return 1;
} else if (op.equals("+") || op.equals("-")) {
return 0;
}
return -1;
}
报错枚举
public enum EqErrorEnum implements IErrorCode {
E_01(30001L, "公式解析异常!"),
E_02(30002L, "四则运算参数异常!"),
E_03(30003L, "分母不能为0!"),
E_04(30004L, "运算符有误!"),
E_05(30005L, "等式运算参数异常!"),
E_06(30006L, "函数参数异常!"),
;
private Long code;
private String message;
EqErrorEnum(Long code, String message) {
this.code = code;
this.message = message;
}
@Override
public long getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}