我们试图在Java Maven项目中使用POI来计算xlsx公式,但如果公式有范围或数组,则输出值是重复的.

以下是预期VS POI输出:

Expected output

Expected output

poi output

poi output

列J和K没有任何公式单元格. L列和M列具有依赖于J和K数据的公式单元.

下面是在列J和K的7行中定义的公式:

Column J :
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J6)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J6))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J7)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J7))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J8)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J8))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J9)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J9))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J10)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J10))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J11)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J11))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J12)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J12))))

Column K :
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K6)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K6))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K7)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K7))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K8)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K8))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K9)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K9))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K10)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K10))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K11)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K11))))
=IF(COUNTIF($J$6:$J$62,"?*")<ROW(K12)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K12))))

如果我们比较上面的图像(预期与POI输出),我们会看到POI给出了错误的结果,并重复了J和K中的项目,而MS EXCEL能够正确计算.

Tried formula evaluator evaluateAll and evaluateFormulaCell. Doesn't yield correct results.
Tried with evaluator.clearAllCachedResultValues(); and evaluator.notifySetFormula(cell); This looks like that poi is not supporting these formulas. XSSFWorkBook is used.

    FileInputStream fis = new FileInputStream(inputFile);
    Workbook workbook = new XSSFWorkbook(fis);
    FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
    // some code in between, loops etc
    if (cell.getCellType() == CellType.FORMULA) {
        evaluator.evaluateFormulaCell(cell);    
    }
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>            
        <version>5.2.4</version>
    </dependency>       
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.4</version>
    </dependency>

推荐答案

这里的主要问题是,ApachePOI没有提供使用AS ArrayFunction的所有功能.在该特殊情况下,Excel function ROWJava函数org.apache.poi.ss.formula.functions.RowFunc表示,该函数org.apache.poi.ss.formula.functions.RowFunc未准备好在数组上下文中运行.

这可以通过更改Java函数的代码来更改.该代码需要考虑"如果Reference是一个单元格区域,并且如果以垂直数组的形式输入row,则row以垂直数组的形式返回引用的行号."我的函数RowFuncArrayReady可以做到这一点.

import org.apache.poi.ss.formula.functions.*;

import org.apache.poi.ss.formula.eval.AreaEval;
import org.apache.poi.ss.formula.eval.ErrorEval;
import org.apache.poi.ss.formula.eval.NumberEval;
import org.apache.poi.ss.formula.eval.RefEval;
import org.apache.poi.ss.formula.eval.ValueEval;
import org.apache.poi.ss.formula.CacheAreaEval;

/**
 * Implementation for the Excel function ROW ready for usage as ArrayFunction
 */
public final class RowFuncArrayReady implements Function, ArrayFunction {
    @Override
    public ValueEval evaluate(ValueEval[] args, int srcRowIndex, int srcColumnIndex) {
        if (args.length > 1) {
            return ErrorEval.VALUE_INVALID;
        }
        if (args.length == 0) {
            return new NumberEval(srcRowIndex + 1.);
        } else {
            return evaluate(srcRowIndex, srcColumnIndex, args[0]);
        }
    }
        
