]> source.dussan.org Git - poi.git/commitdiff
#60724 - Partial implementation for SUBTOTAL() 'ignore hidden rows' variations
authorGreg Woolsey <gwoolsey@apache.org>
Mon, 18 Mar 2019 19:09:11 +0000 (19:09 +0000)
committerGreg Woolsey <gwoolsey@apache.org>
Mon, 18 Mar 2019 19:09:11 +0000 (19:09 +0000)
The function still doesn't deal with auto-filtering, but it now handles variations that should skip hidden rows.

Taught the evaluation framework to know about hidden rows similar to what was already there for skipping subtotals within subtotal ranges.

Added unit test cases.

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1855789 13f79535-47bb-0310-9956-ffa450edef68

17 files changed:
src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java
src/java/org/apache/poi/ss/formula/EvaluationSheet.java
src/java/org/apache/poi/ss/formula/LazyAreaEval.java
src/java/org/apache/poi/ss/formula/LazyRefEval.java
src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java
src/java/org/apache/poi/ss/formula/TwoDEval.java
src/java/org/apache/poi/ss/formula/eval/AreaEvalBase.java
src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationSheet.java
src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java
src/java/org/apache/poi/ss/formula/functions/Count.java
src/java/org/apache/poi/ss/formula/functions/Counta.java
src/java/org/apache/poi/ss/formula/functions/MultiOperandNumericFunction.java
src/java/org/apache/poi/ss/formula/functions/Subtotal.java
src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationSheet.java
src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java
src/testcases/org/apache/poi/ss/formula/functions/TestSubtotal.java
test-data/spreadsheet/SubtotalsNested.xls

index d2d6927c82b16e4c6a9cc129a43081c574dbbbc4..04eed2d30c88a079874c5ef488074f9e6a996e6e 100644 (file)
@@ -45,7 +45,17 @@ final class HSSFEvaluationSheet implements EvaluationSheet {
     public int getLastRowNum() {
         return _hs.getLastRowNum();
     }
-    
+
+    /* (non-Javadoc)
+     * @see org.apache.poi.ss.formula.EvaluationSheet#isRowHidden(int)
+     * @since POI 4.0.2
+     */
+    public boolean isRowHidden(int rowIndex) {
+        HSSFRow row = _hs.getRow(rowIndex);
+        if (row == null) return false;
+        return row.getZeroHeight();
+    }
+
     @Override
     public EvaluationCell getCell(int rowIndex, int columnIndex) {
         HSSFRow row = _hs.getRow(rowIndex);
index 82f36a971e9b917267804c1d9d7d01946a089c62..27606ba6949b76b9bb69a05d8b5c674d318f2b8a 100644 (file)
@@ -48,4 +48,12 @@ public interface EvaluationSheet {
      * @since POI 4.0.0
      */
     public int getLastRowNum();
+    
+    /**
+     * Used by SUBTOTAL and similar functions that have options to ignore hidden rows
+     * @param rowIndex
+     * @return true if the row is hidden, false if not
+     * @since POI 4.0.2
+     */
+    public boolean isRowHidden(int rowIndex);
 }
index 4bfbf41d30348e05cac28cb0a41a79abe65191e2..f10cfe6de961b27ec7128c427dfddd140f48bbfc 100644 (file)
@@ -94,4 +94,14 @@ final class LazyAreaEval extends AreaEvalBase {
         SheetRefEvaluator _sre = _evaluator.getSheetEvaluator(_evaluator.getFirstSheetIndex());
         return _sre.isSubTotal(getFirstRow() + rowIndex, getFirstColumn() + columnIndex);
     }
+    
+    /**
+     * @return whether the row at rowIndex is hidden
+     * @see org.apache.poi.ss.formula.eval.AreaEvalBase#isRowHidden(int)
+     */
+    public boolean isRowHidden(int rowIndex) {
+        // delegate the query to the sheet evaluator which has access to internal ptgs
+        SheetRefEvaluator _sre = _evaluator.getSheetEvaluator(_evaluator.getFirstSheetIndex());
+        return _sre.isRowHidden(getFirstRow() + rowIndex);
+    }
 }
index c71b0e97792a4ad6a644c3d692da79d1e07852fa..4456c23a625d51368941b7b01f300a0d7a9b06b5 100644 (file)
@@ -47,10 +47,22 @@ public final class LazyRefEval extends RefEvalBase {
                return new LazyAreaEval(area, _evaluator);
        }
 
+       /**
+        * @return true if the cell is a subtotal
+        */
        public boolean isSubTotal() {
                SheetRefEvaluator sheetEvaluator = _evaluator.getSheetEvaluator(getFirstSheetIndex());
                return sheetEvaluator.isSubTotal(getRow(), getColumn());
        }
+    
+    /**
+     * @return whether the row at rowIndex is hidden
+     */
+    public boolean isRowHidden() {
+        // delegate the query to the sheet evaluator which has access to internal ptgs
+        SheetRefEvaluator _sre = _evaluator.getSheetEvaluator(_evaluator.getFirstSheetIndex());
+        return _sre.isRowHidden(getRow());
+    }
 
        public String toString() {
                CellReference cr = new CellReference(getRow(), getColumn());
index c9b408fa51f1d4b5dba9f2bbdf5fe18e08fc309c..2324eda75b62b2f85905fdb38c6bf97ac27f7962 100644 (file)
@@ -56,6 +56,8 @@ final class SheetRefEvaluator {
        }
 
     /**
+     * @param rowIndex 
+     * @param columnIndex 
      * @return  whether cell at rowIndex and columnIndex is a subtotal
      * @see org.apache.poi.ss.formula.functions.Subtotal
      */
@@ -77,4 +79,14 @@ final class SheetRefEvaluator {
         return subtotal;
     }
 
+    /**
+     * Used by functions that calculate differently depending on row visibility, like some
+     * variations of SUBTOTAL()
+     * @see org.apache.poi.ss.formula.functions.Subtotal
+     * @param rowIndex
+     * @return true if the row is hidden
+     */
+    public boolean isRowHidden(int rowIndex) {
+        return getSheet().isRowHidden(rowIndex);
+    }
 }
index c52d0f3eff46fd64cebfd9216a2b2ad3cd6434bb..f3c8c974f566a69571db8b8c67c662478ef51e18 100644 (file)
@@ -19,6 +19,7 @@ package org.apache.poi.ss.formula;
 
 import org.apache.poi.ss.formula.eval.AreaEval;
 import org.apache.poi.ss.formula.eval.ValueEval;
+import org.apache.poi.ss.formula.functions.Subtotal;
 
 /**
  * Common interface of {@link AreaEval} and {@link org.apache.poi.ss.formula.eval.AreaEvalBase},
@@ -64,5 +65,13 @@ public interface TwoDEval extends ValueEval {
      * @return true if the  cell at row and col is a subtotal
      */
     boolean isSubTotal(int rowIndex, int columnIndex);
+    
+    /**
+     *
+     * @param rowIndex
+     * @return true if the row is hidden
+     * @see Subtotal
+     */
+    boolean isRowHidden(int rowIndex);
 
 }
