Apache POI 和 SUBTOTAL 公式

Apache POI and SUBTOTAL formula

=SUMPRODUCT((K:K="yes")*(SUBTOTAL(103,OFFSET(K10,ROW(K:K)-MIN(ROW(K10:K10)),0))))

=SUMPRODUCT((K:K="yes")*(SUBTOTAL(103,OFFSET(K10,ROW(K:K)-MIN(ROW(K10:K10)),0))))+(NOW()*0)

这是使用 java poi api 在生成的 xls 工作簿中的一张工作表中使用的 excel 公式。 只有当我在 Excel 中的单元格上按 Enter 时,它才会正确计算。 公式计算器和 wb.setForceFormulaRecalculation(true) 似乎不起作用。

java 代码是:

cell.setCellFormula("SUMPRODUCT((K:K=\"yes\")*(SUBTOTAL(103,OFFSET(K10,ROW(K:K)-MIN(ROW(K10:K10)),0))))");

公式的目标是计算 "yes" 在 K 列中的出现次数,但仅针对过滤后的可见行。 K10 是实际数据开始的单元格。此单元格上方的行包含 headers。

我可以使用 HSSF 确认这是一个问题。但它与公式重新计算无关。 OFFSET 本身和 NOW 一样易变。即使不设置 setForceFormulaRecalculation(true),这两个强制公式重新计算。但是在二进制 *.xls 文件中,OFFSET 中的 ROW(K:K) 最初并不作为数组求值。所以它只计算一次 1 (ROW(K1)) 而不是数组 {1,2,3,4,5,...} (ROW(K1), ROW(K2), ROW(K3), ...)。使用 XSSF (*.xlsx) 它有效。

我发现问题在于 apache poi 如何为 SUMPRODUCT 创建 HSSF 公式。 SUMPRODUCT 总是一个数组函数。所以 SUMPRODUCT 中嵌入的所有函数也应该是数组函数。但是 apache poi 没有将 CLASS_ARRAY 设置为嵌入在 SUMPRODUCT 中的函数。相反,它将 CLASS_VALUE 设置为好像函数是独立的。

以下工作草案显示了这个问题。有一种方法 makeArrayFormula 可以将所有 FuncVarPtg(函数变量解析对象)从 CLASS_VALUE 更改为 CLASS_ARRAY。在 运行 该方法之后,SUMPRODUCT 公式在 HSSF.

中也按预期工作
import java.io.FileOutputStream;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
import org.apache.poi.ss.formula.ptg.Ptg;
import org.apache.poi.ss.formula.ptg.FuncVarPtg;

import java.lang.reflect.Field;

class CreateExcelFormula {

 static void makeArrayFormula(HSSFCell formulaCell) throws Exception {
  Field _record = HSSFCell.class.getDeclaredField("_record");
  _record.setAccessible(true); 
  CellValueRecordInterface recordInterface = (CellValueRecordInterface)_record.get(formulaCell);
  System.out.println(recordInterface);
  if (recordInterface instanceof FormulaRecordAggregate) {
   FormulaRecordAggregate formulaRecordAggregate = (FormulaRecordAggregate)recordInterface;
   FormulaRecord formulaRecord = formulaRecordAggregate.getFormulaRecord();
   Ptg[] ptgs = formulaRecord.getParsedExpression();
   for (Ptg ptg : ptgs) {
    if (ptg instanceof FuncVarPtg) {
     if (ptg.getPtgClass() == Ptg.CLASS_VALUE) {
      ptg.setClass(Ptg.CLASS_ARRAY);
     }
    }
   }
   formulaRecord.setParsedExpression(ptgs);
  }
  System.out.println(recordInterface);
 }

 public static void main(String[] args) throws Exception {

  try (
       Workbook workbook = new HSSFWorkbook(); FileOutputStream fileout = new FileOutputStream("Excel.xls") ) {
       //Workbook workbook = new XSSFWorkbook(); FileOutputStream fileout = new FileOutputStream("Excel.xlsx") ) {

   Sheet sheet = workbook.createSheet();
   Row row;
   Cell cell;

   row = sheet.createRow(0);

   cell = row.createCell(0);
   cell.setCellValue("F:");
   cell = row.createCell(1);
   //cell.setCellFormula("SUMPRODUCT((K:K=\"yes\")*(SUBTOTAL(103,OFFSET(K10,ROW(K:K)-MIN(ROW(K10:K10)),0))))");
   cell.setCellFormula("SUMPRODUCT((K10:K10000=\"yes\")*(SUBTOTAL(103,OFFSET(K10,ROW(K10:K10000)-ROW(K10),0))))");
   if (cell instanceof HSSFCell) {
    makeArrayFormula((HSSFCell)cell);
   }

   for (int r = 9; r < 30; r++) {
    row = sheet.createRow(r);
    cell = row.createCell(10);
    if (r % 2 == 0) cell.setCellValue("yes"); else cell.setCellValue("no");
   }

   for (int r = 14; r < 19; r++) {
    sheet.getRow(r).setZeroHeight(true);
   }  

   workbook.write(fileout);
  }

 }
}

顺便说一句:永远不要在数组公式中使用像 K:K 这样的完整列引用。这是一场性能噩梦。 ROW(K10:K10) 周围的 MIN 是多余的。