业务需要,做一个用户自定义的公式计算。

公式由前端自定义,生成字符串发给后端,由后端完成计算。

公式需要支持四则运算,支持常用函数,支持函数的嵌套运算。

字符串类似这种

“1-2.5+SUM(3*4,5,SUM(IF(“皮卡”=“皮卡丘”,5,DAYS(“2022-10-19”,“2022-10-29”)),6))”

最终实现方案直接点这里

excel表格有很强大的公式计算功能,找了一下发现easyExcel已经实现了相关功能,写了一个简单工具

public class ExcelEqUtils {

    private static XSSFWorkbook workbook;

    private static XSSFCell cell;

    public static Object calculate(String text) {
        setTemplate(text);
        XSSFFormulaEvaluator evaluator = new XSSFFormulaEvaluator(workbook);
        CellValue evaluate = evaluator.evaluate(cell);
        CellType cellType = evaluate.getCellType();
        //根据返回值类型不同,读取对应返回值
        if (cellType.equals(CellType._NONE)) {
            return null;
        } else if (cellType.equals(CellType.NUMERIC)) {
            return BigDecimal.valueOf(cellValue.getNumberValue());
        } else if (cellType.equals(CellType.STRING)) {
            return cellValue.getStringValue();
        } else if (cellType.equals(CellType.FORMULA)) {
            return null;
        } else if (cellType.equals(CellType.BLANK)) {
            return null;
        } else if (cellType.equals(CellType.BOOLEAN)) {
            return cellValue.getBooleanValue();
        } else if (cellType.equals(CellType.ERROR)) {
            return ErrorEval.getText(cellValue.getErrorValue());
        } else {
            return null;
        }

    }

    private static void setTemplate(String text) {
        if (workbook == null) {
            workbook = new XSSFWorkbook();
            XSSFSheet sheet = workbook.createSheet();
            cell = sheet.createRow(0).createCell(0);
        }
        cell.setCellFormula(text);
    }
}

测试后发现简单的加减乘除四则运算,IF,SUM函数都没有问题,但是加了DAYS函数以后会报错

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))";
    System.out.println(calculate(expression));
}

Caused by: org.apache.poi.ss.formula.eval.NotImplementedFunctionException: DAYS

意思是 表达式里面的DAYS函数没有实现类。那可不可以自己实现呢?如果可以自己编写实现类进性拓展的话,是不是也可以实现自定义函数呢?

尝试了一下,发现完全可行

点进报错看了一下,找到了这个接口FreeRefFunction,再往上找到了Function

/**
 * Excel内置函数的所有实现的通用接口。
 *
 * @author Amol S. Deshmukh < amolweb at ya hoo dot com >
 */
public interface Function {

   /**
    * @param args the evaluated function arguments.  Empty values are represented with
    * {@link BlankEval} or {@link MissingArgEval}, never <code>null</code>.
    * @param srcRowIndex row index of the cell containing the formula under evaluation
    * @param srcColumnIndex column index of the cell containing the formula under evaluation
    * @return The evaluated result, possibly an {@link ErrorEval}, never <code>null</code>.
    * <b>Note</b> - Excel uses the error code <i>#NUM!</i> instead of IEEE <i>NaN</i>, so when
    * numeric functions evaluate to {@link Double#NaN} be sure to translate the result to {@link
    * ErrorEval#NUM_ERROR}.
    * 参数基本就是公式的参数值,excel里的方格坐标
    */
   ValueEval evaluate(ValueEval[] args, int srcRowIndex, int srcColumnIndex);
}

Excel内置函数的所有实现的通用接口。

image-20221025154703692

官方文档找一下,发现下面还有有很多接口,对应不同个数参数和自定义函数的情况,加减乘除四则运算其实就是Function2Arg的一个实现类。这样问题就好解决了。

找到了自定义函数的拓展路径,再把自定义的函数放进去让POI能读取到即可

点开XSSFWorkbook的源码,发现_udfFinder 是用户定义函数的定位器,默认包含Excel分析工具包中的功能

