基于范围的 Apache poi 公式评估

Apache poi formula evaluation for range based

提问人:Shaurya 提问时间:10/16/2023 最后编辑:Black catShaurya 更新时间:10/18/2023 访问量:174

问:

我们尝试在 java maven 项目中使用 poi 评估 xlsx 公式,但是如果公式具有范围或数组,则输出值是重复的。

以下是预期的 vs poi 输出:

预期输出

Expected output

POI 输出

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 能够正确计算。

尝试了公式赋值器和 .不会产生正确的结果。
尝试过,这看起来 poi 不支持这些公式。 使用 XSSFWorkBook。
evaluateAllevaluateFormulaCellevaluator.clearAllCachedResultValues();evaluator.notifySetFormula(cell);

    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>
java excel spring-boot apache-poi xlsx

评论

0赞 K.Nicholas 10/16/2023
stackoverflow 上的帖子从我这里一无所获。

答:

1赞 Axel Richter 10/17/2023 #1

这里的主要问题是 Apache POI 没有提供所有用作 .在这种特殊情况下,Excel 函数 ROW 由函数 org.apache.poi.ss.formula.functions.RowFunc 表示,该函数尚未准备好在数组上下文中运行。ArrayFunctionJava

这可以通过更改该函数的代码来更改。该代码需要考虑“如果 reference 是单元格范围,并且如果 ROW 作为垂直数组输入,则 ROW 以垂直数组的形式返回引用的行号。我的函数就是这样做的。JavaRowFuncArrayReady

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 处的 org.apache.poi.ss.formula.eval.FunctionEval 数组中。在下面的代码中,该方法将在调用 时执行此操作。Function[] functionsprepareFunctionEvalprepareFunctionEval(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

请注意,这些公式是使用 输入的数组公式。Apache POI 无法评估 Excel 365 的新动态数组公式和溢出数组行为。{=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

然后我打印: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 函数的 Apache POI Java 函数表示形式已准备好在数组上下文中工作,除了查看源代码外,别无他法。所有这些代码都位于 https://svn.apache.org/viewvc/poi/tags/REL_5_2_4/poi/src/main/java/org/apache/poi/ss/formula/ 和 sub 目录中。命名函数位于 /functions 或 /atp 子目录中。

但操作数有时也需要是 s。大多数操作数都位于 /eval 子目录中。例如,数组上下文已准备就绪。因此将在数组上下文中工作。但事实并非如此。所以在数组上下文中不起作用。ArrayFunctionUnaryPlusEval+A1:A10ConcatEvalA1:A10&B1:B10

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

评论

0赞 Shaurya 10/17/2023
谢谢你的回答@Alex。您建议的更改,是否会在 poi 的未来版本中添加?
0赞 Axel Richter 10/17/2023
@Shaurya:忘记了一个代码。.现在也提供了。而且我不知道在未来的 poi 版本中会添加什么。class ExcelEvaluateROWFormulaAsArray
0赞 Shaurya 10/18/2023
我尝试了上述方法,它确实适用于原始问题中的公式。但对于使用范围的更复杂的公式,这仍然失败。公式为: =IFERROR(SMALL(IF((COUNTIF($69:B69$B, $D$6:$D$62)=0)*ISNUMBER($D$6:$D$62), $D$6:$D$62, “A”), 1), INDEX($D$6:$D$62, MATCH(SMALL(IF(ISTEXT($D$6:$D$62)*(COUNTIF(B69:$B$69, $D$6:$D$62)=0), COUNTIF($D$6:$D$62, “<”&$D$6:$D$62), “”), 1), IF(ISTEXT($D$6:$D$62), COUNTIF($D$6:$D$62, “<”&$D$6:$D$62), “”), 0)))
0赞 Shaurya 10/18/2023
因此,对于 INDEX、ISTEXT 等函数,我们也必须编写这些自定义修复程序。我们如何检查哪个函数支持数组使用,哪个不支持?
0赞 Axel Richter 10/18/2023
@Shaurya:INDEX 和 ISTEXT 不是问题,而是非常特殊的未记录用法。请参阅我对答案的补充。COUNTIF