diff --git a/src/java/org/apache/poi/ss/formula/FormulaParser.java b/src/java/org/apache/poi/ss/formula/FormulaParser.java index 74cbd87fb..3ddcd7463 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParser.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParser.java @@ -614,7 +614,7 @@ public final class FormulaParser { } Table tbl = _book.getTable(tableName); if (tbl == null) { - throw new FormulaParseException("Illegal table name!"); + throw new FormulaParseException("Illegal table name: '" + tableName + "'"); } String sheetName = tbl.getSheetName(); @@ -623,6 +623,8 @@ public final class FormulaParser { int startRow = tbl.getStartRowIndex(); int endRow = tbl.getEndRowIndex(); + // Do NOT return before done reading all the structured reference tokens from the input stream. + // Throwing exceptions is okay. int savePtr0 = _pointer; GetChar(); @@ -650,9 +652,9 @@ public final class FormulaParser { } else if (specName.equals(specTotals)) { isTotalsSpec = true; } else { - throw new FormulaParseException("Unknown special qunatifier "+ specName); + throw new FormulaParseException("Unknown special quantifier "+ specName); } - nSpecQuantifiers++ ; + nSpecQuantifiers++; if (look == ','){ GetChar(); } else { @@ -708,7 +710,7 @@ public final class FormulaParser { } else if (name.equals(specTotals)) { isTotalsSpec = true; } else { - throw new FormulaParseException("Unknown special qunatifier "+ name); + throw new FormulaParseException("Unknown special quantifier "+ name); } nSpecQuantifiers++; } else { @@ -718,6 +720,22 @@ public final class FormulaParser { } else { Match(']'); } + // Done reading from input stream + // Ok to return now + + if (isTotalsSpec && !tbl.isHasTotalsRow()) { + return new ParseNode(ErrPtg.REF_INVALID); + } + if ((isThisRow || isThisRowSpec) && (_rowIndex < startRow || endRow < _rowIndex)) { + // structured reference is trying to reference a row above or below the table with [#This Row] or [@] + if (_rowIndex >= 0) { + return new ParseNode(ErrPtg.VALUE_INVALID); + } else { + throw new FormulaParseException( + "Formula contained [#This Row] or [@] structured reference but this row < 0. " + + "Row index must be specified for row-referencing structured references."); + } + } int actualStartRow = startRow; int actualEndRow = endRow; @@ -756,10 +774,11 @@ public final class FormulaParser { actualStartRow++; } } + //Selecting cols - if (nColQuantifiers == 2){ - if (startColumnName == null || endColumnName == null){ + if (nColQuantifiers == 2) { + if (startColumnName == null || endColumnName == null) { throw new IllegalStateException("Fatal error"); } int startIdx = tbl.findColumnIndex(startColumnName); @@ -770,8 +789,8 @@ public final class FormulaParser { actualStartCol = startCol+ startIdx; actualEndCol = startCol + endIdx; - } else if(nColQuantifiers == 1){ - if (startColumnName == null){ + } else if (nColQuantifiers == 1 && !isThisRow) { + if (startColumnName == null) { throw new IllegalStateException("Fatal error"); } int idx = tbl.findColumnIndex(startColumnName); @@ -781,10 +800,10 @@ public final class FormulaParser { actualStartCol = startCol + idx; actualEndCol = actualStartCol; } - CellReference tl = new CellReference(actualStartRow, actualStartCol); - CellReference br = new CellReference(actualEndRow, actualEndCol); + CellReference topLeft = new CellReference(actualStartRow, actualStartCol); + CellReference bottomRight = new CellReference(actualEndRow, actualEndCol); SheetIdentifier sheetIden = new SheetIdentifier( null, new NameIdentifier(sheetName, true)); - Ptg ptg = _book.get3DReferencePtg(new AreaReference(tl, br), sheetIden); + Ptg ptg = _book.get3DReferencePtg(new AreaReference(topLeft, bottomRight), sheetIden); return new ParseNode(ptg); } @@ -793,7 +812,7 @@ public final class FormulaParser { * Caller should save pointer. * @return */ - private String parseAsColumnQuantifier(){ + private String parseAsColumnQuantifier() { if ( look != '[') { return null; } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaParser.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaParser.java index d6802d0fb..bda58860d 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaParser.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaParser.java @@ -22,6 +22,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.IOException; + import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; @@ -44,6 +46,9 @@ public final class TestXSSFFormulaParser { private static Ptg[] parse(FormulaParsingWorkbook fpb, String fmla) { return FormulaParser.parse(fmla, fpb, FormulaType.CELL, -1); } + private static Ptg[] parse(FormulaParsingWorkbook fpb, String fmla, int rowIndex) { + return FormulaParser.parse(fmla, fpb, FormulaType.CELL, -1, rowIndex); + } @Test public void basicParsing() { @@ -137,7 +142,7 @@ public final class TestXSSFFormulaParser { } @Test - public void formaulReferncesSameWorkbook() { + public void formulaReferencesSameWorkbook() { // Use a test file with "other workbook" style references // to itself XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("56737.xlsx"); @@ -530,4 +535,169 @@ public final class TestXSSFFormulaParser { assertTrue("Had " + Arrays.toString(ptgs), ptgs[2] instanceof AreaPtg); assertTrue("Had " + Arrays.toString(ptgs), ptgs[3] instanceof NameXPxg); } + + @Test + public void parseStructuredReferences() throws IOException { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("StructuredReferences.xlsx"); + + XSSFEvaluationWorkbook fpb = XSSFEvaluationWorkbook.create(wb); + Ptg[] ptgs; + + /* + The following cases are tested (copied from FormulaParser.parseStructuredReference) + 1 Table1[col] + 2 Table1[[#Totals],[col]] + 3 Table1[#Totals] + 4 Table1[#All] + 5 Table1[#Data] + 6 Table1[#Headers] + 7 Table1[#Totals] + 8 Table1[#This Row] + 9 Table1[[#All],[col]] + 10 Table1[[#Headers],[col]] + 11 Table1[[#Totals],[col]] + 12 Table1[[#All],[col1]:[col2]] + 13 Table1[[#Data],[col1]:[col2]] + 14 Table1[[#Headers],[col1]:[col2]] + 15 Table1[[#Totals],[col1]:[col2]] + 16 Table1[[#Headers],[#Data],[col2]] + 17 Table1[[#This Row], [col1]] + 18 Table1[ [col1]:[col2] ] + */ + + final String tbl = "\\_Prime.1"; + final String noTotalsRowReason = ": Tables without a Totals row should return #REF! on [#Totals]"; + + ////// Case 1: Evaluate Table1[col] with apostrophe-escaped #-signs //////// + ptgs = parse(fpb, "SUM("+tbl+"[calc='#*'#])"); + assertEquals(2, ptgs.length); + + // Area3DPxg [sheet=Table ! A2:A7] + assertTrue(ptgs[0] instanceof Area3DPxg); + Area3DPxg ptg0 = (Area3DPxg) ptgs[0]; + assertEquals("Table", ptg0.getSheetName()); + assertEquals("A2:A7", ptg0.format2DRefAsString()); + // Note: structured references are evaluated and resolved to regular 3D area references. + assertEquals("Table!A2:A7", ptg0.toFormulaString()); + + // AttrPtg [sum ] + assertTrue(ptgs[1] instanceof AttrPtg); + AttrPtg ptg1 = (AttrPtg) ptgs[1]; + assertTrue(ptg1.isSum()); + + ////// Case 1: Evaluate "Table1[col]" //////// + ptgs = parse(fpb, tbl+"[Name]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[col]", "Table!B2:B7", ptgs[0].toFormulaString()); + + ////// Case 2: Evaluate "Table1[[#Totals],[col]]" //////// + ptgs = parse(fpb, tbl+"[[#Totals],[col]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Totals],[col]]" + noTotalsRowReason, ErrPtg.REF_INVALID, ptgs[0]); + + ////// Case 3: Evaluate "Table1[#Totals]" //////// + ptgs = parse(fpb, tbl+"[#Totals]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[#Totals]" + noTotalsRowReason, ErrPtg.REF_INVALID, ptgs[0]); + + ////// Case 4: Evaluate "Table1[#All]" //////// + ptgs = parse(fpb, tbl+"[#All]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[#All]", "Table!A1:C7", ptgs[0].toFormulaString()); + + ////// Case 5: Evaluate "Table1[#Data]" (excludes Header and Data rows) //////// + ptgs = parse(fpb, tbl+"[#Data]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[#Data]", "Table!A2:C7", ptgs[0].toFormulaString()); + + ////// Case 6: Evaluate "Table1[#Headers]" //////// + ptgs = parse(fpb, tbl+"[#Headers]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[#Headers]", "Table!A1:C1", ptgs[0].toFormulaString()); + + ////// Case 7: Evaluate "Table1[#Totals]" //////// + ptgs = parse(fpb, tbl+"[#Totals]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[#Totals]" + noTotalsRowReason, ErrPtg.REF_INVALID, ptgs[0]); + + ////// Case 8: Evaluate "Table1[#This Row]" //////// + ptgs = parse(fpb, tbl+"[#This Row]", 2); + assertEquals(1, ptgs.length); + assertEquals("Table1[#This Row]", "Table!A3:C3", ptgs[0].toFormulaString()); + + ////// Evaluate "Table1[@]" (equivalent to "Table1[#This Row]") //////// + ptgs = parse(fpb, tbl+"[@]", 2); + assertEquals(1, ptgs.length); + assertEquals("Table!A3:C3", ptgs[0].toFormulaString()); + + ////// Evaluate "Table1[#This Row]" when rowIndex is outside Table //////// + ptgs = parse(fpb, tbl+"[#This Row]", 10); + assertEquals(1, ptgs.length); + assertEquals("Table1[#This Row]", ErrPtg.VALUE_INVALID, ptgs[0]); + + ////// Evaluate "Table1[@]" when rowIndex is outside Table //////// + ptgs = parse(fpb, tbl+"[@]", 10); + assertEquals(1, ptgs.length); + assertEquals("Table1[@]", ErrPtg.VALUE_INVALID, ptgs[0]); + + ////// Evaluate "Table1[[#Data],[col]]" //////// + ptgs = parse(fpb, tbl+"[[#Data], [Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Data],[col]]", "Table!C2:C7", ptgs[0].toFormulaString()); + + + ////// Case 9: Evaluate "Table1[[#All],[col]]" //////// + ptgs = parse(fpb, tbl+"[[#All], [Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#All],[col]]", "Table!C1:C7", ptgs[0].toFormulaString()); + + ////// Case 10: Evaluate "Table1[[#Headers],[col]]" //////// + ptgs = parse(fpb, tbl+"[[#Headers], [Number]]"); + assertEquals(1, ptgs.length); + // also acceptable: Table1!B1 + assertEquals("Table1[[#Headers],[col]]", "Table!C1:C1", ptgs[0].toFormulaString()); + + ////// Case 11: Evaluate "Table1[[#Totals],[col]]" //////// + ptgs = parse(fpb, tbl+"[[#Totals],[Name]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Totals],[col]]" + noTotalsRowReason, ErrPtg.REF_INVALID, ptgs[0]); + + ////// Case 12: Evaluate "Table1[[#All],[col1]:[col2]]" //////// + ptgs = parse(fpb, tbl+"[[#All], [Name]:[Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#All],[col1]:[col2]]", "Table!B1:C7", ptgs[0].toFormulaString()); + + ////// Case 13: Evaluate "Table1[[#Data],[col]:[col2]]" //////// + ptgs = parse(fpb, tbl+"[[#Data], [Name]:[Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Data],[col]:[col2]]", "Table!B2:C7", ptgs[0].toFormulaString()); + + ////// Case 14: Evaluate "Table1[[#Headers],[col1]:[col2]]" //////// + ptgs = parse(fpb, tbl+"[[#Headers], [Name]:[Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Headers],[col1]:[col2]]", "Table!B1:C1", ptgs[0].toFormulaString()); + + ////// Case 15: Evaluate "Table1[[#Totals],[col]:[col2]]" //////// + ptgs = parse(fpb, tbl+"[[#Totals], [Name]:[Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Totals],[col]:[col2]]" + noTotalsRowReason, ErrPtg.REF_INVALID, ptgs[0]); + + ////// Case 16: Evaluate "Table1[[#Headers],[#Data],[col]]" //////// + ptgs = parse(fpb, tbl+"[[#Headers],[#Data],[Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[#Headers],[#Data],[col]]", "Table!C1:C7", ptgs[0].toFormulaString()); + + ////// Case 17: Evaluate "Table1[[#This Row], [col1]]" //////// + ptgs = parse(fpb, tbl+"[[#This Row], [Number]]", 2); + assertEquals(1, ptgs.length); + // also acceptable: Table!C3 + assertEquals("Table1[[#This Row], [col1]]", "Table!C3:C3", ptgs[0].toFormulaString()); + + ////// Case 18: Evaluate "Table1[[col]:[col2]]" //////// + ptgs = parse(fpb, tbl+"[[Name]:[Number]]"); + assertEquals(1, ptgs.length); + assertEquals("Table1[[col]:[col2]]", "Table!B2:C7", ptgs[0].toFormulaString()); + + wb.close(); + } }