]> source.dussan.org Git - poi.git/commitdiff
Optimised the FormulaEvaluator to take cell dependencies into account
authorJosh Micich <josh@apache.org>
Tue, 23 Sep 2008 00:40:22 +0000 (00:40 +0000)
committerJosh Micich <josh@apache.org>
Tue, 23 Sep 2008 00:40:22 +0000 (00:40 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@698047 13f79535-47bb-0310-9956-ffa450edef68

23 files changed:
src/documentation/content/xdocs/changes.xml
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java
src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java
src/java/org/apache/poi/ss/formula/CellCacheEntry.java [new file with mode: 0644]
src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java
src/java/org/apache/poi/ss/formula/CellEvaluator.java [deleted file]
src/java/org/apache/poi/ss/formula/CellLocation.java [new file with mode: 0644]
src/java/org/apache/poi/ss/formula/EvaluationCache.java
src/java/org/apache/poi/ss/formula/EvaluationTracker.java
src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java
src/java/org/apache/poi/ss/formula/IEvaluationListener.java [new file with mode: 0644]
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 [new file with mode: 0644]
src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java
src/testcases/org/apache/poi/hssf/HSSFTests.java
src/testcases/org/apache/poi/hssf/record/formula/eval/TestCircularReferences.java
src/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorBugs.java
src/testcases/org/apache/poi/hssf/usermodel/TestHSSFFormulaEvaluator.java
src/testcases/org/apache/poi/ss/formula/EvaluationListener.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/formula/WorkbookEvaluatorTestHelper.java [new file with mode: 0644]

index 3afe4e077b2c09a25a796063bf07027bfe30dd89..c054f65be1cd6788634d0354a9bc6b6025c15ed9 100644 (file)
@@ -37,6 +37,7 @@
 
                <!-- Don't forget to update status.xml too! -->
         <release version="3.2-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="add">Optimised the FormulaEvaluator to take cell dependencies into account</action>
            <action dev="POI-DEVELOPERS" type="add">16936 - Initial support for whole-row cell styling</action>
            <action dev="POI-DEVELOPERS" type="add">Update hssf.extractor.ExcelExtractor to optionally output blank cells too</action>
            <action dev="POI-DEVELOPERS" type="add">Include the sheet name in the output of examples.XLS2CSVmra</action>
index d745ce41eb605ec5fbae4ec6dec8ae991de95ff0..b416d344faf3e16abeeba41eca43f2e347e59d1f 100644 (file)
@@ -34,6 +34,7 @@
        <!-- Don't forget to update changes.xml too! -->
     <changes>
         <release version="3.2-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="add">Optimised the FormulaEvaluator to take cell dependencies into account</action>
            <action dev="POI-DEVELOPERS" type="add">16936 - Initial support for whole-row cell styling</action>
            <action dev="POI-DEVELOPERS" type="add">Update hssf.extractor.ExcelExtractor to optionally output blank cells too</action>
            <action dev="POI-DEVELOPERS" type="add">Include the sheet name in the output of examples.XLS2CSVmra</action>
index 16ef3b2a9f898c575bc24e2def684a1ca0c3ca73..a9c16b090d4c41586fec8d3533ad6af284369047 100644 (file)
@@ -71,11 +71,9 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
        public HSSFSheet getSheet(int sheetIndex) {\r
                return _uBook.getSheetAt(sheetIndex);\r
        }\r
-\r
-       public HSSFSheet getSheetByExternSheetIndex(int externSheetIndex) {\r
-               int sheetIndex = _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);\r
-               return _uBook.getSheetAt(sheetIndex);\r
-       }\r
+       public int convertFromExternSheetIndex(int externSheetIndex) {\r
+               return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);\r
+}\r
 \r
        public HSSFWorkbook getWorkbook() {\r
                return _uBook;\r
index 1e6aa163c065854b8a6c0c9de4460870ed65e263..ab75f8ba88be195b3ac4526debad6b325cbeaad5 100644 (file)
@@ -19,6 +19,7 @@ package org.apache.poi.hssf.usermodel;
 \r
 import java.util.Iterator;\r
 \r
+import org.apache.poi.hssf.record.formula.eval.BlankEval;\r
 import org.apache.poi.hssf.record.formula.eval.BoolEval;\r
 import org.apache.poi.hssf.record.formula.eval.ErrorEval;\r
 import org.apache.poi.hssf.record.formula.eval.NumberEval;\r
@@ -50,14 +51,7 @@ public class HSSFFormulaEvaluator {
                }\r
        }\r
        public HSSFFormulaEvaluator(HSSFWorkbook workbook) {\r
-               _bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook));\r
-       }\r
-\r
-       /**\r
-        * TODO for debug/test use\r
-        */\r
-       /* package */ int getEvaluationCount() {\r
-               return _bookEvaluator.getEvaluationCount();\r
+               _bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook));\r
        }\r
 \r
        /**\r
@@ -81,12 +75,23 @@ public class HSSFFormulaEvaluator {
                _bookEvaluator.clearAllCachedResultValues();\r
        }\r
        /**\r
+        * Sets the cached value for a plain (non-formula) cell.\r
         * Should be called whenever there are changes to individual input cells in the evaluated workbook.\r
         * Failure to call this method after changing cell values will cause incorrect behaviour\r
         * of the evaluate~ methods of this class\r
+        * @param never <code>null</code>. Use {@link BlankEval#INSTANCE} when the cell is being \r
+        * cleared. Otherwise an instance of {@link NumberEval}, {@link StringEval}, {@link BoolEval}\r
+        * or {@link ErrorEval} to represent a plain cell value.\r
         */\r
