灵活自定义数学公式,业务场景高级使用jep

自定义数学公式: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);
    }

相关推荐
bearpping29 分钟前
SpringBoot最佳实践之 - 使用AOP记录操作日志
java·spring boot·后端
一叶飘零_sweeeet31 分钟前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·后端·spring
开心就好20252 小时前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默2 小时前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦2 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl2 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6863 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情3 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player3 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明3 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展