    public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) {
        int rnum;
        if (arg0 instanceof AreaEval) {
            rnum = ((AreaEval)arg0).getFirstRow();
        } else if (arg0 instanceof RefEval) {
            rnum = ((RefEval)arg0).getRow();
        } else {
            // anything else is not valid argument
            return ErrorEval.VALUE_INVALID;
        }
        return new NumberEval(rnum + 1.);
    }
    
    @Override
    public ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex) {
        if (args.length > 1) {
            return ErrorEval.VALUE_INVALID;
        }
        if (args.length == 0) {
            return new NumberEval(srcRowIndex + 1.);
        } else if (args[0] instanceof AreaEval) {
            return evaluateAreaEval((AreaEval)args[0], srcRowIndex, srcColumnIndex);
        } else if (args[0] instanceof RefEval) {
            return evaluate(srcRowIndex, srcColumnIndex, (RefEval)args[0]);
        } else {
            // anything else is not valid argument
            return ErrorEval.VALUE_INVALID;
        }
    }   
    
    private ValueEval evaluateAreaEval(AreaEval ae, int srcRowIndex, int srcColumnIndex) {
        int w1, w2, h1, h2;
        int a1FirstCol = 0, a1FirstRow = 0;
        w1 = ae.getWidth();
        h1 = ae.getHeight();
        a1FirstCol = ae.getFirstColumn();
        a1FirstRow = ae.getFirstRow();
        w2 = 1;
        h2 = 1;
        int width = Math.max(w1, w2);
        int height = Math.max(h1, h2);
        ValueEval[] vals = new ValueEval[height * width];
        int idx = 0;
        for(int i = 0; i < height; i++){
            for(int j = 0; j < width; j++){
                vals[idx++] = evaluate(srcRowIndex, srcColumnIndex, ae.offset(i, i, j, j));
            }
        }
        if (vals.length == 1) {
            return vals[0];
        }
        return new CacheAreaEval(srcRowIndex, srcColumnIndex, srcRowIndex + height - 1, srcColumnIndex + width - 1, vals);
    }
        
}

要使该新函数成为Excel的ROW函数的默认表示形式,它需要位于索引8处的Function[] functionsorg.apache.poi.ss.formula.eval.FunctionEval数组中.在下面的代码中,方法prepareFunctionEval将在被调用为prepareFunctionEval(8, new RowFuncArrayReady());时执行此操作.

import org.apache.poi.ss.usermodel.*;

import java.io.FileInputStream;

class ExcelEvaluateROWFormulaAsArray  {
     
 static void prepareFunctionEval(int pos, org.apache.poi.ss.formula.functions.Function function) throws Exception {
  java.lang.reflect.Field _functions = Class.forName("org.apache.poi.ss.formula.eval.FunctionEval").getDeclaredField("functions");
  _functions.setAccessible(true); 
  org.apache.poi.ss.formula.functions.Function[] functions = (org.apache.poi.ss.formula.functions.Function[])_functions.get(null); 
  functions[pos] = function;
 }

 public static void main(String[] args) throws Exception {
     
  prepareFunctionEval(8, new RowFuncArrayReady());
   
  Workbook workbook = WorkbookFactory.create(new FileInputStream("./ExcelUsingRowFormulaInArrayContext.xlsx"));
  FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); 
  
  evaluator.setDebugEvaluationOutputForNextEval(true);  
 
  Sheet sheet = workbook.getSheetAt(0);
  
  for (Row row : sheet) {
   for (Cell cell : row) {
    if (CellType.FORMULA == cell.getCellType()) {
     System.out.println(cell.getCellFormula());
     CellValue cellValue = evaluator.evaluate(cell);
     System.out.println(cellValue);
    }
   }
  }
  
  workbook.close();
 }
}

我的ExcelUsingRowFormulaInArrayContext.xlsx看起来是这样的:

enter image description here

注意,公式{=IF(COUNTIF($J$6:$J$62,"?*")<ROW(J6)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J6))))}是使用CtrlShiftEnter.输入的数组公式Apache POI无法判断Excel 365的新动态数组公式和溢出数组行为.

然后我的ExcelEvaluateROWFormulaAsArray会打印出来:

