业务需要,做一个用户自定义的公式计算。
公式由前端自定义,生成字符串发给后端,由后端完成计算。
公式需要支持四则运算,支持常用函数,支持函数的嵌套运算。
字符串类似这种
“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 null
.
* @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 null
.
* Note - Excel uses the error code #NUM! instead of IEEE NaN, 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内置函数的所有实现的通用接口。
官方文档找一下,发现下面还有有很多接口,对应不同个数参数和自定义函数的情况,加减乘除四则运算其实就是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 null
if
* function isn't contained by any registered tool pack.
*
* @param name Name of function.
* @return Function executor. null
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中即可。