本专栏用于解析自己开源的项目代码,作为复盘和学习使用。欢迎大家一起交流
本样例说明源码开源在:
ruoyi-reoprt gitee仓库
ruoyi-report github仓库
欢迎大家到到项目中多给点star支持,对项目有建议或者有想要了解的欢迎一起讨论
需求背景
在报表数据处理中,用户先使用数据库中的字段向数据源读取到了需要的数据,或者使用字典将数据进行转化。对于一些特殊的字段,希望能进行一定的运算后,将结果输出。
比如在物流系统中,对订单计算出来的仓储费、物流费、卸货费,需要进行汇总,计算出这个订单的总费用。或者进行一些乘法运算,例: 运费 = 运费单价 × 数量。
具体实现
1. 运算公式构造
运算公式分为三个组成部分:变量、运算符号、常量
-
变量:报表中已有的字段,可以是源数据,也可以是其他公式的计算结果得到的值。在系统中用
${字段编码}
对变量进行区分,方便解析公式中哪些是变量
-
运算符号:四则运算符号+ - × /,三目运算符:a>b?1:0,取反符号:~(只能将非0变为0,和0变为1)
-
常量:可以直接填写数字到公式中,
例如计算时间戳时,常量可用86400计算天数, 相距天数 = (开始-结束)/ 86400000 -
整体公式说明:
//这是一个计算相隔天数的公式,其中RS014为结束时间,RS016为开始时间a,RS017为开始时间B // 这里先使用三目运算符进行判断开始时间a和b的大小,将较大的值和结束时间相减然后除于毫秒的时间戳 // 求出相差了多少天,得到一个天数的值。 (${RS014}-(${RS016}>${RS017}?${RS016}:${RS017}))/86400000
2. 运算公式解析
在构造完公式后,需要先对公式进行解析,将其中的变量转换为数值,变量的值是已经生成好并且存储在map中的,所以只需要跟进键值对取出对应的值,替换到公式中即可
// 将公式中的变量取出来,方便后续的步骤对数据进行替换
public static List<String> getVariables(String str) {
List<String> variables = new ArrayList<>();
// 正则表达式,匹配出以${开头,以}结尾的字符串
Pattern pattern = Pattern.compile("\\$\\{([^}]+)}");
if (str == null || str.length() <= 0) return variables;
// 匹配变量
Matcher matcher = pattern.matcher(str);
//将匹配到的变量添加到List中
while (matcher.find()) {
variables.add(matcher.group(1));
}
return variables;
}
// 传入公式以及字段和值的key-value map,
public static CalculateValueVO calculate(String exp, Map<String, Object> billResultMap) {
// 将exp字符串中,截取以RS开头+后3位作为变量值
JEP jep = new JEP();
List<String> varList = getVariables(exp);
// 正则表达式去掉表达式中的${}这三个符号
exp = exp.replaceAll("\\$\\{", "");
// 正则表达式去掉表达式中}这个符号
exp = exp.replaceAll("}", "");
// 抓取 Double.parseDouble转换错误,输入的值无法转化为数字
for (String s : varList) {
if (billResultMap.get(s) == null) {
billResultMap.put(s, "0");
}
if (billResultMap.get(s).toString().contains("计算失败")) {
return CalculateValueVO.builder()
.value("[" + s + "]计算失败,无法进行后续计算")
.calculateStatusEnum(BillResult.CalculateStatusEnum.FAIL)
.build();
}
try {
// 调用jep的方法将值设入变量中
jep.addVariable(s, Double.parseDouble(billResultMap.get(s).toString()));
} catch (Exception e) {
return CalculateValueVO.builder()
.value("[" + billResultMap.get(s).toString() + "]无法转换为公式")
.calculateStatusEnum(BillResult.CalculateStatusEnum.FAIL)
.build();
}
}
// 调用jep的方法计算公式,并获得返回值
jep.parseExpression(exp);
double result = jep.getValue();
return CalculateValueVO.builder()
.value(String.valueOf(result))
.calculateStatusEnum(BillResult.CalculateStatusEnum.SUCCESS)
.build();
}
3. 运算公式计算
这里对公式的核心运算主要调用了JEP这个工具包:
在maven中引入:
<dependency>
<groupId>jep</groupId>
<artifactId>jep</artifactId>
<version>2.24</version>
</dependency>
用法参考测试样例:
public static void testJEP() {
JEP jep = new JEP();
// 添加常用函数
jep.addStandardFunctions();
// 添加常用常量
jep.addStandardConstants();
String exp = "M12*3.14/4*pow(O5,2)*(K11+273-G11)/(G12*sqrt(3.14*M11*P11))"; //给变量赋值
jep.addVariable("M12", 1.1);
jep.addVariable("O5", 11.28665296);
jep.addVariable("K11", 25);
jep.addVariable("G11", 200);
jep.addVariable("G12", 100000);
jep.addVariable("M11", 0.000000129);
jep.addVariable("P11", Double.parseDouble("解析失败"));
try { //执行
jep.parseExpression(exp);
double result = jep.getValue();
System.out.println("计算结果: " + result);
} catch (Throwable e) {
System.out.println("An error occured: " + e.getMessage());
}
}
4. 多字段场景中的运算顺序问题
在第2点中,对公式的解析存在一个问题,如何确保,当前解析出来的变量,对应的值,是已经计算完成了的呢。
比如在计算:费用= 运费+仓储费,运费= 单价*数量,这里两条公式的时候,如何确保运费的计算在费用的计算这个公式前面,保证在计算费用的公式时,运费已经是有值了的。这里的解决方案是使用拓扑排序对所有公式的计算顺序进行了一次排序,具体参考:https://blog.csdn.net/noner_n/article/details/142986322 (拓扑排序在实际开发中的应用)
5. 完整代码含测试样例:
public class JEPUtils {
public static CalculateValueVO calculate(String exp, Map<String, Object> billResultMap) {
// 将exp字符串中,截取以RS开头+后3位作为变量值
JEP jep = new JEP();
List<String> varList = getVariables(exp);
// 正则表达式去掉表达式中的${}这三个符号
exp = exp.replaceAll("\\$\\{", "");
// 正则表达式去掉表达式中}这个符号
exp = exp.replaceAll("}", "");
// 抓取 Double.parseDouble转换错误,输入的值无法转化为数字
for (String s : varList) {
if (billResultMap.get(s) == null) {
billResultMap.put(s, "0");
}
if (billResultMap.get(s).toString().contains("计算失败")) {
return CalculateValueVO.builder()
.value("[" + s + "]计算失败,无法进行后续计算")
.calculateStatusEnum(BillResult.CalculateStatusEnum.FAIL)
.build();
}
try {
jep.addVariable(s, Double.parseDouble(billResultMap.get(s).toString()));
} catch (Exception e) {
return CalculateValueVO.builder()
.value("[" + billResultMap.get(s).toString() + "]无法转换为公式")
.calculateStatusEnum(BillResult.CalculateStatusEnum.FAIL)
.build();
}
}
jep.parseExpression(exp);
double result = jep.getValue();
return CalculateValueVO.builder()
.value(String.valueOf(result))
.calculateStatusEnum(BillResult.CalculateStatusEnum.SUCCESS)
.build();
}
public static List<String> getVariables(String str) {
List<String> variables = new ArrayList<>();
// 正则表达式,匹配出以${开头,以}结尾的字符串
Pattern pattern = Pattern.compile("\\$\\{([^}]+)}");
if (str == null || str.length() <= 0) return variables;
// 匹配变量
Matcher matcher = pattern.matcher(str);
//将匹配到的变量添加到List中
while (matcher.find()) {
variables.add(matcher.group(1));
}
return variables;
}
public static void main(String[] args) {
testJEP();
}
public static void testJEP() {
JEP jep = new JEP();
// 添加常用函数
jep.addStandardFunctions();
// 添加常用常量
jep.addStandardConstants();
String exp = "M12*3.14/4*pow(O5,2)*(K11+273-G11)/(G12*sqrt(3.14*M11*P11))"; //给变量赋值
jep.addVariable("M12", 1.1);
jep.addVariable("O5", 11.28665296);
jep.addVariable("K11", 25);
jep.addVariable("G11", 200);
jep.addVariable("G12", 100000);
jep.addVariable("M11", 0.000000129);
jep.addVariable("P11", Double.parseDouble("解析失败"));
try { //执行
jep.parseExpression(exp);
double result = jep.getValue();
System.out.println("计算结果: " + result);
} catch (Throwable e) {
System.out.println("An error occured: " + e.getMessage());
}
}
//
}