IF(COUNTIF($J$6:$J$62,"?*")<ROW(J6)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J6))))
org.apache.poi.ss.usermodel.CellValue ["Text 1"]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K6)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K6))))
org.apache.poi.ss.usermodel.CellValue [1.0]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(J7)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J7))))
org.apache.poi.ss.usermodel.CellValue ["Text 2"]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K7)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K7))))
org.apache.poi.ss.usermodel.CellValue [2.0]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(J8)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J8))))
org.apache.poi.ss.usermodel.CellValue ["Text 3"]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K8)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K8))))
org.apache.poi.ss.usermodel.CellValue [3.0]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(J9)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J9))))
org.apache.poi.ss.usermodel.CellValue ["Text 4"]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K9)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K9))))
org.apache.poi.ss.usermodel.CellValue [4.0]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(J10)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J10))))
org.apache.poi.ss.usermodel.CellValue ["Text 5"]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K10)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K10))))
org.apache.poi.ss.usermodel.CellValue [5.0]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(J11)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J11))))
org.apache.poi.ss.usermodel.CellValue [""]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K11)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K11))))
org.apache.poi.ss.usermodel.CellValue [""]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(J12)-5,"",INDEX(J:J,SMALL(IF(J$6:J$62<>"",ROW(J$6:J$62)),ROWS(J$6:J12))))
org.apache.poi.ss.usermodel.CellValue [""]
IF(COUNTIF($J$6:$J$62,"?*")<ROW(K12)-5,"",INDEX(K:K,SMALL(IF(K$6:K$62<>"",ROW(K$6:K$62)),ROWS(K$6:K12))))
org.apache.poi.ss.usermodel.CellValue [""]

这与Excel的判断结果相同.

要判断Excel函数的哪些ApachePOI Java函数表示可以在数组上下文中使用,除了查看源代码之外别无 Select .所有这样的代码都在https://svn.apache.org/viewvc/poi/tags/REL_5_2_4/poi/src/main/java/org/apache/poi/ss/formula/及其子目录中.命名函数位于/Functions或/ATP子目录中.

但操作数有时也需要ArrayFunction.大多数操作数都在/ev子目录中.例如,UnaryPlusEval是数组上下文就绪的.因此+A1:A10将在数组上下文中工作.但ConcatEval并非如此.因此A1:A10&B1:B10将不能在数组上下文中工作.

但也有一些函数,其中数组上下文中的函数根本没有记录在某个地方.例如,对于COUNTIF function,如果在数组上下文中像COUNTIF($B$69:B69, $D$6:$D$62)一样使用,将会发生什么情况是不清楚的.如何处理What do you want to look for?中的数组?微软没有透露任何关于这方面的信息.因此,我们所能做的就是判断Excel在这种情况下做了什么,然后try 使用Java来编程Excel的行为.这是非常具有挑战性的,而且容易出错.因此,我怀疑会有人这么做.我怀疑即使是微软也不知道Excel用户发现了什么有趣的公式解决方案,特别是在使用数组上下文的情况下.因此,如果有人需要那些有趣的公式解决方案,那么这个解决方案应该使用真正的Excel应用程序.

Java相关问答推荐

ActivityCompat.请求收件箱自动拒绝权限

Java字符串常数池困惑

我可以从Java模块中排除maven资源文件夹吗?

Java 21虚拟线程会解决转向react 式单线程框架的主要原因吗?

Java JAR环境(JRE)是否支持模块?

Listview—在Android Java中正确链接项目时出错

无法传递消费者<;>;实例

编译多个.Java文件并运行一个依赖于用户参数的文件

在运行MVN测试时,为什么构建失败,并显示了java.lang.ClassNotFoundException:java.net.http.HttpResponse?

更新GWT 2.5.1到2.11.0和sencha GXT 3.1.1到4.1时出现错误

Mac上的全屏截图在使用JavaFX时不能正常工作吗?

S,要对Java复制构造函数深度克隆所有属性进行单元测试,最可靠的方法是什么?

使用While循环打印素数,无法正常工作

垃圾回收器是否真的删除超出作用域的对象?

X=x*0.90;产生有损转换误差.X*=0.90;不是.为什么?

使用MediaPlayer类在一段时间后停止播放音乐

持续时间--为什么在秒为负数的情况下还要做额外的工作?

java21预览未命名的符号用于try-with-resources

Spring Integration SFTP 连接失败 - 无法协商 kex 算法的密钥交换

对于 Hangman 游戏,索引 0 超出长度 0 的范围