From b9fab268254974d4185b74ccc42697f85acab293 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Wed, 23 Dec 2009 20:58:01 +0000 Subject: [PATCH] added tests for XSSF usermodel for array formulas, this change is a step towards adoption of the patch submitted in Bugzilla 48292 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@893625 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/ss/util/CellRangeAddress.java | 17 +- .../apache/poi/xssf/usermodel/XSSFCell.java | 58 ++++++- .../apache/poi/xssf/usermodel/XSSFSheet.java | 108 ++++++++++++- .../poi/xssf/usermodel/TestXSSFSheet.java | 151 ++++++++++++++++++ .../poi/hssf/record/cf/TestCellRange.java | 14 ++ 5 files changed, 342 insertions(+), 6 deletions(-) diff --git a/src/java/org/apache/poi/ss/util/CellRangeAddress.java b/src/java/org/apache/poi/ss/util/CellRangeAddress.java index a84f13573..8c907116a 100644 --- a/src/java/org/apache/poi/ss/util/CellRangeAddress.java +++ b/src/java/org/apache/poi/ss/util/CellRangeAddress.java @@ -82,10 +82,21 @@ public class CellRangeAddress extends CellRangeAddressBase { return sb.toString(); } + /** + * @param ref usually a standard area ref (e.g. "B1:D8"). May be a single cell + * ref (e.g. "B5") in which case the result is a 1 x 1 cell range. + */ public static CellRangeAddress valueOf(String ref) { int sep = ref.indexOf(":"); - CellReference cellFrom = new CellReference(ref.substring(0, sep)); - CellReference cellTo = new CellReference(ref.substring(sep + 1)); - return new CellRangeAddress(cellFrom.getRow(), cellTo.getRow(), cellFrom.getCol(), cellTo.getCol()); + CellReference a; + CellReference b; + if (sep == -1) { + a = new CellReference(ref); + b = a; + } else { + a = new CellReference(ref.substring(0, sep)); + b = new CellReference(ref.substring(sep + 1)); + } + return new CellRangeAddress(a.getRow(), b.getRow(), a.getCol(), b.getCol()); } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java index 55aa71cfc..e5a193ee6 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java @@ -37,6 +37,7 @@ import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.FormulaError; import org.apache.poi.ss.usermodel.Hyperlink; import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.model.StylesTable; @@ -344,7 +345,11 @@ public final class XSSFCell implements Cell { if(cellType != CELL_TYPE_FORMULA) throw typeMismatch(CELL_TYPE_FORMULA, cellType, false); CTCellFormula f = _cell.getF(); - if(f.getT() == STCellFormulaType.SHARED){ + if (isPartOfArrayFormulaGroup() && f == null) { + XSSFCell cell = getSheet().getFirstCellInArrayFormula(this); + return cell.getCellFormula(); + } + if (f.getT() == STCellFormulaType.SHARED) { return convertSharedFormula((int)f.getSi()); } return f.getStringValue(); @@ -370,7 +375,29 @@ public final class XSSFCell implements Cell { return FormulaRenderer.toFormulaString(fpb, fmla); } + /** + * Sets formula for this cell. + *

+ * Note, this method only sets the formula string and does not calculate the formula value. + * To set the precalculated value use {@link #setCellValue(double)} or {@link #setCellValue(String)} + *

+ * + * @param formula the formula to set, e.g. "SUM(C4:E4)". + * If the argument is null then the current formula is removed. + * @throws org.apache.poi.ss.formula.FormulaParseException if the formula has incorrect syntax or is otherwise invalid + */ public void setCellFormula(String formula) { + setFormula(formula, FormulaType.CELL); + } + + /* package */ void setCellArrayFormula(String formula, CellRangeAddress range) { + setFormula(formula, FormulaType.ARRAY); + CTCellFormula cellFormula = _cell.getF(); + cellFormula.setT(STCellFormulaType.ARRAY); + cellFormula.setRef(range.formatAsString()); + } + + private void setFormula(String formula, int formulaType) { XSSFWorkbook wb = _row.getSheet().getWorkbook(); if (formula == null) { wb.onDeleteFormula(this); @@ -461,7 +488,7 @@ public final class XSSFCell implements Cell { */ public int getCellType() { - if (_cell.getF() != null) { + if (_cell.getF() != null || getSheet().isCellInArrayFormulaContext(this)) { return CELL_TYPE_FORMULA; } @@ -941,4 +968,31 @@ public final class XSSFCell implements Cell { } throw new IllegalStateException("Unexpected formula result type (" + cellType + ")"); } + + /** + * If this cell is part of an array formula, returns a CellRangeAddress object + * that represents the entire array. + * + * @return the range of the array formula group that this cell belongs to. + * @throws IllegalStateException if this cell is not part of an array formula + * @see #isPartOfArrayFormulaGroup() + */ + public CellRangeAddress getArrayFormulaRange() { + XSSFCell cell = getSheet().getFirstCellInArrayFormula(this); + if (cell == null) { + throw new IllegalStateException("Cell " + _cell.getR() + " is not part of an array formula"); + } + String formulaRef = cell._cell.getF().getRef(); + return CellRangeAddress.valueOf(formulaRef); + } + + /** + * Test if this cell is included in an array formula + * + * @return true if this cell is part of an array formula + * @see #getArrayFormulaRange() + */ + public boolean isPartOfArrayFormulaGroup() { + return getSheet().isCellInArrayFormulaContext(this); + } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index a9b0c145b..0574a000b 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -79,6 +79,7 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { private ColumnHelper columnHelper; private CommentsTable sheetComments; private Map sharedFormulas; + private List arrayFormulas; /** * Creates new XSSFSheet - called by XSSFWorkbook to create a sheet from scratch. @@ -153,6 +154,7 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { private void initRows(CTWorksheet worksheet) { rows = new TreeMap(); sharedFormulas = new HashMap(); + arrayFormulas = new ArrayList(); for (CTRow row : worksheet.getSheetData().getRowArray()) { XSSFRow r = new XSSFRow(row, this); rows.put(r.getRowNum(), r); @@ -2316,9 +2318,12 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { //collect cells holding shared formulas CTCell ct = cell.getCTCell(); CTCellFormula f = ct.getF(); - if(f != null && f.getT() == STCellFormulaType.SHARED && f.isSetRef() && f.getStringValue() != null){ + if (f != null && f.getT() == STCellFormulaType.SHARED && f.isSetRef() && f.getStringValue() != null) { sharedFormulas.put((int)f.getSi(), cell); } + if (f != null && f.getT() == STCellFormulaType.ARRAY && f.getRef() != null) { + arrayFormulas.add(CellRangeAddress.valueOf(f.getRef())); + } } @Override @@ -2676,4 +2681,105 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { private boolean sheetProtectionEnabled() { return worksheet.getSheetProtection().getSheet(); } + + /* package */ boolean isCellInArrayFormulaContext(XSSFCell cell) { + for (CellRangeAddress range : arrayFormulas) { + if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { + return true; + } + } + return false; + } + + /* package */ XSSFCell getFirstCellInArrayFormula(XSSFCell cell) { + for (CellRangeAddress range : arrayFormulas) { + if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { + return getRow(range.getFirstRow()).getCell(range.getFirstColumn()); + } + } + return null; + } + + /** + * Sets array formula to the specified range of cells. + *

+ * Note, that this method silently creates cells in the + * specified range if they don't exist. + *

+ * Example: + *
+     *  Workbook workbook = new XSSFWorkbook();
+     *  Sheet sheet = workbook.createSheet();
+     *  CellRangeAddress range = CellRangeAddress.valueOf("C1:C3");
+     *  Cell[] cells = sheet.setArrayFormula("A1:A3*B1:B3", range);
+     * 
+ * Three cells in the C1:C3 range are created and returned. + * + * @param formula the formula to set + * @param range Region of array formula for result. + * @return the array of cells that represent the entire formula array + * @throws org.apache.poi.ss.formula.FormulaParseException if + * the formula has incorrect syntax or is otherwise invalid + */ + public XSSFCell[] setArrayFormula(String formula, CellRangeAddress range) { + XSSFRow row = getRow(range.getFirstRow()); + if (row == null) { + row = createRow(range.getFirstRow()); + } + XSSFCell mainArrayFormulaCell = row.getCell(range.getFirstColumn()); + if (mainArrayFormulaCell == null) { + mainArrayFormulaCell = row.createCell(range.getFirstColumn()); + } + mainArrayFormulaCell.setCellArrayFormula(formula, range); + arrayFormulas.add(range); + + XSSFCell[] cells = new XSSFCell[range.getNumberOfCells()]; + int k = 0; + for (int rowIndex = range.getFirstRow(); rowIndex <= range.getLastRow(); rowIndex++) { + row = getRow(rowIndex); + if (row == null) { + row = createRow(rowIndex); + } + for (int columnIndex = range.getFirstColumn(); columnIndex <= range.getLastColumn(); columnIndex++) { + XSSFCell arrayFormulaCell = row.getCell(columnIndex); + if (arrayFormulaCell == null) { + arrayFormulaCell = row.createCell(columnIndex); + } + cells[k++] = arrayFormulaCell; + } + } + return cells; + } + + /** + * Remove an Array Formula from this sheet. + *

+ * All cells contained in the Array Formula range are removed as well + *

+ * + * @param cell any cell within Array Formula range + * @return the array of affected cells. + * @throws IllegalArgumentException if the specified cell is not part of an array formula + */ + public XSSFCell[] removeArrayFormula(Cell cell) { + ArrayList lst = new ArrayList(); + for (CellRangeAddress range : arrayFormulas) { + if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { + arrayFormulas.remove(range); + for (int rowIndex = range.getFirstRow(); rowIndex <= range.getLastRow(); rowIndex++) { + XSSFRow row = getRow(rowIndex); + for (int columnIndex = range.getFirstColumn(); columnIndex <= range.getLastColumn(); columnIndex++) { + XSSFCell arrayFormulaCell = row.getCell(columnIndex); + if (arrayFormulaCell != null) { + arrayFormulaCell.setCellType(Cell.CELL_TYPE_BLANK); + lst.add(arrayFormulaCell); + } + } + } + return lst.toArray(new XSSFCell[lst.size()]); + } + } + String ref = ((XSSFCell)cell).getCTCell().getR(); + throw new IllegalArgumentException("Cell " + ref + " is not part of an array formula"); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java index b66d85e2a..5d7dff3ee 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java @@ -32,6 +32,8 @@ import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXf; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STPane; +import java.util.Arrays; + public class TestXSSFSheet extends BaseTestSheet { @@ -912,4 +914,153 @@ public class TestXSSFSheet extends BaseTestSheet { //existing cells are invalidated assertEquals(0, wsh.getSheetData().getRowArray(0).sizeOfCArray()); } + + public void testSetArrayFormula_File() throws Exception { + XSSFWorkbook workbook = new XSSFWorkbook("D:\\java\\apache\\apache-poi\\bugzilla\\array-formulas\\template.xlsx"); + XSSFSheet sheet1 = workbook.getSheetAt(0); + sheet1.setArrayFormula("SUM(C1:C2*D1:D2)", CellRangeAddress.valueOf("B1")); + sheet1.setArrayFormula("MAX(C1:C2-D1:D2)", CellRangeAddress.valueOf("B2")); + + XSSFSheet sheet2 = workbook.getSheetAt(1); + sheet2.setArrayFormula("A1:A3*B1:B3", CellRangeAddress.valueOf("C1:C3")); + + java.io.FileOutputStream out = new java.io.FileOutputStream("D:\\java\\apache\\apache-poi\\bugzilla\\array-formulas\\poi-template.xlsx"); + workbook.write(out); + out.close(); + } + + public void testSetArrayFormula() throws Exception { + XSSFCell[] cells; + + XSSFWorkbook workbook = new XSSFWorkbook(); + XSSFSheet sheet = workbook.createSheet(); + XSSFCell cell = sheet.createRow(0).createCell(0); + assertFalse(cell.isPartOfArrayFormulaGroup()); + assertFalse(sheet.isCellInArrayFormulaContext(cell)); + try { + CellRangeAddress range = cell.getArrayFormulaRange(); + fail("expected exception"); + } catch (IllegalStateException e){ + assertEquals("Cell A1 is not part of an array formula", e.getMessage()); + } + + // 1. single-cell formula + + // row 3 does not yet exist + assertNull(sheet.getRow(2)); + CellRangeAddress range = new CellRangeAddress(2, 2, 2, 2); + cells = sheet.setArrayFormula("SUM(C11:C12*D11:D12)", range); + assertEquals(1, cells.length); + // sheet.setArrayFormula creates rows and cells for the designated range + assertNotNull(sheet.getRow(2)); + cell = sheet.getRow(2).getCell(2); + assertNotNull(cell); + + assertTrue(cell.isPartOfArrayFormulaGroup()); + assertSame(cells[0], sheet.getFirstCellInArrayFormula(cells[0])); + //retrieve the range and check it is the same + assertEquals(range.formatAsString(), cell.getArrayFormulaRange().formatAsString()); + + // 2. multi-cell formula + //rows 3-5 don't exist yet + assertNull(sheet.getRow(3)); + assertNull(sheet.getRow(4)); + assertNull(sheet.getRow(5)); + + range = new CellRangeAddress(3, 5, 2, 2); + assertEquals("C4:C6", range.formatAsString()); + cells = sheet.setArrayFormula("SUM(A1:A3*B1:B3)", range); + assertEquals(3, cells.length); + + // sheet.setArrayFormula creates rows and cells for the designated range + assertEquals("C4", cells[0].getCTCell().getR()); + assertEquals("C5", cells[1].getCTCell().getR()); + assertEquals("C6", cells[2].getCTCell().getR()); + assertSame(cells[0], sheet.getFirstCellInArrayFormula(cells[0])); + + /* + * For a multi-cell formula, the c elements for all cells except the top-left + * cell in that range shall not have an f element; + */ + assertEquals("SUM(A1:A3*B1:B3)", cells[0].getCTCell().getF().getStringValue()); + assertNull(cells[1].getCTCell().getF()); + assertNull(cells[2].getCTCell().getF()); + + for(XSSFCell acell : cells){ + assertTrue(acell.isPartOfArrayFormulaGroup()); + assertEquals(Cell.CELL_TYPE_FORMULA, acell.getCellType()); + assertEquals("SUM(A1:A3*B1:B3)", acell.getCellFormula()); + //retrieve the range and check it is the same + assertEquals(range.formatAsString(), acell.getArrayFormulaRange().formatAsString()); + } + } + + public void testRemoveArrayFormula() throws Exception { + XSSFCell[] cells; + + XSSFWorkbook workbook = new XSSFWorkbook(); + XSSFSheet sheet = workbook.createSheet(); + + CellRangeAddress range = new CellRangeAddress(3, 5, 2, 2); + assertEquals("C4:C6", range.formatAsString()); + cells = sheet.setArrayFormula("SUM(A1:A3*B1:B3)", range); + assertEquals(3, cells.length); + + // remove the formula cells in C4:C6 + XSSFCell[] dcells = sheet.removeArrayFormula(cells[0]); + // removeArrayFormula should return the same cells as setArrayFormula + assertTrue(Arrays.equals(cells, dcells)); + + for(XSSFCell acell : cells){ + assertFalse(acell.isPartOfArrayFormulaGroup()); + assertEquals(Cell.CELL_TYPE_BLANK, acell.getCellType()); + } + + //invocation on a not-array-formula cell throws IllegalStateException + try { + sheet.removeArrayFormula(cells[0]); + fail("expected exception"); + } catch (IllegalArgumentException e){ + assertEquals("Cell C4 is not part of an array formula", e.getMessage()); + } + } + + public void testReadArrayFormula() throws Exception { + XSSFCell[] cells; + + XSSFWorkbook workbook = new XSSFWorkbook(); + XSSFSheet sheet1 = workbook.createSheet(); + cells = sheet1.setArrayFormula("SUM(A1:A3*B1:B3)", CellRangeAddress.valueOf("C4:C6")); + assertEquals(3, cells.length); + + cells = sheet1.setArrayFormula("MAX(A1:A3*B1:B3)", CellRangeAddress.valueOf("A4:A6")); + assertEquals(3, cells.length); + + XSSFSheet sheet2 = workbook.createSheet(); + cells = sheet2.setArrayFormula("MIN(A1:A3*B1:B3)", CellRangeAddress.valueOf("D2:D4")); + assertEquals(3, cells.length); + + workbook = getTestDataProvider().writeOutAndReadBack(workbook); + sheet1 = workbook.getSheetAt(0); + for(int rownum=3; rownum <= 5; rownum++) { + XSSFCell cell1 = sheet1.getRow(rownum).getCell(2); + assertTrue( sheet1.isCellInArrayFormulaContext(cell1)); + assertTrue( cell1.isPartOfArrayFormulaGroup()); + + XSSFCell cell2 = sheet1.getRow(rownum).getCell(0); + assertTrue( sheet1.isCellInArrayFormulaContext(cell2)); + assertTrue( cell2.isPartOfArrayFormulaGroup()); + } + + sheet2 = workbook.getSheetAt(1); + for(int rownum=1; rownum <= 3; rownum++) { + XSSFCell cell1 = sheet2.getRow(rownum).getCell(3); + assertTrue( sheet2.isCellInArrayFormulaContext(cell1)); + assertTrue( cell1.isPartOfArrayFormulaGroup()); + } + XSSFCell acnhorCell = sheet2.getRow(1).getCell(3); + XSSFCell fmlaCell = sheet2.getRow(2).getCell(3); + assertSame(acnhorCell, sheet2.getFirstCellInArrayFormula(fmlaCell)); + assertSame(acnhorCell, sheet2.getFirstCellInArrayFormula(acnhorCell)); + } } diff --git a/src/testcases/org/apache/poi/hssf/record/cf/TestCellRange.java b/src/testcases/org/apache/poi/hssf/record/cf/TestCellRange.java index ced198a5a..b13c6fd24 100644 --- a/src/testcases/org/apache/poi/hssf/record/cf/TestCellRange.java +++ b/src/testcases/org/apache/poi/hssf/record/cf/TestCellRange.java @@ -192,4 +192,18 @@ public final class TestCellRange extends TestCase assertEquals(1, cr3.length); assertEquals("A1:B2", cr3[0].formatAsString()); } + + public void testValueOf() { + CellRangeAddress cr1 = CellRangeAddress.valueOf("A1:B1"); + assertEquals(0, cr1.getFirstColumn()); + assertEquals(0, cr1.getFirstRow()); + assertEquals(1, cr1.getLastColumn()); + assertEquals(0, cr1.getLastRow()); + + CellRangeAddress cr2 = CellRangeAddress.valueOf("B1"); + assertEquals(1, cr2.getFirstColumn()); + assertEquals(0, cr2.getFirstRow()); + assertEquals(1, cr2.getLastColumn()); + assertEquals(0, cr2.getLastRow()); + } }