index edb8af7a1db9534a76588f75c798b93d5b9c67e6..96633318d9128867780a1f69aa55bbf8b02685ff 100644 (file)
@@ -146,4 +146,11 @@ public abstract class AreaEvalBase implements AreaEval {
         return false;
     }
 
+    /**
+     * @return false by default, meaning all rows are calculated
+     * @see org.apache.poi.ss.formula.TwoDEval#isRowHidden(int)
+     */
+    public boolean isRowHidden(int rowIndex) {
+        return false;
+    }
 }
index a223c81dfb9b0782557d67082276329b456ed143..354531de150ce280d4965cf4b7175768cd5689e2 100644 (file)
@@ -62,6 +62,14 @@ final class ForkedEvaluationSheet implements EvaluationSheet {
         return _masterSheet.getLastRowNum();
     }
     
+    /* (non-Javadoc)
+     * @see org.apache.poi.ss.formula.EvaluationSheet#isRowHidden(int)
+     * @since POI 4.0.2
+     */
+    public boolean isRowHidden(int rowIndex) {
+        return _masterSheet.isRowHidden(rowIndex);
+    }
+    
     @Override
     public EvaluationCell getCell(int rowIndex, int columnIndex) {
         RowColKey key = new RowColKey(rowIndex, columnIndex);
index 3d8eb4f83d5efb6c6c2ec9030241eeef60c83133..5eb90c3417b7fd84392309ff11e2492ce1953e5e 100644 (file)
@@ -162,7 +162,7 @@ public abstract class AggregateFunction extends MultiOperandNumericFunction {
      * @param   func  the function to wrap
      * @return  wrapped instance. The actual math is delegated to the argument function.
      */
-    /*package*/ static Function subtotalInstance(Function func) {
+    /*package*/ static Function subtotalInstance(Function func, boolean countHiddenRows) {
         final AggregateFunction arg = (AggregateFunction)func;
         return new AggregateFunction() {
             @Override
@@ -178,6 +178,9 @@ public abstract class AggregateFunction extends MultiOperandNumericFunction {
                 return false;
             }
 
+            public boolean isHiddenRowCounted() {
+                return countHiddenRows;
+            }
         };
     }
 
index b8de432edc8002a7f5bc7631261a20dc951e2a09..25c0799054704e06f85a54eba2e91a934c993120 100644 (file)
@@ -86,6 +86,9 @@ public final class Count implements Function {
                }
        };
 
+    /**
+     * matches hidden rows but not subtotals
+     */
     private static final I_MatchPredicate subtotalPredicate = new I_MatchAreaPredicate() {
         public boolean matches(ValueEval valueEval) {
             return defaultPredicate.matches(valueEval);
@@ -99,16 +102,34 @@ public final class Count implements Function {
         }
     };
 
+    /**
+     * matches nither hidden rows or subtotals
+     */
+    private static final I_MatchPredicate subtotalVisibleOnlyPredicate = new I_MatchAreaPredicate() {
+        public boolean matches(ValueEval valueEval) {
+            return defaultPredicate.matches(valueEval);
+        }
+        
+        /**
+         * don't count cells that are subtotals
+         */
+        public boolean matches(TwoDEval areEval, int rowIndex, int columnIndex) {
+            return !areEval.isSubTotal(rowIndex, columnIndex) && !areEval.isRowHidden(rowIndex);
+        }
+    };
+    
     /**
      *  Create an instance of Count to use in {@link Subtotal}
      * <p>
      *     If there are other subtotals within argument refs (or nested subtotals),
      *     these nested subtotals are ignored to avoid double counting.
      * </p>
+     * @param includeHiddenRows true to include hidden rows in the aggregate, false to skip them
+     * @return function
      *
      *  @see Subtotal
      */
-    public static Count subtotalInstance() {
-        return new Count(subtotalPredicate );
+    public static Count subtotalInstance(boolean includeHiddenRows) {
+        return new Count(includeHiddenRows ? subtotalPredicate : subtotalVisibleOnlyPredicate);
     }
 }
index 5a106be0a90ed5435b72a6e482c5423397d8c67c..c725f012a25bf337d103a7070f37643f5d48d0b6 100644 (file)
@@ -80,7 +80,8 @@ public final class Counta implements Function {
                        return true;
                }
        };
-    private static final I_MatchPredicate subtotalPredicate = new I_MatchAreaPredicate() {
+
+       private static final I_MatchPredicate subtotalPredicate = new I_MatchAreaPredicate() {
         public boolean matches(ValueEval valueEval) {
             return defaultPredicate.matches(valueEval);
         }
@@ -93,8 +94,21 @@ public final class Counta implements Function {
         }
     };
 
-    public static Counta subtotalInstance() {
-        return new Counta(subtotalPredicate);
+    private static final I_MatchPredicate subtotalVisibleOnlyPredicate = new I_MatchAreaPredicate() {
+        public boolean matches(ValueEval valueEval) {
+            return defaultPredicate.matches(valueEval);
+        }
+        
+        /**
+         * don't count cells in rows that are hidden or subtotal cells
+         */
+        public boolean matches(TwoDEval areEval, int rowIndex, int columnIndex) {
+            return !areEval.isSubTotal(rowIndex, columnIndex) && ! areEval.isRowHidden(rowIndex);
+        }
+    };
+    
+    public static Counta subtotalInstance(boolean includeHiddenRows) {
+        return new Counta(includeHiddenRows ? subtotalPredicate : subtotalVisibleOnlyPredicate);
     }
 
 }
index 7ec5a5fcf06e36be819b49b634a9c57d63785cad..89e103ba435758b59497c2de0e92c53096cb960f 100644 (file)
@@ -152,6 +152,14 @@ public abstract class MultiOperandNumericFunction implements Function {
         return true;
     }
 
+    /**
+     * @return true if values in hidden rows are counted
+     * @see Subtotal
+     */
+    public boolean isHiddenRowCounted() {
+        return true;
+    }
+    
     /**
      * Collects values from a single argument
      */
@@ -165,6 +173,7 @@ public abstract class MultiOperandNumericFunction implements Function {
                     for (int rcIx = 0; rcIx < width; rcIx++) {
                         ValueEval ve = ae.getValue(sIx, rrIx, rcIx);
                         if (!isSubtotalCounted() && ae.isSubTotal(rrIx, rcIx)) continue;
+                        if (!isHiddenRowCounted() && ae.isRowHidden(rrIx)) continue;
                         collectValue(ve, true, temp);
                     }
                 }
index 9b5bff897f7d97b33316bfaa1f6ddb4e849727af..8727353ae2716e746755d49e4981fd9e3d1dad62 100644 (file)
@@ -56,7 +56,17 @@ import java.util.List;
  *      <tr align='center'><td>9</td><td>SUM</td></tr>
  *      <tr align='center'><td>10</td><td>VAR *</td></tr>
  *      <tr align='center'><td>11</td><td>VARP *</td></tr>
- *      <tr align='center'><td>101-111</td><td>*</td></tr>
+ *      <tr align='center'><td>101</td><td>AVERAGE</td></tr>
+ *      <tr align='center'><td>102</td><td>COUNT</td></tr>
+ *      <tr align='center'><td>103</td><td>COUNTA</td></tr>
+ *      <tr align='center'><td>104</td><td>MAX</td></tr>
+ *      <tr align='center'><td>105</td><td>MIN</td></tr>
+ *      <tr align='center'><td>106</td><td>PRODUCT</td></tr>
+ *      <tr align='center'><td>107</td><td>STDEV</td></tr>
+ *      <tr align='center'><td>108</td><td>STDEVP *</td></tr>
+ *      <tr align='center'><td>109</td><td>SUM</td></tr>
+ *      <tr align='center'><td>110</td><td>VAR *</td></tr>
+ *      <tr align='center'><td>111</td><td>VARP *</td></tr>
  *  </table><br>
  * * Not implemented in POI yet. Functions 101-111 are the same as functions 1-11 but with
  * the option 'ignore hidden values'.
@@ -68,20 +78,28 @@ public class Subtotal implements Function {
 
        private static Function findFunction(int functionCode) throws EvaluationException {
         switch (functionCode) {
-                       case 1: return subtotalInstance(AggregateFunction.AVERAGE);
-                       case 2: return Count.subtotalInstance();
-                       case 3: return Counta.subtotalInstance();
-                       case 4: return subtotalInstance(AggregateFunction.MAX);
-                       case 5: return subtotalInstance(AggregateFunction.MIN);
-                       case 6: return subtotalInstance(AggregateFunction.PRODUCT);
-                       case 7: return subtotalInstance(AggregateFunction.STDEV);
+                       case 1: return subtotalInstance(AggregateFunction.AVERAGE, true);
+                       case 2: return Count.subtotalInstance(true);
+                       case 3: return Counta.subtotalInstance(true);
+                       case 4: return subtotalInstance(AggregateFunction.MAX, true);
+                       case 5: return subtotalInstance(AggregateFunction.MIN, true);
+                       case 6: return subtotalInstance(AggregateFunction.PRODUCT, true);
+                       case 7: return subtotalInstance(AggregateFunction.STDEV, true);
                        case 8: throw new NotImplementedFunctionException("STDEVP");
-                       case 9: return subtotalInstance(AggregateFunction.SUM);
+                       case 9: return subtotalInstance(AggregateFunction.SUM, true);
                        case 10: throw new NotImplementedFunctionException("VAR");
                        case 11: throw new NotImplementedFunctionException("VARP");
-               }
-               if (functionCode > 100 && functionCode < 112) {
-                       throw new NotImplementedException("SUBTOTAL - with 'exclude hidden values' option");
+                       case 101: return subtotalInstance(AggregateFunction.AVERAGE, false);
+                       case 102: return Count.subtotalInstance(false);
+                       case 103: return Counta.subtotalInstance(false);
+                       case 104: return subtotalInstance(AggregateFunction.MAX, false);
+                       case 105: return subtotalInstance(AggregateFunction.MIN, false);
+                       case 106: return subtotalInstance(AggregateFunction.PRODUCT, false);
+                       case 107: return subtotalInstance(AggregateFunction.STDEV, false);
+                       case 108: throw new NotImplementedFunctionException("STDEVP SUBTOTAL with 'exclude hidden values' option");
+                       case 109: return subtotalInstance(AggregateFunction.SUM, false);
+                       case 110: throw new NotImplementedFunctionException("VAR SUBTOTAL with 'exclude hidden values' option");
+                       case 111: throw new NotImplementedFunctionException("VARP SUBTOTAL with 'exclude hidden values' option");
                }
                throw EvaluationException.invalidValue();
        }
@@ -93,9 +111,10 @@ public class Subtotal implements Function {
                }
 
                final Function innerFunc;
+               int functionCode = 0;
                try {
                        ValueEval ve = OperandResolver.getSingleValue(args[0], srcRowIndex, srcColumnIndex);
-                       int functionCode = OperandResolver.coerceValueToInt(ve);
+            functionCode = OperandResolver.coerceValueToInt(ve);
                        innerFunc = findFunction(functionCode);
                } catch (EvaluationException e) {
                        return e.getErrorEval();
@@ -116,6 +135,9 @@ public class Subtotal implements Function {
                                if(lazyRefEval.isSubTotal()) {
                                        it.remove();
                                }
+                               if (functionCode > 100 && lazyRefEval.isRowHidden()) {
+                                   it.remove();
+                               }
                        }
                }
 
index bb1d1f1698f6678e4b474b38014311b24f1f9aac..21e2dab8be64ecbad53312b74b282c95cb8ccb03 100644 (file)
@@ -45,6 +45,16 @@ final class SXSSFEvaluationSheet implements EvaluationSheet {
         return _xs.getLastRowNum();
     }
     
+    /* (non-Javadoc)
+     * @see org.apache.poi.ss.formula.EvaluationSheet#isRowHidden(int)
+     * @since POI 4.0.2
+     */
+    public boolean isRowHidden(int rowIndex) {
+        SXSSFRow row = _xs.getRow(rowIndex);
+        if (row == null) return false;
+        return row.getZeroHeight();
+    }
+
     @Override
     public EvaluationCell getCell(int rowIndex, int columnIndex) {
         SXSSFRow row = _xs.getRow(rowIndex);
index 74ac23215a15c69f35876bd009edf930062d285a..3d71f08825461ed3a8fabbbcf9be04c4c6dc3a60 100644 (file)
@@ -52,6 +52,16 @@ final class XSSFEvaluationSheet implements EvaluationSheet {
         return _xs.getLastRowNum();
     }
     
+    /* (non-Javadoc)
+     * @see org.apache.poi.ss.formula.EvaluationSheet#isRowHidden(int)
+     * @since POI 4.0.2
+     */
+    public boolean isRowHidden(int rowIndex) {
+        final XSSFRow row = _xs.getRow(rowIndex);
+        if (row == null) return false;
+        return row.getZeroHeight();
+    }
+    
     /* (non-JavaDoc), inherit JavaDoc from EvaluationWorkbook
      * @since POI 3.15 beta 3
      */
index 41ca4b8e980f1da8c7aa44710f0ea4d69a42c6a9..aa16314379fb5e2492c2c2a03fe60d8c5897265d 100644 (file)
@@ -376,6 +376,15 @@ public final class TestSubtotal {
         confirmExpectedResult(evaluator, "SUBTOTAL(COUNT;B2:B8,C2:C8)", cellC2, 3.0);
         confirmExpectedResult(evaluator, "SUBTOTAL(COUNTA;B2:B8,C2:C8)", cellC3, 5.0);
     
+        // test same functions ignoring hidden rows over a copy of the same data
+        cellC1 = sheet.getRow(11).getCell(3);
+        cellC2 = sheet.getRow(12).getCell(3);
+        cellC3 = sheet.getRow(13).getCell(3);
+        confirmExpectedResult(evaluator, "SUBTOTAL(SUM NO HIDDEN;B22:B28;C22:C28)", cellC1, 17.0);
+        confirmExpectedResult(evaluator, "SUBTOTAL(COUNT NO HIDDEN;B22:B28,C22:C28)", cellC2, 2.0);
+        confirmExpectedResult(evaluator, "SUBTOTAL(COUNTA NO HIDDEN;B22:B28,C22:C28)", cellC3, 4.0);
+        
+        
         workbook.close();
     }
 
@@ -393,7 +402,6 @@ public final class TestSubtotal {
             { "SUBTOTAL(8,B2:B3)", NotImplementedException.class.getName() },
             { "SUBTOTAL(10,B2:B3)", NotImplementedException.class.getName() },
             { "SUBTOTAL(11,B2:B3)", NotImplementedException.class.getName() },
-            { "SUBTOTAL(107,B2:B3)", NotImplementedException.class.getName() },
             { "SUBTOTAL(0,B2:B3)", null },
             { "SUBTOTAL(9)", FormulaParseException.class.getName() },
             { "SUBTOTAL()", FormulaParseException.class.getName() },
@@ -404,7 +412,7 @@ public final class TestSubtotal {
             try {
                 a3.setCellFormula(f[0]);
                 fe.evaluateAll();
-                assertEquals(FormulaError.VALUE.getCode(), a3.getErrorCellValue());
+                assertEquals(f[0], FormulaError.VALUE.getCode(), a3.getErrorCellValue());
             } catch (Exception e) {
                 actualEx = e;
             }
index a19571d84918d4fcc844f42be9de72f5b1fda966..830aa5c34c82ac4ce7d54b5b146c2be15b8f4486 100644 (file)
Binary files a/test-data/spreadsheet/SubtotalsNested.xls and b/test-data/spreadsheet/SubtotalsNested.xls differ