自定义数学公式:jep灵活运用。
由于最近事情比较繁忙,所以就没有静下心或者有时间去将所学的东西整理分享。今天忙里偷闲,分享一篇关于jep的灵活使用。
浏览了很多关于jep的文章,感觉都是在原本jep的基础上去应用,计算一些数据。比较局限,那么我在根据自己的业务需求的时候,看了下jep的源码,并且添加了部分代码去更加灵活的应用。
业务需求
为什么想到去用jep呢,是因为我在项目过程中遇到了一个,希望可以自己自定义统计量公式的需求,例如平均值,方差,还有根据他们的值进行一些权重分配,混合运算的要求,那么需要根据存储在数据库中的字符串表达式,来在后端进行动态的识别,并且读取数据,根据公式字符串,返回对应统计量的结果。
我们希望传入动态的数据,根据不同的公式,计算出值。
jep重要源码及使用
首先我们先简介一下jep。
JEP是Java expression parser的简称,即java表达式分析器,Jep是一个用来转换和计算数学表达式的java库。通过这个程序包,用户可以以字符串的形式输入一个、任意的公式,然后快速地计算出结果。Jep支持用户自定义变量、常量和函数。包括许多常用的数学函数和常量。
这些都是封装好的函数。
java
public void addStandardFunctions() {
//add functions to Function Table
funTab.put("sin", new Sine());
funTab.put("cos", new Cosine());
funTab.put("tan", new Tangent());
funTab.put("asin", new ArcSine());
funTab.put("acos", new ArcCosine());
funTab.put("atan", new ArcTangent());
funTab.put("atan2", new ArcTangent2());
funTab.put("sinh", new SineH());
funTab.put("cosh", new CosineH());
funTab.put("tanh", new TanH());
funTab.put("asinh", new ArcSineH());
funTab.put("acosh", new ArcCosineH());
funTab.put("atanh", new ArcTanH());
funTab.put("log", new Logarithm());
funTab.put("ln", new NaturalLogarithm());
funTab.put("exp", new Exp());
funTab.put("pow", new Power());
funTab.put("sqrt",new SquareRoot());
funTab.put("abs", new Abs());
funTab.put("mod", new Modulus());
funTab.put("sum", new Sum());
funTab.put("rand", new org.nfunk.jep.function.Random());
// rjm additions
funTab.put("if", new If());
funTab.put("str", new Str());
// rjm 13/2/05
funTab.put("binom", new Binomial());
// rjm 26/1/07
funTab.put("round",new Round());
funTab.put("floor",new Floor());
funTab.put("ceil",new Ceil());
}
简单的来说,就是可以用算术运算符代替之前的java的公式,显示和逻辑编写更加清晰简单。
我们继续深层的读取源码,会发现它继承了一个类:
PostfixMathCommand:
java
public class PostfixMathCommand implements PostfixMathCommandI {
protected int numberOfParameters = 0;
protected int curNumberOfParameters = 0;
public PostfixMathCommand() {
}
protected void checkStack(Stack inStack) throws ParseException {
if (null == inStack) {
throw new ParseException("Stack argument null");
}
}
public int getNumberOfParameters() {
return this.numberOfParameters;
}
public void setCurNumberOfParameters(int n) {
this.curNumberOfParameters = n;
}
public boolean checkNumberOfParameters(int n) {
if (this.numberOfParameters == -1) {
return true;
} else {
return this.numberOfParameters == n;
}
}
public void run(Stack s) throws ParseException {
throw new ParseException("run() method of PostfixMathCommand called");
}
}
那么这里面比较有用的两个参数:
protected int numberOfParameters = 0;
protected int curNumberOfParameters = 0;
一个表示可变参数,一个表示参数个数。
为什么重要呢,因为这里面的函数计算,会是栈的计算,也就是会把传入参数弹出之后,将计算结果存入栈里返回,如果像定义一个公式sum(data)/count(data),那么count运算时候,弹栈的时候,就不能把sum存入的结果也弹出了,为什么会这么说,因为我最开始运用弹出数据控制的时候,就是用的inStack.isEmpty()方法来控制循环的,所以最后会返回NaN的结果,我们需要根据curNumberOfParameters来进行循环。
简单代码示例
看了上面的说明,如果是初次接触可能会很懵,没关系,我们看一下简单实例来理解。
csharp
@Test
public void getValueNet(){
JEP jep = new JEP();
// 添加常用函数
jep.addStandardFunctions();
// 添加常用常量
jep.addStandardConstants();
String exp = "M12*3.14/4"; //给变量赋值
jep.addVariable("M12", 6.0);
try { //执行
jep.parseExpression(exp);
double result = jep.getValue();
System.out.println("计算结果: " + result);
} catch (Throwable e) {
System.out.println("An error occured: " + e.getMessage());
}
}
这便是最简单的jep代码示例:
下面我通过图片来解释:
编辑
不足的地方
如果说,只是计算一两个数,给定的公式,给定的数据集,而且必须很小,才能这样计算,因为我们看源码发现,这个jep原本add变量的时候,竟然是一个一个加的。
这里的symTab就是它存储变量,也就是后面用来公式识别的一个成员变量。
说白了,就是太笨拙了,而我们后端需要的数据,是变化的,因此,我们需要更改源码,并且编写工具类,来提升它。
修改源码
我规定传入的时候是:
count(A1,A2,A3,....)等等这样的数据
JepPlus
kotlin
public class JepPlus extends JEP {
public JepPlus(){
super();
}
public Double addVariable(String name, double value) {
Double object = new Double(value);
//这个就是读取对象放进去,给对应的字符串对应值
this.symTab.makeVarIfNeeded(name, object);
return object;
}
public void addArrayVariable(List<String> value){
//现在接收到一个数组,data。data里有三个数据
// 现在我们遍历数组,给数组的每个对象按顺序起一名字并把对应的值放入进去
for(int i=0;i<value.size();i++){
this.symTab.makeVarIfNeeded("A"+(i+1),Double.parseDouble(value.get(i)));
}
}
}
快排工具类:
java
package com.neu.statistics.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class QuickSortUtil {
public static void main(String[] args) {
List<Double> arr= new ArrayList<>();
System.out.println("原始数组:"+ Arrays.toString(arr.toArray()));
//获取数组arr的长度
List<Double> newList = quickSort(arr, 0, arr.size() - 1);
System.out.println("排序后的数组:"+ Arrays.toString(newList.toArray()));
}
public static List<Double> quickSort(List<Double> arr, int left, int right) {
//递归结束条件left < right
if (left < right) {
// 通过分区函数得到基准元素的索引
int pivotIndex = partition(arr, left, right);
//递归对基准元素左边的子数组进行快速排序
quickSort(arr, left, pivotIndex - 1);
//递归对基准元素右边的子数组进行快速排序
quickSort(arr, pivotIndex + 1, right);
}
return arr;
}
public static int partition(List<Double> arr, int left, int right) {
// 选择最后一个元素作为基准元素
double pivot = arr.get(right);
int i = left;
//int[] arr = new int[]{5,7,3,3,6,4};
//循环数组,如果满足条件,则将满足条件的元素交换到arr[i],同时i++,循环完成之后i之前的元素则全部为小于基准元素的元素
for (int j = left; j < right; j++) {
if (arr.get(j) < pivot) {
if (j != i) {
swap(arr, i, j);
}
i++;//数交换之后,需要左指针i右移
}
}
// 交换 arr[i] 和基准元素
swap(arr, i, right);
//返回基准元素的下标
return i;
}
public static void swap(List<Double> arr, int left, int right) {
double temp = arr.get(left);
//交换list中的两个元素
arr.set(left, arr.get(right));
arr.set(right, temp);
}
}
字符串转换工具类
我这里定义我的数据库存储数据,数据集合统一用data来表示。以后也可以根据业务不同需求来进行扩展。
由于我们数据库存储的是sum(data)/count(data)
那么data是我们传入的一组字符串数据,我们需要在识别到data之后,把他变成sum(A1,A2,A3)/count(A1,A2,A3),这样jep就可以将变量A1,A2,A3存入他的成员变量里面
java
package com.neu.statistics.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DynamicStringConversion {
public static void main(String[] args) {
// 原始表达式
String formula = "sum(data)/count(data)";
// 模拟从数据库或其他源获取数据
List<String> dataList = new ArrayList<>();
dataList.add("1");
dataList.add("2");
dataList.add("3");
// 动态转换表达式
String dynamicFormula = convertFormula(formula, dataList);
// 输出转换后的表达式
System.out.println("初始字符串: " + formula);
System.out.println("转换后的字符串: " + dynamicFormula);
}
// 动态替换表达式中的 data
public static String convertFormula(String formula, List<String> dataList) {
// 匹配 data 字符串的正则表达式
String regex = "\bdata\b";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(formula);
// 动态替换每个匹配的 data
StringBuffer result = new StringBuffer();
while (matcher.find()) {
// 将匹配到的 data 替换为 a1, a2, a3 格式
matcher.appendReplacement(result, buildReplacement(dataList));
}
matcher.appendTail(result);
return result.toString();
}
// 根据 dataList 构建替换字符串,使用 a1, a2, ... 的变量名
public static String buildReplacement(List<String> dataList) {
// 构建 a1, a2, a3 格式
StringBuilder replacement = new StringBuilder();
for (int i = 0; i < dataList.size(); i++) {
replacement.append("A").append(i + 1);
if (i < dataList.size() - 1) {
replacement.append(", ");
}
}
return replacement.toString();
}
}
自定义公式
目前我写了平均值,最大值,最小值方差等公式,以后我会根据业务需求继续拓展,目前给出这些。
scss
package com.neu.statistics.utils;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.PostfixMathCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
public class FormulaCalculator {
public String calculateFormula(List<String> valueList, String formula) throws ParseException {
String exp = DynamicStringConversion.convertFormula(formula, valueList);
JepPlus jep = new JepPlus();
jep.addStandardFunctions(); // 添加标准函数(可选)
jep.addStandardConstants(); // 添加标准常量(可选)
// 注册自定义的 sum 和 count 函数
jep.addFunction("sum", new SumFunction());
jep.addFunction("count", new CountFunction());
jep.addFunction("min",new MinFunction());
jep.addFunction("max",new MaxFunction());
jep.addFunction("median",new MedianFunction());
jep.addFunction("variance",new VarianceFunction());
jep.addArrayVariable(valueList);
jep.parseExpression(exp);
// 输出结果
return String.valueOf(jep.getValue());
}
}
//求和
class SumFunction extends PostfixMathCommand {
public SumFunction(){
numberOfParameters = -1; //可变参数
}
@Override
public void run(Stack inStack) throws ParseException {
//检查栈
this.checkStack(inStack);
List<Object> params = new ArrayList<>();
for(int i=0;i<this.curNumberOfParameters;i++){
params.add(inStack.pop());
}
inStack.push(this.sum(params));
}
public Object sum(List<Object> params) throws ParseException {
double sum=0.0;
for (Object param : params) {
if (param instanceof Number) {
sum += ((Number)param).doubleValue();
} else {
throw new ParseException("Invalid parameter type");
}
}
return sum;
}
}
//求总数
class CountFunction extends PostfixMathCommand{
public CountFunction(){
numberOfParameters = -1; //可变参数
}
public void run(Stack inStack) throws ParseException {
//检查栈
this.checkStack(inStack);
List<Object> params = new ArrayList<>();
for(int i=0;i<this.curNumberOfParameters;i++){
params.add(inStack.pop());
}
// while(!inStack.isEmpty()){
// params.add(inStack.pop());
// }
inStack.push(this.count(params));
}
public Object count(List<Object> params) throws ParseException {
double count=0.0;
for (Object param : params) {
if (param instanceof Number) {
count += 1;
} else {
throw new ParseException("Invalid parameter type");
}
}
return count;
}
}
//求最小值
class MinFunction extends PostfixMathCommand {
public MinFunction() {
numberOfParameters = -1; //可变参数
}
public void run(Stack inStack) throws ParseException {
//检查栈
this.checkStack(inStack);
List<Object> params = new ArrayList<>();
for (int i = 0; i < this.curNumberOfParameters; i++) {
params.add(inStack.pop());
}
inStack.push(this.min(params));
}
public Object min(List<Object> params) throws ParseException{
//快速排序,返回最小值,也就是数组的第一个元素
//将object转换为double
List<Double> list = new ArrayList<>();
for (Object param : params) {
if (param instanceof Number) {
list.add(((Number) param).doubleValue());
} else {
throw new ParseException("Invalid parameter type");
}
}
List<Double> newList=QuickSortUtil.quickSort(list,0,list.size()-1);
Object result=newList.get(0);
return result;
}
}
//求最大值
class MaxFunction extends PostfixMathCommand {
public MaxFunction() {
numberOfParameters = -1; //可变参数
}
public void run(Stack inStack) throws ParseException {
//检查栈
this.checkStack(inStack);
List<Object> params = new ArrayList<>();
for (int i = 0; i < this.curNumberOfParameters; i++) {
params.add(inStack.pop());
}
inStack.push(this.max(params));
}
public Object max(List<Object> params) throws ParseException{
//快速排序,返回最小值,也就是数组的第一个元素
//将object转换为double
List<Double> list = new ArrayList<>();
for (Object param : params) {
if (param instanceof Number) {
list.add(((Number) param).doubleValue());
} else {
throw new ParseException("Invalid parameter type");
}
}
List<Double> newList=QuickSortUtil.quickSort(list,0,list.size()-1);
Object result=newList.get(list.size()-1);
return result;
}
}
//中位数
class MedianFunction extends PostfixMathCommand {
public MedianFunction() {
numberOfParameters = -1; //可变参数
}
public void run(Stack inStack) throws ParseException {
//检查栈
this.checkStack(inStack);
List<Object> params = new ArrayList<>();
for (int i = 0; i < this.curNumberOfParameters; i++) {
params.add(inStack.pop());
}
inStack.push(this.median(params));
}
public Object median(List<Object> params) throws ParseException{
//快速排序,返回最小值,也就是数组的第一个元素
//将object转换为double
List<Double> list = new ArrayList<>();
for (Object param : params) {
if (param instanceof Number) {
list.add(((Number) param).doubleValue());
} else {
throw new ParseException("Invalid parameter type");
}
}
List<Double> newList=QuickSortUtil.quickSort(list,0,list.size()-1);
int length=newList.size();
if(newList.size()%2==0){
Object result=(newList.get(length/2)+newList.get(length/2-1))/2;
return result;
}
else{
Object result=newList.get(length/2);
return result;
}
}
}
//方差
class VarianceFunction extends PostfixMathCommand {
public VarianceFunction() {
numberOfParameters = -1; //可变参数
}
public void run(Stack inStack) throws ParseException {
//检查栈
this.checkStack(inStack);
List<Object> params = new ArrayList<>();
for (int i = 0; i < this.curNumberOfParameters; i++) {
params.add(inStack.pop());
}
inStack.push(this.variance(params));
}
public Object variance(List<Object> params) throws ParseException{
SumFunction sf=new SumFunction();
double sum=(double)sf.sum(params);
CountFunction cf=new CountFunction();
double count=(double)cf.count(params);
double average=sum/count;
//快速排序,返回最小值,也就是数组的第一个元素
//将object转换为double
List<Double> list = new ArrayList<>();
for (Object param : params) {
if (param instanceof Number) {
list.add(((Number) param).doubleValue());
} else {
throw new ParseException("Invalid parameter type");
}
}
List<Double> newList=QuickSortUtil.quickSort(list,0,list.size()-1);
double sumOfSquaredDifferences = 0.0;
for(double value:newList){
double difference=value-average;
sumOfSquaredDifferences+=difference*difference;
}
Object result=sumOfSquaredDifferences/count;
return result;
}
}
应用示例:
ini
@Test
public void test1() throws ParseException {
List<String> list=new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
String formula = "variance(data)";
FormulaCalculator formulaCalculator=new FormulaCalculator();
String result = formulaCalculator.calculateFormula(list,formula);
System.out.println(result);
}