-       public void clearCachedResultValue(HSSFSheet sheet, int rowIndex, int columnIndex) {\r
-               _bookEvaluator.clearCachedResultValue(sheet, rowIndex, columnIndex);\r
+       public void setCachedPlainValue(HSSFSheet sheet, int rowIndex, int columnIndex, ValueEval value) {\r
+               _bookEvaluator.setCachedPlainValue(sheet, rowIndex, columnIndex, value);\r
+       }\r
+       /**\r
+        * Should be called to tell the cell value cache that the specified cell has just become a\r
+        * formula cell, or the formula text has changed \r
+        */\r
+       public void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex) {\r
+               _bookEvaluator.notifySetFormula(sheet, rowIndex, columnIndex);\r
        }\r
 \r
        /**\r
@@ -95,7 +100,9 @@ public class HSSFFormulaEvaluator {
         * the cell and also its cell type. This method should be preferred over\r
         * evaluateInCell() when the call should not modify the contents of the\r
         * original cell.\r
-        * @param cell\r
+        * \r
+        * @param cell may be <code>null</code> signifying that the cell is not present (or blank)\r
+        * @return <code>null</code> if the supplied cell is <code>null</code> or blank\r
         */\r
        public CellValue evaluate(HSSFCell cell) {\r
                if (cell == null) {\r
@@ -113,6 +120,8 @@ public class HSSFFormulaEvaluator {
                                return new CellValue(cell.getNumericCellValue());\r
                        case HSSFCell.CELL_TYPE_STRING:\r
                                return new CellValue(cell.getRichStringCellValue().getString());\r
+                       case HSSFCell.CELL_TYPE_BLANK:\r
+                               return null;\r
                }\r
                throw new IllegalStateException("Bad cell type (" + cell.getCellType() + ")");\r
        }\r
diff --git a/src/java/org/apache/poi/ss/formula/CellCacheEntry.java b/src/java/org/apache/poi/ss/formula/CellCacheEntry.java
new file mode 100644 (file)
index 0000000..861874c
--- /dev/null
@@ -0,0 +1,114 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.formula;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.poi.hssf.record.formula.eval.BoolEval;
+import org.apache.poi.hssf.record.formula.eval.ErrorEval;
+import org.apache.poi.hssf.record.formula.eval.NumberEval;
+import org.apache.poi.hssf.record.formula.eval.StringEval;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+
+/**
+ * Stores the parameters that identify the evaluation of one cell.<br/>
+ */
+final class CellCacheEntry {
+       private ValueEval _value;
+       private final Set _consumingCells;
+       private CellLocation[] _usedCells;
+       private boolean _isPlainValueCell;
+       
+       public CellCacheEntry() {
+               _consumingCells = new HashSet();
+       }
+       public boolean updatePlainValue(ValueEval value) {
+               boolean wasChanged = false;
+               if (!_isPlainValueCell) {
+                       wasChanged = true;
+               }
+               if (!areValuesEqual(_value, value)) {
+                       wasChanged = true;
+               }
+               _isPlainValueCell = true;
+               _value = value;
+               _usedCells = null;
+               return wasChanged;
+       }
+       private boolean areValuesEqual(ValueEval a, ValueEval b) {
+               if (a == null) {
+                       return false;
+               }
+               Class cls = a.getClass();
+               if (cls != b.getClass()) {
+                       // value type is changing
+                       return false;
+               }
+               if (cls == NumberEval.class) {
+                       return ((NumberEval)a).getNumberValue() == ((NumberEval)b).getNumberValue();
+               }
+               if (cls == StringEval.class) {
+                       return ((StringEval)a).getStringValue().equals(((StringEval)b).getStringValue());
+               }
+               if (cls == BoolEval.class) {
+                       return ((BoolEval)a).getBooleanValue() == ((BoolEval)b).getBooleanValue();
+               }
+               if (cls == ErrorEval.class) {
+                       return ((ErrorEval)a).getErrorCode() == ((ErrorEval)b).getErrorCode();
+               }
+               throw new IllegalStateException("Unexpected value class (" + cls.getName() + ")");
+       }
+       public void setFormulaResult(ValueEval value, CellLocation[] usedCells) {
+               _isPlainValueCell = false;
+               _value = value;
+               _usedCells = usedCells;
+       }
+       public void addConsumingCell(CellLocation cellLoc) {
+               _consumingCells.add(cellLoc);
+               
+       }
+       public CellLocation[] getConsumingCells() {
+               int nItems = _consumingCells.size();
+               if (nItems < 1) {
+                       return CellLocation.EMPTY_ARRAY;
+               }
+               CellLocation[] result = new CellLocation[nItems];
+               _consumingCells.toArray(result);
+               return result;
+       }
+       public CellLocation[] getUsedCells() {
+               return _usedCells;
+       }
+       public void clearConsumingCell(CellLocation fc) {
+               if(!_consumingCells.remove(fc)) {
+                       throw new IllegalStateException("Cell '" + fc.formatAsString() + "' does not use this cell");
+               }
+               
+       }
+       public ValueEval getValue() {
+               ValueEval result = _value;
+               if (result == null) {
+                       if (_isPlainValueCell) {
+                               throw new IllegalStateException("Plain value cell should always have a value");
+                               
+                       }
+               }
+               return result;
+       }
+}
\ No newline at end of file
index b6f542a082c5be229a980054455053eaf90f8086..1d438527ae69c4ee103a270446a407b963e88bac 100644 (file)
 \r
 package org.apache.poi.ss.formula;\r
 \r
+import java.util.HashSet;\r
+import java.util.Set;\r
+\r
 /**\r
- * Stores the parameters that identify the evaluation of one cell.<br/>\r
+ * Stores details about the current evaluation of a cell.<br/>\r
  */\r
 final class CellEvaluationFrame {\r
 \r
-       private final int _sheetIndex;\r
-       private final int _srcRowNum;\r
-       private final int _srcColNum;\r
-       private final int _hashCode;\r
-\r
-       public CellEvaluationFrame(int sheetIndex, int srcRowNum, int srcColNum) {\r
-               if (sheetIndex < 0) {\r
-                       throw new IllegalArgumentException("sheetIndex must not be negative");\r
-               }\r
-               _sheetIndex = sheetIndex;\r
-               _srcRowNum = srcRowNum;\r
-               _srcColNum = srcColNum;\r
-               _hashCode = sheetIndex + 17 * (srcRowNum + 17 * srcColNum);\r
-       }\r
+       private final CellLocation _cellLocation;\r
+       private final Set _usedCells;\r
 \r
-       public boolean equals(Object obj) {\r
-               CellEvaluationFrame other = (CellEvaluationFrame) obj;\r
-               if (_sheetIndex != other._sheetIndex) {\r
-                       return false;\r
-               }\r
-               if (_srcRowNum != other._srcRowNum) {\r
-                       return false;\r
-               }\r
-               if (_srcColNum != other._srcColNum) {\r
-                       return false;\r
-               }\r
-               return true;\r
-       }\r
-       public int hashCode() {\r
-               return _hashCode;\r
+       public CellEvaluationFrame(CellLocation cellLoc) {\r
+               _cellLocation = cellLoc;\r
+               _usedCells = new HashSet();\r
        }\r
-\r
-       /**\r
-        * @return human readable string for debug purposes\r
-        */\r
-       public String formatAsString() {\r
-               return "R=" + _srcRowNum + " C=" + _srcColNum + " ShIx=" + _sheetIndex;\r
+       public CellLocation getCoordinates() {\r
+               return _cellLocation;\r
        }\r
 \r
        public String toString() {\r
                StringBuffer sb = new StringBuffer(64);\r
                sb.append(getClass().getName()).append(" [");\r
-               sb.append(formatAsString());\r
+               sb.append(_cellLocation.formatAsString());\r
                sb.append("]");\r
                return sb.toString();\r
        }\r
+       public void addUsedCell(CellLocation coordinates) {\r
+               _usedCells.add(coordinates);\r
+       }\r
+       /**\r
+        * @return never <code>null</code>, (possibly empty) array of all cells directly used while \r
+        * evaluating the formula of this frame.  For non-formula cells this will always be an empty\r
+        * array;\r
+        */\r
+       public CellLocation[] getUsedCells() {\r
+               int nItems = _usedCells.size();\r
+               if (nItems < 1) {\r
+                       return CellLocation.EMPTY_ARRAY;\r
+               }\r
+               CellLocation[] result = new CellLocation[nItems];\r
+               _usedCells.toArray(result);\r
+               return result;\r
+       }\r
 }
\ No newline at end of file
diff --git a/src/java/org/apache/poi/ss/formula/CellEvaluator.java b/src/java/org/apache/poi/ss/formula/CellEvaluator.java
deleted file mode 100644 (file)
index 22846c2..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.apache.poi.ss.formula;\r
-\r
-import org.apache.poi.hssf.record.formula.eval.BlankEval;\r
-import org.apache.poi.hssf.record.formula.eval.BoolEval;\r
-import org.apache.poi.hssf.record.formula.eval.ErrorEval;\r
-import org.apache.poi.hssf.record.formula.eval.NumberEval;\r
-import org.apache.poi.hssf.record.formula.eval.StringEval;\r
-import org.apache.poi.hssf.record.formula.eval.ValueEval;\r
-import org.apache.poi.hssf.usermodel.HSSFCell;\r
-import org.apache.poi.hssf.usermodel.HSSFSheet;\r
-\r
-final class CellEvaluator {\r
-\r
-       private final WorkbookEvaluator _bookEvaluator;\r
-       private final EvaluationTracker _tracker;\r
-\r
-       public CellEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker) {\r
-               _bookEvaluator = bookEvaluator;\r
-               _tracker = tracker;\r
-       }\r
-\r
-          /**\r
-     * Given a cell, find its type and from that create an appropriate ValueEval\r
-     * impl instance and return that. Since the cell could be an external\r
-     * reference, we need the sheet that this belongs to.\r
-     * Non existent cells are treated as empty.\r
-     */\r
-       public ValueEval getEvalForCell(HSSFCell cell) {\r
-\r
-        if (cell == null) {\r
-            return BlankEval.INSTANCE;\r
-        }\r
-        switch (cell.getCellType()) {\r
-            case HSSFCell.CELL_TYPE_NUMERIC:\r
-                return new NumberEval(cell.getNumericCellValue());\r
-            case HSSFCell.CELL_TYPE_STRING:\r
-                return new StringEval(cell.getRichStringCellValue().getString());\r
-            case HSSFCell.CELL_TYPE_FORMULA:\r
-                return _bookEvaluator.internalEvaluate(cell, _tracker);\r
-            case HSSFCell.CELL_TYPE_BOOLEAN:\r
-                return BoolEval.valueOf(cell.getBooleanCellValue());\r
-            case HSSFCell.CELL_TYPE_BLANK:\r
-                return BlankEval.INSTANCE;\r
-            case HSSFCell.CELL_TYPE_ERROR:\r
-                return ErrorEval.valueOf(cell.getErrorCellValue());\r
-        }\r
-        throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")");\r
-    }\r
-\r
-       public String getSheetName(HSSFSheet sheet) {\r
-        return _bookEvaluator.getSheetName(sheet);\r
-       }\r
-\r
-}\r
diff --git a/src/java/org/apache/poi/ss/formula/CellLocation.java b/src/java/org/apache/poi/ss/formula/CellLocation.java
new file mode 100644 (file)
index 0000000..d6bb9b7
--- /dev/null
@@ -0,0 +1,81 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.formula;
+
+/**
+ * Stores the parameters that identify the evaluation of one cell.<br/>
+ */
+final class CellLocation {
+       public static final CellLocation[] EMPTY_ARRAY = { };
+       
+       private final int _sheetIndex;
+       private final int _rowIndex;
+       private final int _columnIndex;
+       private final int _hashCode;
+
+       public CellLocation(int sheetIndex, int rowIndex, int columnIndex) {
+               if (sheetIndex < 0) {
+                       throw new IllegalArgumentException("sheetIndex must not be negative");
+               }
+               _sheetIndex = sheetIndex;
+               _rowIndex = rowIndex;
+               _columnIndex = columnIndex;
+               _hashCode = sheetIndex + 17 * (rowIndex + 17 * columnIndex);
+       }
+       public int getSheetIndex() {
+               return _sheetIndex;
+       }
+       public int getRowIndex() {
+               return _rowIndex;
+       }
+       public int getColumnIndex() {
+               return _columnIndex;
+       }
+
+       public boolean equals(Object obj) {
+               CellLocation other = (CellLocation) obj;
+               if (getSheetIndex() != other.getSheetIndex()) {
+                       return false;
+               }
+               if (getRowIndex() != other.getRowIndex()) {
+                       return false;
+               }
+               if (getColumnIndex() != other.getColumnIndex()) {
+                       return false;
+               }
+               return true;
+       }
+       public int hashCode() {
+               return _hashCode;
+       }
+
+       /**
+        * @return human readable string for debug purposes
+        */
+       public String formatAsString() {
+               return  "ShIx=" + getSheetIndex() + " R=" + getRowIndex() + " C=" + getColumnIndex();
+       }
+
+       public String toString() {
+               StringBuffer sb = new StringBuffer(64);
+               sb.append(getClass().getName()).append(" [");
+               sb.append(formatAsString());
+               sb.append("]");
+               return sb.toString();
+       }
+}
\ No newline at end of file
index d78f4a7286ff8222822320d316e218a003a144f6..fdc933f6fa684ef0b0a5e66fe642d6c958b09cb5 100644 (file)
 
 package org.apache.poi.ss.formula;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
-import java.util.List;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.poi.hssf.record.formula.eval.ValueEval;
 import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
@@ -34,66 +38,176 @@ import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
  */
 final class EvaluationCache {
 
-       private static final CellEvaluationFrame[] EMPTY_CEF_ARRAY = { };
-       private final Map _valuesByKey;
-       private final Map _consumingCellsByDest;
+       private final Map _entriesByLocation;
+       private final Map _consumingCellsByUsedCells;
+       /** only used for testing. <code>null</code> otherwise */
+       private final IEvaluationListener _evaluationListener;
 
-       /* package */EvaluationCache() {
-               _valuesByKey = new HashMap();
-               _consumingCellsByDest = new HashMap();
+       /* package */EvaluationCache(IEvaluationListener evaluationListener) {
+               _evaluationListener = evaluationListener;
+               _entriesByLocation = new HashMap();
+               _consumingCellsByUsedCells = new HashMap();
        }
 
-       public ValueEval getValue(int sheetIndex, int srcRowNum, int srcColNum) {
-               return getValue(new CellEvaluationFrame(sheetIndex, srcRowNum, srcColNum));
+       /**
+        * @param cellLoc never <code>null</code>
+        * @return only ever <code>null</code> for formula cells that have had their cached value cleared
+        */
+       public ValueEval getValue(CellLocation cellLoc) {
+               return getEntry(cellLoc).getValue();
        }
 
-       /* package */ ValueEval getValue(CellEvaluationFrame key) {
-               return (ValueEval) _valuesByKey.get(key);
+       /**
+        * @param cellLoc
+        * @param usedCells never <code>null</code>, (possibly zero length) array of all cells actually 
+        * directly used when evaluating the formula
+        * @param isPlainValue pass <code>true</code> if cellLoc refers to a plain value (non-formula) 
+        * cell, <code>false</code> for a formula cell.
+        * @param value the value of a non-formula cell or the result of evaluating the cell formula
+        * Pass <code>null</code> to signify clearing the cached result of a formula cell) 
+        */
+       public void setValue(CellLocation cellLoc, boolean isPlainValue, 
+                       CellLocation[] usedCells, ValueEval value) {
+
+               CellCacheEntry existingEntry = (CellCacheEntry) _entriesByLocation.get(cellLoc);
+               if (existingEntry != null && existingEntry.getValue() != null) {
+                       if (isPlainValue) {
+                               // can set a plain cell cached value any time
+                       } else if (value == null) {
+                               // or clear the cached value of a formula cell at any time
+                       } else {
+                               // but a formula cached value would only be getting updated if the cache didn't have a value to start with
+                               throw new IllegalStateException("Already have cached value for this cell: "
+                                               + cellLoc.formatAsString());
+                       }
+               }
+               if (_evaluationListener == null) {
+                       // optimisation - don't bother sorting if there is no listener.
+               } else {
+                       // for testing
+                       // make order of callbacks to listener more deterministic
+                       Arrays.sort(usedCells, CellLocationComparator);
+               }
+               CellCacheEntry entry = getEntry(cellLoc);
+               CellLocation[] consumingFormulaCells = entry.getConsumingCells();
+               CellLocation[] prevUsedCells = entry.getUsedCells();
+               if (isPlainValue) {
+                       
+                       if(!entry.updatePlainValue(value)) {
+                               return;
+                       }
+               } else {
+                       entry.setFormulaResult(value, usedCells);
+                       for (int i = 0; i < usedCells.length; i++) {
+                               getEntry(usedCells[i]).addConsumingCell(cellLoc);
+                       }
+               }
+               // need to tell all cells that were previously used, but no longer are, 
+               // that they are not consumed by this cell any more
+               unlinkConsumingCells(prevUsedCells, usedCells, cellLoc);
+               
+               // clear all cells that directly or indirectly depend on this one
+               recurseClearCachedFormulaResults(consumingFormulaCells, 0);
        }
 
-       public void setValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value) {
-               setValue(new CellEvaluationFrame(sheetIndex, srcRowNum, srcColNum), value);
+       private void unlinkConsumingCells(CellLocation[] prevUsedCells, CellLocation[] usedCells,
+                       CellLocation cellLoc) {
+               if (prevUsedCells == null) {
+                       return;
+               }
+               int nPrevUsed = prevUsedCells.length;
+               if (nPrevUsed < 1) {
+                       return;
+               }
+               int nUsed = usedCells.length;
+               Set usedSet;
+               if (nUsed < 1) {
+                       usedSet = Collections.EMPTY_SET;
+               } else {
+                       usedSet = new HashSet(nUsed * 3 / 2);
+                       for (int i = 0; i < nUsed; i++) {
+                               usedSet.add(usedCells[i]);
+                       }
+               }
+               for (int i = 0; i < nPrevUsed; i++) {
+                       CellLocation prevUsed = prevUsedCells[i];
+                       if (!usedSet.contains(prevUsed)) {
+                               // previously was used by cellLoc, but not anymore
+                               getEntry(prevUsed).clearConsumingCell(cellLoc);
+                               if (_evaluationListener != null) {
+                                       //TODO _evaluationListener.onUnconsume(prevUsed.getSheetIndex(), etc)
+                               }
+                       }
+               }
+               
        }
 
-       /* package */ void setValue(CellEvaluationFrame key, ValueEval value) {
-               if (_valuesByKey.containsKey(key)) {
-                       throw new RuntimeException("Already have cached value for this cell");
+       /**
+        * Calls formulaCell.setFormulaResult(null, null) recursively all the way up the tree of 
+        * dependencies. Calls usedCell.clearConsumingCell(fc) for each child of a cell that is
+        * cleared along the way.
+        * @param formulaCells
+        */
+       private void recurseClearCachedFormulaResults(CellLocation[] formulaCells, int depth) {
+               int nextDepth = depth+1;
+               for (int i = 0; i < formulaCells.length; i++) {
+                       CellLocation fc = formulaCells[i];
+                       CellCacheEntry formulaCell = getEntry(fc);
+                       CellLocation[] usedCells = formulaCell.getUsedCells();
+                       if (usedCells != null) {
+                               for (int j = 0; j < usedCells.length; j++) {
+                                       CellCacheEntry usedCell = getEntry(usedCells[j]);
+                                       usedCell.clearConsumingCell(fc);
+                               }
+                       }
+                       if (_evaluationListener != null) {
+                               ValueEval value = formulaCell.getValue();
+                               _evaluationListener.onClearDependentCachedValue(fc.getSheetIndex(), fc.getRowIndex(), fc.getColumnIndex(), value, nextDepth);
+                       }
+                       formulaCell.setFormulaResult(null, null);
+                       recurseClearCachedFormulaResults(formulaCell.getConsumingCells(), nextDepth);
                }
-               _valuesByKey.put(key, value);
        }
 
+       private CellCacheEntry getEntry(CellLocation cellLoc) {
+               CellCacheEntry result = (CellCacheEntry)_entriesByLocation.get(cellLoc);
+               if (result == null) {
+                       result = new CellCacheEntry();
+                       _entriesByLocation.put(cellLoc, result);
+               }
+               return result;
+       }
        /**
         * Should be called whenever there are changes to input cells in the evaluated workbook.
         */
        public void clear() {
-               _valuesByKey.clear();
-       }
-
-       public void clearValue(int sheetIndex, int rowIndex, int columnIndex) {
-               clearValuesRecursive(new CellEvaluationFrame(sheetIndex, rowIndex, columnIndex));
-               
-       }
-
-       private void clearValuesRecursive(CellEvaluationFrame cef) {
-               CellEvaluationFrame[] consumingCells = getConsumingCells(cef);
-               for (int i = 0; i < consumingCells.length; i++) {
-                       clearValuesRecursive(consumingCells[i]);
+               if(_evaluationListener != null) {
+                       _evaluationListener.onClearWholeCache();
                }
-               _valuesByKey.remove(cef);
-               _consumingCellsByDest.remove(cef);
+               _entriesByLocation.clear();
+               _consumingCellsByUsedCells.clear();
        }
 
-       private CellEvaluationFrame[] getConsumingCells(CellEvaluationFrame cef) {
-               List temp = (List) _consumingCellsByDest.get(cef);
-               if (temp == null) {
-                       return EMPTY_CEF_ARRAY;
-               }
-               int nItems = temp.size();
-               if (temp.size() < 1) {
-                       return EMPTY_CEF_ARRAY;
+
+       private static final Comparator CellLocationComparator = new Comparator() {
+
+               public int compare(Object a, Object b) {
+                       CellLocation clA = (CellLocation) a;
+                       CellLocation clB = (CellLocation) b;
+                       
+                       int cmp;
+                       cmp = clA.getSheetIndex() - clB.getSheetIndex();
+                       if (cmp != 0) {
+                               return cmp;
+                       }
+                       cmp = clA.getRowIndex() - clB.getRowIndex();
+                       if (cmp != 0) {
+                               return cmp;
+                       }
+                       cmp = clA.getColumnIndex() - clB.getColumnIndex();
+                       
+                       return cmp;
                }
-               CellEvaluationFrame[] result = new CellEvaluationFrame[nItems];
-               temp.toArray(result);
-               return result;
-       }
+               
+       };
 }
index 20ab3299af0ba55034b24617d8fc82be985ede02..834941e9be62f2a6b8835e1a0c37b0c2d07182e8 100755 (executable)
@@ -18,7 +18,9 @@
 package org.apache.poi.ss.formula;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.poi.hssf.record.formula.eval.ErrorEval;
 import org.apache.poi.hssf.record.formula.eval.ValueEval;
@@ -36,11 +38,13 @@ import org.apache.poi.hssf.usermodel.HSSFCell;
 final class EvaluationTracker {
 
        private final List _evaluationFrames;
+       private final Set _currentlyEvaluatingCells;
        private final EvaluationCache _cache;
 
        public EvaluationTracker(EvaluationCache cache) {
                _cache = cache;
                _evaluationFrames = new ArrayList();
+               _currentlyEvaluatingCells = new HashSet();
        }
 
        /**
@@ -58,14 +62,17 @@ final class EvaluationTracker {
         * @return <code>true</code> if the specified cell has not been visited yet in the current 
         * evaluation. <code>false</code> if the specified cell is already being evaluated.
         */
-       public ValueEval startEvaluate(int sheetIndex, int srcRowNum, int srcColNum) {
-               CellEvaluationFrame cef = new CellEvaluationFrame(sheetIndex, srcRowNum, srcColNum);
-               if (_evaluationFrames.contains(cef)) {
+       public ValueEval startEvaluate(CellLocation cellLoc) {
+               if (cellLoc == null) {
+                       throw new IllegalArgumentException("cellLoc must not be null");
+               }
+               if (_currentlyEvaluatingCells.contains(cellLoc)) {
                        return ErrorEval.CIRCULAR_REF_ERROR;
                }
-               ValueEval result = _cache.getValue(cef);
+               ValueEval result = _cache.getValue(cellLoc);
                if (result == null) {
-                       _evaluationFrames.add(cef);
+                       _currentlyEvaluatingCells.add(cellLoc);
+                       _evaluationFrames.add(new CellEvaluationFrame(cellLoc));
                }
                return result;
        }
@@ -83,24 +90,41 @@ final class EvaluationTracker {
         * and form more meaningful error messages.
         * @param result 
         */
-       public void endEvaluate(int sheetIndex, int srcRowNum, int srcColNum, ValueEval result) {
+       public void endEvaluate(CellLocation cellLoc, ValueEval result, boolean isPlainValueCell) {
+
                int nFrames = _evaluationFrames.size();
                if (nFrames < 1) {
                        throw new IllegalStateException("Call to endEvaluate without matching call to startEvaluate");
                }
 
                nFrames--;
-               CellEvaluationFrame cefExpected = (CellEvaluationFrame) _evaluationFrames.get(nFrames);
-               CellEvaluationFrame cefActual = new CellEvaluationFrame(sheetIndex, srcRowNum, srcColNum);
-               if (!cefActual.equals(cefExpected)) {
+               CellEvaluationFrame frame = (CellEvaluationFrame) _evaluationFrames.get(nFrames);
+               CellLocation coordinates = frame.getCoordinates();
+               if (!coordinates.equals(cellLoc)) {
                        throw new RuntimeException("Wrong cell specified. "
                                        + "Corresponding startEvaluate() call was for cell {"
-                                       + cefExpected.formatAsString() + "} this endEvaluate() call is for cell {"
-                                       + cefActual.formatAsString() + "}");
+                                       + coordinates.formatAsString() + "} this endEvaluate() call is for cell {"
+                                       + cellLoc.formatAsString() + "}");
                }
                // else - no problems so pop current frame 
                _evaluationFrames.remove(nFrames);
-               
-               _cache.setValue(cefActual, result);
+               _currentlyEvaluatingCells.remove(coordinates);
+
+               // TODO - don't cache results of volatile formulas 
+               _cache.setValue(coordinates, isPlainValueCell, frame.getUsedCells(), result);
+       }
+       /**
+        * Tells the currently evaluating cell frame that it has a dependency on the specified 
+        * <tt>usedCell<tt> 
+        * @param usedCell location of cell which is referenced (and used) by the current cell.
+        */
+       public void acceptDependency(CellLocation usedCell) {
+               int prevFrameIndex = _evaluationFrames.size()-1;
+               if (prevFrameIndex < 0) {
+                       throw new IllegalStateException("Call to acceptDependency without prior call to startEvaluate");
+               }
+               CellEvaluationFrame consumingFrame = (CellEvaluationFrame) _evaluationFrames.get(prevFrameIndex);
+               consumingFrame.addUsedCell(usedCell);
        }
+
 }
index 29083f7a5dec452cb09e710bfb46f002d336ee85..664df2c6abeb7dc7bfcb05834589d01e98c31689 100644 (file)
@@ -35,7 +35,7 @@ public interface EvaluationWorkbook {
 \r
        HSSFSheet getSheet(int sheetIndex);\r
 \r
-       HSSFSheet getSheetByExternSheetIndex(int externSheetIndex);\r
+       int convertFromExternSheetIndex(int externSheetIndex);\r
        EvaluationName getName(NamePtg namePtg);\r
        String resolveNameXText(NameXPtg ptg);\r
        Ptg[] getFormulaTokens(HSSFCell cell);\r
diff --git a/src/java/org/apache/poi/ss/formula/IEvaluationListener.java b/src/java/org/apache/poi/ss/formula/IEvaluationListener.java
new file mode 100644 (file)
index 0000000..deba4d5
--- /dev/null
@@ -0,0 +1,39 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.formula;
+
+import org.apache.poi.hssf.record.formula.Ptg;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+
+/**
+ * Tests can implement this class to track the internal working of the {@link WorkbookEvaluator}.<br/>
+ * 
+ * For POI internal testing use only
+ * 
+ * @author Josh Micich
+ */
+interface IEvaluationListener {
+
+       void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result);
+       void onReadPlainValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value);
+       void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs);
+       void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result);
+       void onClearWholeCache();
+       void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value);
+       void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value, int depth);
+}
index 07c368ed0f09ecc9fd22c7c2b0b33e3ae9e8a148..3b891fbbb8317e5e5083d1fb1f9b9b5d9c509778 100644 (file)
@@ -21,11 +21,7 @@ import org.apache.poi.hssf.record.formula.AreaI;
 import org.apache.poi.hssf.record.formula.AreaI.OffsetArea;
 import org.apache.poi.hssf.record.formula.eval.AreaEval;
 import org.apache.poi.hssf.record.formula.eval.AreaEvalBase;
-import org.apache.poi.hssf.record.formula.eval.BlankEval;
 import org.apache.poi.hssf.record.formula.eval.ValueEval;
-import org.apache.poi.hssf.usermodel.HSSFCell;
-import org.apache.poi.hssf.usermodel.HSSFRow;
-import org.apache.poi.hssf.usermodel.HSSFSheet;
 import org.apache.poi.hssf.util.CellReference;
 
 /**
@@ -34,12 +30,10 @@ import org.apache.poi.hssf.util.CellReference;
  */
 final class LazyAreaEval extends AreaEvalBase {
 
-       private final HSSFSheet _sheet;
-       private final CellEvaluator _evaluator;
+       private final SheetRefEvaluator _evaluator;
 
-       public LazyAreaEval(AreaI ptg, HSSFSheet sheet, CellEvaluator evaluator) {
+       public LazyAreaEval(AreaI ptg, SheetRefEvaluator evaluator) {
                super(ptg);
-               _sheet = sheet;
                _evaluator = evaluator;
        }
 
@@ -48,30 +42,21 @@ final class LazyAreaEval extends AreaEvalBase {
                int rowIx = (relativeRowIndex + getFirstRow() ) & 0xFFFF;
                int colIx = (relativeColumnIndex + getFirstColumn() ) & 0x00FF;
                
-               HSSFRow row = _sheet.getRow(rowIx);
-               if (row == null) {
-                       return BlankEval.INSTANCE;
-               }
-               HSSFCell cell = row.getCell(colIx);
-               if (cell == null) {
-                       return BlankEval.INSTANCE;
-               }
-               return _evaluator.getEvalForCell(cell);
+               return _evaluator.getEvalForCell(rowIx, colIx);
        }
 
        public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) {
                AreaI area = new OffsetArea(getFirstRow(), getFirstColumn(),
                                relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx);
 
-               return new LazyAreaEval(area, _sheet, _evaluator);
+               return new LazyAreaEval(area, _evaluator);
        }
        public String toString() {
                CellReference crA = new CellReference(getFirstRow(), getFirstColumn());
                CellReference crB = new CellReference(getLastRow(), getLastColumn());
                StringBuffer sb = new StringBuffer();
                sb.append(getClass().getName()).append("[");
-               String sheetName = _evaluator.getSheetName(_sheet);
-               sb.append(sheetName);
+               sb.append(_evaluator.getSheetName());
                sb.append('!');
                sb.append(crA.formatAsString());
                sb.append(':');
index fa1f3a383dce9c66aa8a02e9f11f8bcf6f30d639..f25e86fc405e9b5279e5a6ab430737a6fecad3e7 100644 (file)
@@ -22,12 +22,8 @@ import org.apache.poi.hssf.record.formula.Ref3DPtg;
 import org.apache.poi.hssf.record.formula.RefPtg;\r
 import org.apache.poi.hssf.record.formula.AreaI.OffsetArea;\r
 import org.apache.poi.hssf.record.formula.eval.AreaEval;\r
-import org.apache.poi.hssf.record.formula.eval.BlankEval;\r
 import org.apache.poi.hssf.record.formula.eval.RefEvalBase;\r
 import org.apache.poi.hssf.record.formula.eval.ValueEval;\r
-import org.apache.poi.hssf.usermodel.HSSFCell;\r
-import org.apache.poi.hssf.usermodel.HSSFRow;\r
-import org.apache.poi.hssf.usermodel.HSSFSheet;\r
 import org.apache.poi.hssf.util.CellReference;\r
 \r
 /**\r
@@ -36,34 +32,19 @@ import org.apache.poi.hssf.util.CellReference;
 */\r
 final class LazyRefEval extends RefEvalBase {\r
 \r
-       private final HSSFSheet _sheet;\r
-       private final CellEvaluator _evaluator;\r
+       private final SheetRefEvaluator _evaluator;\r
 \r
-\r
-       public LazyRefEval(RefPtg ptg, HSSFSheet sheet, CellEvaluator evaluator) {\r
+       public LazyRefEval(RefPtg ptg, SheetRefEvaluator sre) {\r
                super(ptg.getRow(), ptg.getColumn());\r
-               _sheet = sheet;\r
-               _evaluator = evaluator;\r
+               _evaluator = sre;\r
        }\r
-       public LazyRefEval(Ref3DPtg ptg, HSSFSheet sheet, CellEvaluator evaluator) {\r
+       public LazyRefEval(Ref3DPtg ptg, SheetRefEvaluator sre) {\r
                super(ptg.getRow(), ptg.getColumn());\r
-               _sheet = sheet;\r
-               _evaluator = evaluator;\r
+               _evaluator = sre;\r
        }\r
 \r
        public ValueEval getInnerValueEval() {\r
-               int rowIx = getRow();\r
-               int colIx = getColumn();\r
-               \r
-               HSSFRow row = _sheet.getRow(rowIx);\r
-               if (row == null) {\r
-                       return BlankEval.INSTANCE;\r
-               }\r
-               HSSFCell cell = row.getCell(colIx);\r
-               if (cell == null) {\r
-                       return BlankEval.INSTANCE;\r
-               }\r
-               return _evaluator.getEvalForCell(cell);\r
+               return _evaluator.getEvalForCell(getRow(), getColumn());\r
        }\r
        \r
        public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) {\r
@@ -71,15 +52,14 @@ final class LazyRefEval extends RefEvalBase {
                AreaI area = new OffsetArea(getRow(), getColumn(),\r
                                relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx);\r
 \r
-               return new LazyAreaEval(area, _sheet, _evaluator);\r
+               return new LazyAreaEval(area, _evaluator);\r
        }\r
        \r
        public String toString() {\r
                CellReference cr = new CellReference(getRow(), getColumn());\r
                StringBuffer sb = new StringBuffer();\r
                sb.append(getClass().getName()).append("[");\r
-               String sheetName = _evaluator.getSheetName(_sheet);\r
-               sb.append(sheetName);\r
+               sb.append(_evaluator.getSheetName());\r
                sb.append('!');\r
                sb.append(cr.formatAsString());\r
                sb.append("]");\r
diff --git a/src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java b/src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java
new file mode 100644 (file)
index 0000000..6babc27
--- /dev/null
@@ -0,0 +1,49 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.ss.formula;\r
+\r
+import org.apache.poi.hssf.record.formula.eval.ValueEval;\r
+import org.apache.poi.hssf.usermodel.HSSFSheet;\r
+/**\r
+ * \r
+ * \r
+ * @author Josh Micich\r
+ */\r
+final class SheetRefEvaluator {\r
+\r
+       private final WorkbookEvaluator _bookEvaluator;\r
+       private final EvaluationTracker _tracker;\r
+       private final HSSFSheet _sheet;\r
+       private final int _sheetIndex;\r
+\r
+       public SheetRefEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker,\r
+                       EvaluationWorkbook _workbook, int sheetIndex) {\r
+               _bookEvaluator = bookEvaluator;\r
+               _tracker = tracker;\r
+               _sheet = _workbook.getSheet(sheetIndex);\r
+               _sheetIndex = sheetIndex;\r
+       }\r
+\r
+       public String getSheetName() {\r
+               return _bookEvaluator.getSheetName(_sheetIndex);\r
+       }\r
+\r
+       public ValueEval getEvalForCell(int rowIndex, int columnIndex) {\r
+               return _bookEvaluator.evaluateReference(_sheet, _sheetIndex, rowIndex, columnIndex, _tracker);\r
+       }\r
+}\r
index 199d35b66e5d510c50f420055b65ef51225dbdda..36ad65b7f1ad29499dceab89924bfb6c002bdd17 100644 (file)
@@ -17,6 +17,8 @@
 
 package org.apache.poi.ss.formula;
 
+import java.util.IdentityHashMap;
+import java.util.Map;
 import java.util.Stack;
 
 import org.apache.poi.hssf.record.formula.Area3DPtg;
@@ -51,6 +53,7 @@ import org.apache.poi.hssf.record.formula.eval.RefEval;
 import org.apache.poi.hssf.record.formula.eval.StringEval;
 import org.apache.poi.hssf.record.formula.eval.ValueEval;
 import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFRow;
 import org.apache.poi.hssf.usermodel.HSSFSheet;
 import org.apache.poi.hssf.util.CellReference;
 
@@ -67,43 +70,28 @@ import org.apache.poi.hssf.util.CellReference;
  */
 public class WorkbookEvaluator {
 
-       /**
-        * used to track the number of evaluations
-        */
-       private static final class Counter {
-               public int value;
-               public int depth;
-               public Counter() {
-                       value = 0;
-               }
-       }
-
        private final EvaluationWorkbook _workbook;
        private final EvaluationCache _cache;
 
-       private Counter _evaluationCounter;
+       private final IEvaluationListener _evaluationListener;
+       private final Map _sheetIndexesBySheet;
 
        public WorkbookEvaluator(EvaluationWorkbook workbook) {
+               this (workbook, null);
+       }
+       /* package */ WorkbookEvaluator(EvaluationWorkbook workbook, IEvaluationListener evaluationListener) {
                _workbook = workbook;
-               _cache = new EvaluationCache();
-               _evaluationCounter = new Counter();
+               _evaluationListener = evaluationListener;
+               _cache = new EvaluationCache(evaluationListener);
+               _sheetIndexesBySheet = new IdentityHashMap();
        }
 
        /**
-        * for debug use. Used in toString methods
+        * also for debug use. Used in toString methods
         */
-       /* package */ String getSheetName(HSSFSheet sheet) {
-               return getSheetName(getSheetIndex(sheet));
-       }
-       private String getSheetName(int sheetIndex) {
+       /* package */ String getSheetName(int sheetIndex) {
                return _workbook.getSheetName(sheetIndex);
        }
-       /**
-        * for debug/test use
-        */
-       public int getEvaluationCount() {
-               return _evaluationCounter.value;
-       }
 
        private static boolean isDebugLogEnabled() {
                return false;
@@ -121,56 +109,122 @@ public class WorkbookEvaluator {
         */
        public void clearAllCachedResultValues() {
                _cache.clear();
+               _sheetIndexesBySheet.clear();
        }
 
-       public void clearCachedResultValue(HSSFSheet sheet, int rowIndex, int columnIndex) {
+       /**
+        * Sets the cached value for a plain (non-formula) cell.
+        * @param never <code>null</code>. Use {@link BlankEval#INSTANCE} when the cell is being 
+        * cleared. Otherwise an instance of {@link NumberEval}, {@link StringEval}, {@link BoolEval}
+        * or {@link ErrorEval} to represent a plain cell value.
+        */
+       public void setCachedPlainValue(HSSFSheet sheet, int rowIndex, int columnIndex, ValueEval value) {
+               if (value == null) {
+                       throw new IllegalArgumentException("value must not be null");
+               }
+               int sheetIndex = getSheetIndex(sheet);
+               _cache.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), true, CellLocation.EMPTY_ARRAY, value);
+
+       }
+       /**
+        * Should be called to tell the cell value cache that the specified cell has just become a
+        * formula cell, or the formula text has changed 
+        */
+       public void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex) {
                int sheetIndex = getSheetIndex(sheet);
-               _cache.clearValue(sheetIndex, rowIndex, columnIndex);
+               _cache.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null);
 
        }
        private int getSheetIndex(HSSFSheet sheet) {
-               // TODO cache sheet indexes too
-               return _workbook.getSheetIndex(sheet);
+               Integer result = (Integer) _sheetIndexesBySheet.get(sheet);
+               if (result == null) {
+                       result = new Integer(_workbook.getSheetIndex(sheet));
+                       _sheetIndexesBySheet.put(sheet, result);
+               }
+               return result.intValue();
        }
 
        public ValueEval evaluate(HSSFCell srcCell) {
-               return internalEvaluate(srcCell, new EvaluationTracker(_cache));
+               int sheetIndex = getSheetIndex(srcCell.getSheet());
+               CellLocation cellLoc = new CellLocation(sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum());
+               return internalEvaluate(srcCell, cellLoc, new EvaluationTracker(_cache));
        }
 
        /**
-        * Dev. Note: Internal evaluate must be passed only a formula cell
-        * else a runtime exception will be thrown somewhere inside the method.
-        * (Hence this is a private method.)
         * @return never <code>null</code>, never {@link BlankEval}
         */
-       /* package */ ValueEval internalEvaluate(HSSFCell srcCell, EvaluationTracker tracker) {
-               int srcRowNum = srcCell.getRowIndex();
-               int srcColNum = srcCell.getCellNum();
+       private ValueEval internalEvaluate(HSSFCell srcCell, CellLocation cellLoc, EvaluationTracker tracker) {
+               int sheetIndex = cellLoc.getSheetIndex();
+               int rowIndex = cellLoc.getRowIndex();
+               int columnIndex = cellLoc.getColumnIndex();
 
                ValueEval result;
 
-               int sheetIndex = getSheetIndex(srcCell.getSheet());
-               result = tracker.startEvaluate(sheetIndex, srcRowNum, srcColNum);
+               result = tracker.startEvaluate(cellLoc);
+               IEvaluationListener evalListener = _evaluationListener;
                if (result != null) {
+                       if(evalListener != null) {
+                               evalListener.onCacheHit(sheetIndex, rowIndex, columnIndex, result);
+                       }
                        return result;
                }
-               _evaluationCounter.value++;
-               _evaluationCounter.depth++;
 
+               boolean isPlainFormulaCell = false;
                try {
-                       Ptg[] ptgs = _workbook.getFormulaTokens(srcCell);
-                       result = evaluateCell(sheetIndex, srcRowNum, (short)srcColNum, ptgs, tracker);
+                       result = getValueFromNonFormulaCell(srcCell);
+                       if (result != null) {
+                               isPlainFormulaCell = true;
+                               if(evalListener != null) {
+                                       evalListener.onReadPlainValue(sheetIndex, rowIndex, columnIndex, result);
+                               }
+                       } else {
+                               isPlainFormulaCell = false;
+                               Ptg[] ptgs = _workbook.getFormulaTokens(srcCell);
+                               if(evalListener == null) {
+                                       result = evaluateCell(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker);
+                               } else {
+                                       evalListener.onStartEvaluate(sheetIndex, rowIndex, columnIndex, ptgs);
+                                       result = evaluateCell(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker);
+                                       evalListener.onEndEvaluate(sheetIndex, rowIndex, columnIndex, result);
+                               }
+                       }
                } finally {
-                       tracker.endEvaluate(sheetIndex, srcRowNum, srcColNum, result);
-                       _evaluationCounter.depth--;
+                       tracker.endEvaluate(cellLoc, result, isPlainFormulaCell);
                }
                if (isDebugLogEnabled()) {
                        String sheetName = getSheetName(sheetIndex);
-                       CellReference cr = new CellReference(srcRowNum, srcColNum);
+                       CellReference cr = new CellReference(rowIndex, columnIndex);
                        logDebug("Evaluated " + sheetName + "!" + cr.formatAsString() + " to " + result.toString());
                }
                return result;
        }
+       /**
+        * Gets the value from a non-formula cell.
+        * @param cell may be <code>null</code>
+        * @return {@link BlankEval} if cell is <code>null</code> or blank, <code>null</code> if cell 
+        * is a formula cell.
+        */
+       private static ValueEval getValueFromNonFormulaCell(HSSFCell cell) {
+               if (cell == null) {
+                       return BlankEval.INSTANCE;
+               }
+               int cellType = cell.getCellType();
+               switch (cellType) {
+                       case HSSFCell.CELL_TYPE_FORMULA:
+                               return null;
+                       case HSSFCell.CELL_TYPE_NUMERIC:
+                               return new NumberEval(cell.getNumericCellValue());
+                       case HSSFCell.CELL_TYPE_STRING:
+                               return new StringEval(cell.getRichStringCellValue().getString());
+                       case HSSFCell.CELL_TYPE_BOOLEAN:
+                               return BoolEval.valueOf(cell.getBooleanCellValue());
+                       case HSSFCell.CELL_TYPE_BLANK:
+                               return BlankEval.INSTANCE;
+                       case HSSFCell.CELL_TYPE_ERROR:
+                               return ErrorEval.valueOf(cell.getErrorCellValue());
+               }
+               throw new RuntimeException("Unexpected cell type (" + cellType + ")");
+       }
        private ValueEval evaluateCell(int sheetIndex, int srcRowNum, short srcColNum, Ptg[] ptgs, EvaluationTracker tracker) {
 
                Stack stack = new Stack();
@@ -268,10 +322,6 @@ public class WorkbookEvaluator {
                return operation.evaluate(ops, srcRowNum, (short)srcColNum);
        }
 
-       private HSSFSheet getOtherSheet(int externSheetIndex) {
-               return _workbook.getSheetByExternSheetIndex(externSheetIndex);
-       }
-
        /**
         * returns an appropriate Eval impl instance for the Ptg. The Ptg must be
         * one of: Area3DPtg, AreaPtg, ReferencePtg, Ref3DPtg, IntPtg, NumberPtg,
@@ -311,23 +361,24 @@ public class WorkbookEvaluator {
                if (ptg instanceof ErrPtg) {
                        return ErrorEval.valueOf(((ErrPtg) ptg).getErrorCode());
                }
-               CellEvaluator ce = new CellEvaluator(this, tracker);
-               HSSFSheet sheet = _workbook.getSheet(sheetIndex);
-               if (ptg instanceof RefPtg) {
-                       return new LazyRefEval(((RefPtg) ptg), sheet, ce);
-               }
-               if (ptg instanceof AreaPtg) {
-                       return new LazyAreaEval(((AreaPtg) ptg), sheet, ce);
-               }
                if (ptg instanceof Ref3DPtg) {
                        Ref3DPtg refPtg = (Ref3DPtg) ptg;
-                       HSSFSheet xsheet = getOtherSheet(refPtg.getExternSheetIndex());
-                       return new LazyRefEval(refPtg, xsheet, ce);
+                       int otherSheetIndex = _workbook.convertFromExternSheetIndex(refPtg.getExternSheetIndex());
+                       SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex);
+                       return new LazyRefEval(refPtg, sre);
                }
                if (ptg instanceof Area3DPtg) {
-                       Area3DPtg a3dp = (Area3DPtg) ptg;
-                       HSSFSheet xsheet = getOtherSheet(a3dp.getExternSheetIndex());
-                       return new LazyAreaEval(a3dp, xsheet, ce);
+                       Area3DPtg aptg = (Area3DPtg) ptg;
+                       int otherSheetIndex = _workbook.convertFromExternSheetIndex(aptg.getExternSheetIndex());
+                       SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex);
+                       return new LazyAreaEval(aptg, sre);
+               }
+               SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, sheetIndex);
+               if (ptg instanceof RefPtg) {
+                       return new LazyRefEval(((RefPtg) ptg), sre);
+               }
+               if (ptg instanceof AreaPtg) {
+                       return new LazyAreaEval(((AreaPtg) ptg), sre);
                }
 
                if (ptg instanceof UnknownPtg) {
@@ -345,4 +396,22 @@ public class WorkbookEvaluator {
                }
                return getEvalForPtg(ptgs[0], sheetIndex, tracker);
        }
+       
+       /**
+        * Used by the lazy ref evals whenever they need to get the value of a contained cell.
+        */
+       /* package */ ValueEval evaluateReference(HSSFSheet sheet, int sheetIndex, int rowIndex,
+                       int columnIndex, EvaluationTracker tracker) {
+               
+               HSSFRow row = sheet.getRow(rowIndex);
+               HSSFCell cell;
+               if (row == null) {
+                       cell = null;
+               } else {
+                       cell = row.getCell(columnIndex);
+               }
+               CellLocation cellLoc = new CellLocation(sheetIndex, rowIndex, columnIndex);
+               tracker.acceptDependency(cellLoc);
+               return internalEvaluate(cell, cellLoc, tracker);
+       }
 }
index 1edaf7e151a9ad3634c076b9e9d4c52675c7b90d..aa5bf46d31fd3b501ee25d1e05fc83bbf2a079a6 100644 (file)
@@ -28,6 +28,7 @@ import org.apache.poi.hssf.model.AllModelTests;
 import org.apache.poi.hssf.record.AllRecordTests;
 import org.apache.poi.hssf.usermodel.AllUserModelTests;
 import org.apache.poi.hssf.util.AllHSSFUtilTests;
+import org.apache.poi.ss.formula.TestEvaluationCache;
 
 /**
  * Test Suite for all sub-packages of org.apache.poi.hssf<br/>
@@ -52,6 +53,7 @@ public final class HSSFTests {
         }
         suite.addTest(new TestSuite(TestEventRecordFactory.class));
         suite.addTest(new TestSuite(TestModelFactory.class));
+        suite.addTest(new TestSuite(TestEvaluationCache.class));
         return suite;
     }
 }
index 3fb2ed8d869843faa68138cd5cd10fb08c881a3d..5de348bdaf9b1ef7a7ddbbee392d75bcffe98d06 100755 (executable)
@@ -71,8 +71,9 @@ public final class TestCircularReferences extends TestCase {
                // This formula should evaluate to the contents of B2,
                testCell.setCellFormula("INDEX(A1:B4,2,2)");
                // However the range A1:B4 also includes the current cell A4.  If the other parameters
-               // were 4 and 1, this would represent a circular reference.  Since POI 'fully' evaluates
-               // arguments before invoking operators, POI must handle such potential cycles gracefully.
+               // were 4 and 1, this would represent a circular reference.  Prior to v3.2 POI would 
+               // 'fully' evaluate ref arguments before invoking operators, which raised the possibility of
+               // cycles / StackOverflowErrors.
                
 
                CellValue cellValue = evaluateWithCycles(wb, testCell);
index c2f11cfcc9b48895a23da7fb22d8e67254fbc1c6..baa72743d04fa2b826c093ecdde90d8277a0776f 100644 (file)
@@ -31,6 +31,10 @@ import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
 import org.apache.poi.hssf.record.formula.AreaPtg;
 import org.apache.poi.hssf.record.formula.FuncVarPtg;
 import org.apache.poi.hssf.record.formula.Ptg;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+import org.apache.poi.ss.formula.EvaluationListener;
+import org.apache.poi.ss.formula.WorkbookEvaluator;
+import org.apache.poi.ss.formula.WorkbookEvaluatorTestHelper;
 
 /**
  * 
@@ -287,6 +291,29 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
                }
        }
        
+       private static final class EvalListener extends EvaluationListener {
+               private int _countCacheHits;
+               private int _countCacheMisses;
+
+               public EvalListener() {
+                       _countCacheHits = 0;
+                       _countCacheMisses = 0;
+               }
+               public int getCountCacheHits() {
+                       return _countCacheHits;
+               }
+               public int getCountCacheMisses() {
+                       return _countCacheMisses;
+               }
+
+               public void onCacheHit(int sheetIndex, int srcRowNum, int srcColNum, ValueEval result) {
+                       _countCacheHits++;
+               }
+               public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) {
+                       _countCacheMisses++;
+               }
+       }
+       
        /**
         * The HSSFFormula evaluator performance benefits greatly from caching of intermediate cell values
         */
@@ -312,16 +339,20 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
                
                // Choose cell A9, so that the failing test case doesn't take too long to execute.
                HSSFCell cell = row.getCell(8);
-               HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(wb);
+               EvalListener evalListener = new EvalListener();
+               WorkbookEvaluator evaluator = WorkbookEvaluatorTestHelper.createEvaluator(wb, evalListener);
                evaluator.evaluate(cell);
-               int evalCount = evaluator.getEvaluationCount();
-               // With caching, the evaluationCount is 8 which is a big improvement
-               assertTrue(evalCount > 0);  // make sure the counter is actually working
+               int evalCount = evalListener.getCountCacheMisses();
                if (evalCount > 10) {
                        // Without caching, evaluating cell 'A9' takes 21845 evaluations which consumes
                        // much time (~3 sec on Core 2 Duo 2.2GHz)
                        System.err.println("Cell A9 took " + evalCount + " intermediate evaluations");
                        throw new AssertionFailedError("Identifed bug 45376 - Formula evaluator should cache values");
                }
+               // With caching, the evaluationCount is 8 which is a big improvement
+               // Note - these expected values may change if the WorkbookEvaluator is 
+               // ever optimised to short circuit 'if' functions.
+               assertEquals(8, evalCount);
+               assertEquals(24, evalListener.getCountCacheHits());
        }
 }
\ No newline at end of file
index 06213b762ceab2ef01788a0934e79037df0239b4..ce77bfd0250e44d66674874da5698925cf71972f 100644 (file)
 
 package org.apache.poi.hssf.usermodel;
 
+import junit.framework.TestCase;
+
 import org.apache.poi.hssf.HSSFTestDataSamples;
 import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue;
-
-import junit.framework.TestCase;
 /**
  * 
  * @author Josh Micich
@@ -79,4 +79,17 @@ public final class TestHSSFFormulaEvaluator extends TestCase {
                }
                row.createCell(colIndex).setCellValue(value);
        }
+
+       /**
+        * {@link HSSFFormulaEvaluator#evaluate(HSSFCell)} should behave the same whether the cell
+        * is <code>null</code> or blank.
+        */
+       public void testEvaluateBlank() {
+               HSSFWorkbook wb = new HSSFWorkbook();
+               HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb);
+               assertNull(fe.evaluate(null));
+               HSSFSheet sheet = wb.createSheet("Sheet1");
+               HSSFCell cell = sheet.createRow(0).createCell(0);
+               assertNull(fe.evaluate(cell));
+       }
 }
diff --git a/src/testcases/org/apache/poi/ss/formula/EvaluationListener.java b/src/testcases/org/apache/poi/ss/formula/EvaluationListener.java
new file mode 100644 (file)
index 0000000..e6ae025
--- /dev/null
@@ -0,0 +1,53 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.formula;
+
+import org.apache.poi.hssf.record.formula.Ptg;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+
+/**
+ * Tests should extend this class if they need to track the internal working of the {@link WorkbookEvaluator}.<br/>
+ * 
+ * Default method implementations all do nothing
+ * 
+ * @author Josh Micich
+ */
+public abstract class EvaluationListener implements IEvaluationListener {
+       public void onCacheHit(int sheetIndex, int rowIndex, int srcColNum, ValueEval result) {
+               // do nothing 
+       }
+       public void onReadPlainValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value) {
+               // do nothing 
+       }
+       public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) {
+               // do nothing 
+       }
+       public void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) {
+               // do nothing 
+       }
+       public void onClearWholeCache() {
+               // do nothing 
+       }
+       public void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) {
+               // do nothing 
+       }
+       public void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex,
+                       ValueEval value,int depth) {
+               // do nothing 
+       }
+}
diff --git a/src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java b/src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java
new file mode 100644 (file)
index 0000000..bba2ec0
--- /dev/null
@@ -0,0 +1,492 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.formula;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import org.apache.poi.hssf.model.HSSFFormulaParser;
+import org.apache.poi.hssf.record.formula.Ptg;
+import org.apache.poi.hssf.record.formula.eval.BlankEval;
+import org.apache.poi.hssf.record.formula.eval.BoolEval;
+import org.apache.poi.hssf.record.formula.eval.ErrorEval;
+import org.apache.poi.hssf.record.formula.eval.NumberEval;
+import org.apache.poi.hssf.record.formula.eval.StringEval;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.hssf.util.CellReference;
+
+/**
+ * Tests {@link EvaluationCache}.  Makes sure that where possible (previously calculated) cached 
+ * values are used.  Also checks that changing cell values causes the correct (minimal) set of
+ * dependent cached values to be cleared.
+ *
+ * @author Josh Micich
+ */
+public class TestEvaluationCache extends TestCase {
+       private static final class EvalListener extends EvaluationListener {
+
+               private final List _logList;
+               private final HSSFWorkbook _book;
+
+               public EvalListener(HSSFWorkbook wb) {
+                       _book = wb;
+                       _logList = new ArrayList();
+               }
+               public void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) {
+                       log("hit", rowIndex, columnIndex, result);
+               }
+               public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) {
+                       log("value", rowIndex, columnIndex, value);
+               }
+               public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) {
+                       log("start", rowIndex, columnIndex, ptgs);
+               }
+               public void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) {
+                       log("end", rowIndex, columnIndex, result);
+               }
+               public void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) {
+                       log("clear", rowIndex, columnIndex, value);
+               }
+               public void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex,
+                               ValueEval value,int depth) {
+                       log("clear" + depth, rowIndex, columnIndex, value);
+               }
+               private void log(String tag, int rowIndex, int columnIndex, Object value) {
+                       StringBuffer sb = new StringBuffer(64);
+                       sb.append(tag).append(' ');
+                       sb.append(new CellReference(rowIndex, columnIndex).formatAsString());
+                       if (value != null) {
+                               sb.append(' ').append(formatValue(value));
+                       }
+                       _logList.add(sb.toString());
+               }
+               private String formatValue(Object value) {
+                       if (value instanceof Ptg[]) {
+                               Ptg[] ptgs = (Ptg[]) value;
+                               return HSSFFormulaParser.toFormulaString(_book, ptgs);
+                       }
+                       if (value instanceof NumberEval) {
+                               NumberEval ne = (NumberEval) value;
+                               return ne.getStringValue();
+                       }
+                       if (value instanceof StringEval) {
+                               StringEval se = (StringEval) value;
+                               return "'" + se.getStringValue() + "'";
+                       }
+                       if (value instanceof BoolEval) {
+                               BoolEval be = (BoolEval) value;
+                               return be.getStringValue();
+                       }
+                       if (value == BlankEval.INSTANCE) {
+                               return "#BLANK#";
+                       }
+                       if (value instanceof ErrorEval) {
+                               ErrorEval ee = (ErrorEval) value;
+                               return ErrorEval.getText(ee.getErrorCode());
+                       }
+                       throw new IllegalArgumentException("Unexpected value class ("
+                                       + value.getClass().getName() + ")");
+               }
+               public String[] getAndClearLog() {
+                       String[] result = new String[_logList.size()];
+                       _logList.toArray(result);
+                       _logList.clear();
+                       return result;
+               }
+       }
+       /**
+        * Wrapper class to manage repetitive tasks from this test,
+        *
+        * Note - this class does a little bit more than just plain set-up of data. The method
+        * {@link WorkbookEvaluator#clearCachedResultValue(HSSFSheet, int, int)} is called whenever a
+        * cell value is changed.
+        *
+        */
+       private static final class MySheet {
+
+               private final HSSFSheet _sheet;
+               private final WorkbookEvaluator _evaluator;
+               private final HSSFWorkbook _wb;
+               private final EvalListener _evalListener;
+
+               public MySheet() {
+                       _wb = new HSSFWorkbook();
+                       _evalListener = new EvalListener(_wb);
+                       _evaluator = WorkbookEvaluatorTestHelper.createEvaluator(_wb, _evalListener);
+                       _sheet = _wb.createSheet("Sheet1");
+               }
+
+               public void setCellValue(String cellRefText, double value) {
+                       HSSFCell cell = getOrCreateCell(cellRefText);
+                       // be sure to blank cell, in case it is currently a formula
+                       cell.setCellType(HSSFCell.CELL_TYPE_BLANK);
+                       // otherwise this line will only set the formula cached result;
+                       cell.setCellValue(value);
+                       _evaluator.setCachedPlainValue(_sheet, cell.getRowIndex(), cell.getCellNum(), new NumberEval(value));
+               }
+
+               public void setCellFormula(String cellRefText, String formulaText) {
+                       HSSFCell cell = getOrCreateCell(cellRefText);
+                       cell.setCellFormula(formulaText);
+                       _evaluator.notifySetFormula(_sheet, cell.getRowIndex(), cell.getCellNum());
+               }
+
+               private HSSFCell getOrCreateCell(String cellRefText) {
+                       CellReference cr = new CellReference(cellRefText);
+                       int rowIndex = cr.getRow();
+                       HSSFRow row = _sheet.getRow(rowIndex);
+                       if (row == null) {
+                               row = _sheet.createRow(rowIndex);
+                       }
+                       int cellIndex = cr.getCol();
+                       HSSFCell cell = row.getCell(cellIndex);
+                       if (cell == null) {
+                               cell = row.createCell(cellIndex);
+                       }
+                       return cell;
+               }
+
+               public ValueEval evaluateCell(String cellRefText) {
+                       return _evaluator.evaluate(getOrCreateCell(cellRefText));
+               }
+
+               public String[] getAndClearLog() {
+                       return _evalListener.getAndClearLog();
+               }
+
+               public void clearAllCachedResultValues() {
+                       _evaluator.clearAllCachedResultValues();
+               }
+       }
+
+       private static MySheet createMediumComplex() {
+               MySheet ms = new MySheet();
+
+               // plain data in D1:F3
+               ms.setCellValue("D1", 12);
+               ms.setCellValue("E1", 13);
+               ms.setCellValue("D2", 14);
+               ms.setCellValue("E2", 15);
+               ms.setCellValue("D3", 16);
+               ms.setCellValue("E3", 17);
+
+
+               ms.setCellFormula("C1", "SUM(D1:E2)");
+               ms.setCellFormula("C2", "SUM(D2:E3)");
+               ms.setCellFormula("C3", "SUM(D3:E4)");
+
+               ms.setCellFormula("B1", "C2-C1");
+               ms.setCellFormula("B2", "B3*C1-C2");
+               ms.setCellValue("B3", 2);
+
+               ms.setCellFormula("A1", "MAX(B1:B2)");
+               ms.setCellFormula("A2", "MIN(B3,D2:F2)");
+               ms.setCellFormula("A3", "B3*C3");
+
+               // clear all the logging from the above initialisation
+               ms.getAndClearLog();
+               ms.clearAllCachedResultValues();
+               return ms;
+       }
+
+       public void testMediumComplex() {
+
+               MySheet ms = createMediumComplex();
+               // completely fresh evaluation
+               confirmEvaluate(ms, "A1", 46);
+               confirmLog(ms, new String[] {
+                       "start A1 MAX(B1:B2)",
+                               "start B1 C2-C1",
+                                       "start C2 SUM(D2:E3)",
+                                               "value D2 14", "value E2 15", "value D3 16", "value E3 17",
+                                       "end C2 62",
+                                       "start C1 SUM(D1:E2)",
+                                               "value D1 12", "value E1 13", "hit D2 14", "hit E2 15",
+                                       "end C1 54",
+                               "end B1 8",
+                               "start B2 B3*C1-C2",
+                                       "value B3 2",
+                                       "hit C1 54",
+                                       "hit C2 62",
+                               "end B2 46",
+                       "end A1 46",
+               });
+
+
+               // simple cache hit - immediate re-evaluation with no changes
+               confirmEvaluate(ms, "A1", 46);
+               confirmLog(ms, new String[] { "hit A1 46", });
+
+               // change a low level cell
+               ms.setCellValue("D1", 10);
+               confirmLog(ms, new String[] {
+                               "clear1 C1 54",
+                               "clear2 B1 8",
+                               "clear3 A1 46",
+                               "clear2 B2 46",
+               });
+               confirmEvaluate(ms, "A1", 42);
+               confirmLog(ms, new String[] {
+                       "start A1 MAX(B1:B2)",
+                               "start B1 C2-C1",
+                                       "hit C2 62",
+                                       "start C1 SUM(D1:E2)",
+                                               "hit D1 10", "hit E1 13", "hit D2 14", "hit E2 15",
+                                       "end C1 52",
+                               "end B1 10",
+                               "start B2 B3*C1-C2",
+                                       "hit B3 2",
+                                       "hit C1 52",
+                                       "hit C2 62",
+                               "end B2 42",
+                       "end A1 42",
+               });
+
+               // Reset and try changing an intermediate value
+               ms = createMediumComplex();
+               confirmEvaluate(ms, "A1", 46);
+               ms.getAndClearLog();
+
+               ms.setCellValue("B3", 3); // B3 is in the middle of the dependency tree
+               confirmLog(ms, new String[] {
+                               "clear1 B2 46",
+                               "clear2 A1 46",
+               });
+               confirmEvaluate(ms, "A1", 100);
+               confirmLog(ms, new String[] {
+                       "start A1 MAX(B1:B2)",
+                               "hit B1 8",
+                               "start B2 B3*C1-C2",
+                                       "hit B3 3",
+                                       "hit C1 54",
+                                       "hit C2 62",
+                               "end B2 100",
+                       "end A1 100",
+               });
+       }
+
+       public void testMediumComplexWithDependencyChange() {
+
+               // Changing an intermediate formula
+               MySheet ms = createMediumComplex();
+               confirmEvaluate(ms, "A1", 46);
+               ms.getAndClearLog();
+               ms.setCellFormula("B2", "B3*C2-C3"); // used to be "B3*C1-C2"
+               confirmLog(ms, new String[] {
+                       "clear1 A1 46",
+               });
+
+               confirmEvaluate(ms, "A1", 91);
+               confirmLog(ms, new String[] {
+                       "start A1 MAX(B1:B2)",
+                               "hit B1 8",
+                               "start B2 B3*C2-C3",
+                                       "hit B3 2",
+                                       "hit C2 62",
+                                       "start C3 SUM(D3:E4)",
+                                               "hit D3 16", "hit E3 17", "value D4 #BLANK#", "value E4 #BLANK#",
+                                       "end C3 33",
+                               "end B2 91",
+                       "end A1 91",
+               });
+
+               //----------------
+               // Note - From now on the demonstrated POI behaviour is not optimal
+               //----------------
+
+               // Now change a value that should no longer affect B2
+               ms.setCellValue("D1", 11);
+               confirmLog(ms, new String[] {
+                       "clear1 C1 54",
+                       // note there is no "clear2 B2 91" here because B2 doesn't depend on C1 anymore
+                       "clear2 B1 8",
+                       "clear3 A1 91",
+               });
+
+               confirmEvaluate(ms, "B2", 91);
+               confirmLog(ms, new String[] {
+                       "hit B2 91",  // further confirmation that B2 was not cleared due to changing D1 above
+               });
+
+               // things should be back to normal now
+               ms.setCellValue("D1", 11);
+               confirmLog(ms, new String[] {  });
+               confirmEvaluate(ms, "B2", 91);
+               confirmLog(ms, new String[] {
+                       "hit B2 91",
+               });
+       }
+
+       /**
+        * verifies that when updating a plain cell, depending (formula) cell cached values are cleared
+        * only when the palin cell's value actually changes
+        */
+       public void testRedundantUpdate() {
+               MySheet ms = new MySheet();
+
+               ms.setCellValue("B1", 12);
+               ms.setCellValue("C1", 13);
+               ms.setCellFormula("A1", "B1+C1");
+
+               // evaluate twice to confirm caching looks OK
+               ms.evaluateCell("A1");
+               ms.getAndClearLog();
+               confirmEvaluate(ms, "A1", 25);
+               confirmLog(ms, new String[] {
+                       "hit A1 25",
+               });
+
+               // Make redundant update, and check re-evaluation
+               ms.setCellValue("B1", 12); // value didn't change
+               confirmLog(ms, new String[] {});
+               confirmEvaluate(ms, "A1", 25);
+               confirmLog(ms, new String[] {
+                       "hit A1 25",
+               });
+
+               ms.setCellValue("B1", 11); // value changing
+               confirmLog(ms, new String[] {
+                       "clear1 A1 25", // expect consuming formula cached result to get cleared
+               });
+               confirmEvaluate(ms, "A1", 24);
+               confirmLog(ms, new String[] {
+                       "start A1 B1+C1",
+                       "hit B1 11",
+                       "hit C1 13",
+                       "end A1 24",
+               });
+       }
+
+       /**
+        * Changing any input to a formula may cause the formula to 'use' a different set of cells.
+        * Functions like INDEX and OFFSET make this effect obvious, with functions like MATCH
+        * and VLOOKUP the effect can be subtle.  The presence of error values can also produce this
+        * effect in almost every function and operator.
+        */
+       public void testSimpleWithDependencyChange() {
+
+               MySheet ms = new MySheet();
+
+               ms.setCellFormula("A1", "INDEX(C1:E1,1,B1)");
+               ms.setCellValue("B1", 1);
+               ms.setCellValue("C1", 17);
+               ms.setCellValue("D1", 18);
+               ms.setCellValue("E1", 19);
+               ms.clearAllCachedResultValues();
+               ms.getAndClearLog();
+
+               confirmEvaluate(ms, "A1", 17);
+               confirmLog(ms, new String[] {
+                       "start A1 INDEX(C1:E1,1,B1)",
+                       "value B1 1",
+                       "value C1 17",
+                       "end A1 17",
+               });
+               ms.setCellValue("B1", 2);
+               ms.getAndClearLog();
+
+               confirmEvaluate(ms, "A1", 18);
+               confirmLog(ms, new String[] {
+                       "start A1 INDEX(C1:E1,1,B1)",
+                       "hit B1 2",
+                       "value D1 18",
+                       "end A1 18",
+               });
+
+               // change C1. Note - last time A1 evaluated C1 was not used
+               ms.setCellValue("C1", 15);
+               ms.getAndClearLog();
+               confirmEvaluate(ms, "A1", 18);
+               confirmLog(ms, new String[] {
+                       "hit A1 18",
+               });
+
+               // but A1 still uses D1, so if it changes...
+               ms.setCellValue("D1", 25);
+               ms.getAndClearLog();
+               confirmEvaluate(ms, "A1", 25);
+               confirmLog(ms, new String[] {
+                       "start A1 INDEX(C1:E1,1,B1)",
+                       "hit B1 2",
+                       "hit D1 25",
+                       "end A1 25",
+               });
+
+       }
+
+       private static void confirmEvaluate(MySheet ms, String cellRefText, double expectedValue) {
+               ValueEval v = ms.evaluateCell(cellRefText);
+               assertEquals(NumberEval.class, v.getClass());
+               assertEquals(expectedValue, ((NumberEval)v).getNumberValue(), 0.0);
+       }
+
+       private static void confirmLog(MySheet ms, String[] expectedLog) {
+               String[] actualLog = ms.getAndClearLog();
+               int endIx = actualLog.length;
+               PrintStream ps = System.err;
+               if (endIx != expectedLog.length) {
+                       ps.println("Log lengths mismatch");
+                       dumpCompare(ps, expectedLog, actualLog);
+                       throw new AssertionFailedError("Log lengths mismatch");
+               }
+               for (int i=0; i< endIx; i++) {
+                       if (!actualLog[i].equals(expectedLog[i])) {
+                               String msg = "Log entry mismatch at index " + i;
+                               ps.println(msg);
+                               dumpCompare(ps, expectedLog, actualLog);
+                               throw new AssertionFailedError(msg);
+                       }
+               }
+
+       }
+
+       private static void dumpCompare(PrintStream ps, String[] expectedLog, String[] actualLog) {
+               int max = Math.max(actualLog.length, expectedLog.length);
+               ps.println("Index\tExpected\tActual");
+               for(int i=0; i<max; i++) {
+                       ps.print(i + "\t");
+                       printItem(ps, expectedLog, i);
+                       ps.print("\t");
+                       printItem(ps, actualLog, i);
+                       ps.println();
+               }
+               ps.println();
+               debugPrint(ps, actualLog);
+       }
+
+       private static void printItem(PrintStream ps, String[] ss, int index) {
+               if (index < ss.length) {
+                       ps.print(ss[index]);
+               }
+       }
+
+       private static void debugPrint(PrintStream ps, String[] log) {
+               for (int i = 0; i < log.length; i++) {
+                       ps.println('"' + log[i] + "\",");
+
+               }
+       }
+}
diff --git a/src/testcases/org/apache/poi/ss/formula/WorkbookEvaluatorTestHelper.java b/src/testcases/org/apache/poi/ss/formula/WorkbookEvaluatorTestHelper.java
new file mode 100644 (file)
index 0000000..2111cf4
--- /dev/null
@@ -0,0 +1,37 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.formula;
+
+import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+
+/**
+ * Allows tests to execute {@link WorkbookEvaluator}s and track the internal workings.
+ *  
+ * @author Josh Micich
+ */
+public final class WorkbookEvaluatorTestHelper {
+
+       private WorkbookEvaluatorTestHelper() {
+               // no instances of this class
+       }
+       
+       public static WorkbookEvaluator createEvaluator(HSSFWorkbook wb, EvaluationListener listener) {
+               return new WorkbookEvaluator(HSSFEvaluationWorkbook.create(wb), listener);
+       }
+}