public class XSSFWorkbook extends POIXMLDocument implements Workbook, Date1904Support {
	/**
 	* The locator of user-defined functions.
 	* By default includes functions from the Excel Analysis Toolpack
 	*/
	private IndexedUDFFinder _udfFinder = new IndexedUDFFinder(AggregatingUDFFinder.DEFAULT);

    UDFFinder getUDFFinder() {
        return _udfFinder;
    }

    //用这个方法,可以直接往workbook里面加UDFFinder
    @Override
    public void addToolPack(UDFFinder toopack){
        _udfFinder.add(toopack);
    }
}

再往下点,点开默认值

/**
	一个UDFFinder,可以通过名称和伪索引检索函数。
 */
@Internal
public class IndexedUDFFinder extends AggregatingUDFFinder {
    private final HashMap<Integer, String> _funcMap;

    public IndexedUDFFinder(UDFFinder... usedToolPacks) {
        super(usedToolPacks);
        _funcMap = new HashMap<Integer, String>();
    }

    @Override
    public FreeRefFunction findFunction(String name) {
        FreeRefFunction func = super.findFunction(name);
        if (func != null) {
            int idx = getFunctionIndex(name);
            _funcMap.put(idx, name);
        }
        return func;
    }

    public String getFunctionName(int idx) {
        return _funcMap.get(idx);
    }

    public int getFunctionIndex(String name) {
        return name.hashCode();
    }
}

/**
 * 
 *将外接程序库收集到一个UDF查找器中
 * @author PUdalau
 */
public class AggregatingUDFFinder implements UDFFinder {
    
    /**
     * Default UDFFinder implementation
     */ 
    public static final UDFFinder DEFAULT = new AggregatingUDFFinder(AnalysisToolPak.instance);

    private final Collection<UDFFinder> _usedToolPacks;

    public AggregatingUDFFinder(UDFFinder ... usedToolPacks) {
        _usedToolPacks = new ArrayList<UDFFinder>(usedToolPacks.length);
        _usedToolPacks.addAll(Arrays.asList(usedToolPacks));
    }

    /**
     * Returns executor by specified name. Returns <code>null</code> if
     * function isn't contained by any registered tool pack.
     *
     * @param name Name of function.
     * @return Function executor. <code>null</code> if not found
     */
    @Override
    public FreeRefFunction findFunction(String name) {
        FreeRefFunction evaluatorForFunction;
        for (UDFFinder pack : _usedToolPacks) {
            evaluatorForFunction = pack.findFunction(name);
            if (evaluatorForFunction != null) {
                return evaluatorForFunction;
            }
        }
        return null;
    }

    /**
     * Add a new toolpack
     *
     * @param toolPack the UDF toolpack to add
     */
    public void add(UDFFinder toolPack){
        _usedToolPacks.add(toolPack);
    }
}

可以看到IndexedUDFFinder内部维护了一个UDFFinder集合,这个集合初始化的时候放进了一个默认工具包,里面实现了一些默认实现的函数。我们也可以将我们实现的函数放进去。

下面就是拓展代码

UDFFinder的findFunction方法是通过方法名发现FreeRefFunction类型的函数,那么我们的自定义函数也实现这个接口。

public class DAYSFunction implements FreeRefFunction {

    public static final FreeRefFunction instance = new DAYSFunction();

    private static final String DATE_TIME_PATTERN = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}";

    private static final String DATE_PATTERN = "\\d{4}-\\d{2}-\\d{2}";

    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) {
        //参数个数错误
        if (args.length != 2) {
            return ErrorEval.VALUE_INVALID;
        }
        try {
            LocalDateTime value0 = getValue(args[0]);
            LocalDateTime value1 = getValue(args[1]);

            long result = value0.until(value1, ChronoUnit.DAYS);
            return new NumberEval(result);
        } catch (EvaluationException e) {
            return e.getErrorEval();
        }
    }

    private LocalDateTime getValue(ValueEval arg) throws EvaluationException {
        //只接收字符串类型
        if (!(arg instanceof StringValueEval)) {
            throw new EvaluationException(ErrorEval.VALUE_INVALID);
        }
        StringValueEval realArg = (StringValueEval) arg;
        String stringValue = realArg.getStringValue();
        LocalDateTime localDateTime;
        if (stringValue.matches(DATE_TIME_PATTERN)) {
            localDateTime = LocalDateTime.parse(stringValue, DATE_TIME_FORMATTER);
        } else if (stringValue.matches(DATE_PATTERN)) {
            localDateTime = LocalDateTime.of(LocalDate.parse(stringValue, DATE_FORMATTER), LocalTime.MIN);
        } else {
            throw new EvaluationException(ErrorEval.VALUE_INVALID);
        }
        return localDateTime;
    }
}

