<!-- 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>
<!-- 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>
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
\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
}\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
_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
* 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
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
--- /dev/null
+/* ====================================================================
+ 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
\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
+++ /dev/null
-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
--- /dev/null
+/* ====================================================================
+ 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
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;
*/
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;
- }
+
+ };
}
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;
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();
}
/**
* @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;
}
* 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);
}
+
}
\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
--- /dev/null
+/* ====================================================================
+ 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);
+}
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;
/**
*/
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;
}
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(':');
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
*/\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
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
--- /dev/null
+/* ====================================================================\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
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;
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;
*/
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;
*/
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();
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,
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) {
}
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);
+ }
}
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/>
}
suite.addTest(new TestSuite(TestEventRecordFactory.class));
suite.addTest(new TestSuite(TestModelFactory.class));
+ suite.addTest(new TestSuite(TestEvaluationCache.class));
return suite;
}
}
// 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);
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;
/**
*
}
}
+ 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
*/
// 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
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
}
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));
+ }
}
--- /dev/null
+/* ====================================================================
+ 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
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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] + "\",");
+
+ }
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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);
+ }
+}