参数校验和运算错误的时候,可以用POI的EvaluationException,也可以用自己定义的。

函数实现写好了,接下来就是实现我们自己的工具包,仿照Excel默认工具包AnalysisToolPak去写

/**
 * 自定义的工具包,这个UDFFinder可以发现我们自定义的函数
 */
public class MyPikaToolPak implements UDFFinder {

    public static final UDFFinder instance = new MyPikaToolPak();

    /**
     * 函数名:函数
     */
    private final Map<String, FreeRefFunction> _functionsByName = createFunctionsMap();

    private MyPikaToolPak() {
    }
    /**
     * 自定义的函数 注册进map里
     */
    private Map<String, FreeRefFunction> createFunctionsMap() {
        Map<String, FreeRefFunction> m = new HashMap(10);
        r(m, "DAYS", DAYSFunction.instance);
        return m;
    }
    
    @Override
    public FreeRefFunction findFunction(String name) {
        return _functionsByName.get(name);
    }

    private static void r(Map<String, FreeRefFunction> m, String functionName, FreeRefFunction pFunc) {
        FreeRefFunction func = pFunc == null ? new NotImplemented(functionName) : pFunc;
        m.put(functionName, func);
    }
    
    private static final class NotImplemented implements FreeRefFunction {

        private final String _functionName;

        private NotImplemented(String functionName) {
            _functionName = functionName;
        }

        public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) {
            throw new NotImplementedFunctionException(_functionName);
        }

    }
}

最后修改一下最初的工具类,把我们自己定义的工具包加载进去

public class ExcelEqUtils {

    private static XSSFWorkbook workbook;

    private static XSSFCell cell;

    private static UDFFinder myToolPack = MyPikaToolPak.instance;

    public static Object calculate(String text) {
        CellValue cellValue = getCellValue(text);
        CellType cellType = cellValue.getCellType();
        if (cellType.equals(CellType._NONE)) {
            return null;
        } else if (cellType.equals(CellType.NUMERIC)) {
            return BigDecimal.valueOf(cellValue.getNumberValue());
        } else if (cellType.equals(CellType.STRING)) {
            return cellValue.getStringValue();
        } else if (cellType.equals(CellType.FORMULA)) {
            return null;
        } else if (cellType.equals(CellType.BLANK)) {
            return null;
        } else if (cellType.equals(CellType.BOOLEAN)) {
            return cellValue.getBooleanValue();
        } else if (cellType.equals(CellType.ERROR)) {
            return ErrorEval.getText(cellValue.getErrorValue());
        } else {
            return null;
        }
    }

    private static CellValue getCellValue(String text) {
        if (workbook == null) {
            workbook = new XSSFWorkbook();
            workbook.addToolPack(myToolPack);
            XSSFSheet sheet = workbook.createSheet();
            cell = sheet.createRow(0).createCell(0);
        }
        cell.setCellFormula(text);
        XSSFFormulaEvaluator evaluator = new XSSFFormulaEvaluator(workbook);
        return evaluator.evaluate(cell);
    }

    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))";
        System.out.println("计算结果:" + calculate(expression));
    }

}

最后打印:计算结果:31.5

后续如果需要拓展新的自定义函数或者POI未实现的函数,增加一个FreeRefFunction接口的实现类,然后在我们自定义的工具包中把函数名和对应的实现类实例put进map中即可。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