Optimised the FormulaEvaluator to take cell dependencies into account

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@698047 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Josh Micich 2008-09-23 00:40:22 +00:00
parent 8791b1662d
commit 2a9b94d931
23 changed files with 1326 additions and 295 deletions

View 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>

View 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>

View File

@ -71,11 +71,9 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
public HSSFSheet getSheet(int sheetIndex) {
return _uBook.getSheetAt(sheetIndex);
}
public HSSFSheet getSheetByExternSheetIndex(int externSheetIndex) {
int sheetIndex = _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);
return _uBook.getSheetAt(sheetIndex);
}
public int convertFromExternSheetIndex(int externSheetIndex) {
return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);
}
public HSSFWorkbook getWorkbook() {
return _uBook;

View File

@ -19,6 +19,7 @@ package org.apache.poi.hssf.usermodel;
import java.util.Iterator;
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;
@ -50,14 +51,7 @@ public class HSSFFormulaEvaluator {
}
}
public HSSFFormulaEvaluator(HSSFWorkbook workbook) {
_bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook));
}
/**
* TODO for debug/test use
*/
/* package */ int getEvaluationCount() {
return _bookEvaluator.getEvaluationCount();
_bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook));
}
/**
@ -81,12 +75,23 @@ public class HSSFFormulaEvaluator {
_bookEvaluator.clearAllCachedResultValues();
}
/**
* Sets the cached value for a plain (non-formula) cell.
* Should be called whenever there are changes to individual input cells in the evaluated workbook.
* Failure to call this method after changing cell values will cause incorrect behaviour
* of the evaluate~ methods of this class
* @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 clearCachedResultValue(HSSFSheet sheet, int rowIndex, int columnIndex) {
_bookEvaluator.clearCachedResultValue(sheet, rowIndex, columnIndex);
public void setCachedPlainValue(HSSFSheet sheet, int rowIndex, int columnIndex, ValueEval value) {
_bookEvaluator.setCachedPlainValue(sheet, rowIndex, columnIndex, 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) {
_bookEvaluator.notifySetFormula(sheet, rowIndex, columnIndex);
}
/**
@ -95,7 +100,9 @@ public class HSSFFormulaEvaluator {
* the cell and also its cell type. This method should be preferred over
* evaluateInCell() when the call should not modify the contents of the
* original cell.
* @param cell
*
* @param cell may be <code>null</code> signifying that the cell is not present (or blank)
* @return <code>null</code> if the supplied cell is <code>null</code> or blank
*/
public CellValue evaluate(HSSFCell cell) {
if (cell == null) {
@ -113,6 +120,8 @@ public class HSSFFormulaEvaluator {
return new CellValue(cell.getNumericCellValue());
case HSSFCell.CELL_TYPE_STRING:
return new CellValue(cell.getRichStringCellValue().getString());
case HSSFCell.CELL_TYPE_BLANK:
return null;
}
throw new IllegalStateException("Bad cell type (" + cell.getCellType() + ")");
}

View File

@ -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;
}
}

View File

@ -17,55 +17,47 @@
package org.apache.poi.ss.formula;
import java.util.HashSet;
import java.util.Set;
/**
* Stores the parameters that identify the evaluation of one cell.<br/>
* Stores details about the current evaluation of a cell.<br/>
*/
final class CellEvaluationFrame {
private final int _sheetIndex;
private final int _srcRowNum;
private final int _srcColNum;
private final int _hashCode;
private final CellLocation _cellLocation;
private final Set _usedCells;
public CellEvaluationFrame(int sheetIndex, int srcRowNum, int srcColNum) {
if (sheetIndex < 0) {
throw new IllegalArgumentException("sheetIndex must not be negative");
}
_sheetIndex = sheetIndex;
_srcRowNum = srcRowNum;
_srcColNum = srcColNum;
_hashCode = sheetIndex + 17 * (srcRowNum + 17 * srcColNum);
public CellEvaluationFrame(CellLocation cellLoc) {
_cellLocation = cellLoc;
_usedCells = new HashSet();
}
public boolean equals(Object obj) {
CellEvaluationFrame other = (CellEvaluationFrame) obj;
if (_sheetIndex != other._sheetIndex) {
return false;
}
if (_srcRowNum != other._srcRowNum) {
return false;
}
if (_srcColNum != other._srcColNum) {
return false;
}
return true;
}
public int hashCode() {
return _hashCode;
}
/**
* @return human readable string for debug purposes
*/
public String formatAsString() {
return "R=" + _srcRowNum + " C=" + _srcColNum + " ShIx=" + _sheetIndex;
public CellLocation getCoordinates() {
return _cellLocation;
}
public String toString() {
StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" [");
sb.append(formatAsString());
sb.append(_cellLocation.formatAsString());
sb.append("]");
return sb.toString();
}
public void addUsedCell(CellLocation coordinates) {
_usedCells.add(coordinates);
}
/**
* @return never <code>null</code>, (possibly empty) array of all cells directly used while
* evaluating the formula of this frame. For non-formula cells this will always be an empty
* array;
*/
public CellLocation[] getUsedCells() {
int nItems = _usedCells.size();
if (nItems < 1) {
return CellLocation.EMPTY_ARRAY;
}
CellLocation[] result = new CellLocation[nItems];
_usedCells.toArray(result);
return result;
}
}

View File

@ -1,54 +0,0 @@
package org.apache.poi.ss.formula;
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.HSSFSheet;
final class CellEvaluator {
private final WorkbookEvaluator _bookEvaluator;
private final EvaluationTracker _tracker;
public CellEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker) {
_bookEvaluator = bookEvaluator;
_tracker = tracker;
}
/**
* Given a cell, find its type and from that create an appropriate ValueEval
* impl instance and return that. Since the cell could be an external
* reference, we need the sheet that this belongs to.
* Non existent cells are treated as empty.
*/
public ValueEval getEvalForCell(HSSFCell cell) {
if (cell == null) {
return BlankEval.INSTANCE;
}
switch (cell.getCellType()) {
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_FORMULA:
return _bookEvaluator.internalEvaluate(cell, _tracker);
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 (" + cell.getCellType() + ")");
}
public String getSheetName(HSSFSheet sheet) {
return _bookEvaluator.getSheetName(sheet);
}
}

View File

@ -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();
}
}

View File

@ -17,9 +17,13 @@
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) {
public void setValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value) {
setValue(new CellEvaluationFrame(sheetIndex, srcRowNum, srcColNum), value);
}
/* package */ void setValue(CellEvaluationFrame key, ValueEval value) {
if (_valuesByKey.containsKey(key)) {
throw new RuntimeException("Already have cached value for this cell");
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());
}
}
_valuesByKey.put(key, value);
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);
}
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)
}
}
}
}
/**
* 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);
}
}
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();
if(_evaluationListener != null) {
_evaluationListener.onClearWholeCache();
}
_entriesByLocation.clear();
_consumingCellsByUsedCells.clear();
}
public void clearValue(int sheetIndex, int rowIndex, int columnIndex) {
clearValuesRecursive(new CellEvaluationFrame(sheetIndex, rowIndex, columnIndex));
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;
}
}
private void clearValuesRecursive(CellEvaluationFrame cef) {
CellEvaluationFrame[] consumingCells = getConsumingCells(cef);
for (int i = 0; i < consumingCells.length; i++) {
clearValuesRecursive(consumingCells[i]);
}
_valuesByKey.remove(cef);
_consumingCellsByDest.remove(cef);
}
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;
}
CellEvaluationFrame[] result = new CellEvaluationFrame[nItems];
temp.toArray(result);
return result;
}
};
}

View File

@ -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);
}
}

View File

@ -35,7 +35,7 @@ public interface EvaluationWorkbook {
HSSFSheet getSheet(int sheetIndex);
HSSFSheet getSheetByExternSheetIndex(int externSheetIndex);
int convertFromExternSheetIndex(int externSheetIndex);
EvaluationName getName(NamePtg namePtg);
String resolveNameXText(NameXPtg ptg);
Ptg[] getFormulaTokens(HSSFCell cell);

View File

@ -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);
}

View 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(':');

View File

@ -22,12 +22,8 @@ import org.apache.poi.hssf.record.formula.Ref3DPtg;
import org.apache.poi.hssf.record.formula.RefPtg;
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.BlankEval;
import org.apache.poi.hssf.record.formula.eval.RefEvalBase;
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;
/**
@ -36,34 +32,19 @@ import org.apache.poi.hssf.util.CellReference;
*/
final class LazyRefEval extends RefEvalBase {
private final HSSFSheet _sheet;
private final CellEvaluator _evaluator;
private final SheetRefEvaluator _evaluator;
public LazyRefEval(RefPtg ptg, HSSFSheet sheet, CellEvaluator evaluator) {
public LazyRefEval(RefPtg ptg, SheetRefEvaluator sre) {
super(ptg.getRow(), ptg.getColumn());
_sheet = sheet;
_evaluator = evaluator;
_evaluator = sre;
}
public LazyRefEval(Ref3DPtg ptg, HSSFSheet sheet, CellEvaluator evaluator) {
public LazyRefEval(Ref3DPtg ptg, SheetRefEvaluator sre) {
super(ptg.getRow(), ptg.getColumn());
_sheet = sheet;
_evaluator = evaluator;
_evaluator = sre;
}
public ValueEval getInnerValueEval() {
int rowIx = getRow();
int colIx = getColumn();
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(getRow(), getColumn());
}
public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) {
@ -71,15 +52,14 @@ final class LazyRefEval extends RefEvalBase {
AreaI area = new OffsetArea(getRow(), getColumn(),
relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx);
return new LazyAreaEval(area, _sheet, _evaluator);
return new LazyAreaEval(area, _evaluator);
}
public String toString() {
CellReference cr = new CellReference(getRow(), getColumn());
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(cr.formatAsString());
sb.append("]");

View File

@ -0,0 +1,49 @@
/* ====================================================================
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.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFSheet;
/**
*
*
* @author Josh Micich
*/
final class SheetRefEvaluator {
private final WorkbookEvaluator _bookEvaluator;
private final EvaluationTracker _tracker;
private final HSSFSheet _sheet;
private final int _sheetIndex;
public SheetRefEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker,
EvaluationWorkbook _workbook, int sheetIndex) {
_bookEvaluator = bookEvaluator;
_tracker = tracker;
_sheet = _workbook.getSheet(sheetIndex);
_sheetIndex = sheetIndex;
}
public String getSheetName() {
return _bookEvaluator.getSheetName(_sheetIndex);
}
public ValueEval getEvalForCell(int rowIndex, int columnIndex) {
return _bookEvaluator.evaluateReference(_sheet, _sheetIndex, rowIndex, columnIndex, _tracker);
}
}

View 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();
}
public void clearCachedResultValue(HSSFSheet sheet, int rowIndex, int columnIndex) {
int sheetIndex = getSheetIndex(sheet);
_cache.clearValue(sheetIndex, rowIndex, columnIndex);
}
private int getSheetIndex(HSSFSheet sheet) {
// TODO cache sheet indexes too
return _workbook.getSheetIndex(sheet);
}
public ValueEval evaluate(HSSFCell srcCell) {
return internalEvaluate(srcCell, new EvaluationTracker(_cache));
_sheetIndexesBySheet.clear();
}
/**
* 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.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null);
}
private int getSheetIndex(HSSFSheet 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) {
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);
}
}

View 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;
}
}

View File

@ -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);

View 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());
}
}

View File

@ -17,10 +17,10 @@
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));
}
}

View File

@ -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
}
}

View File

@ -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] + "\",");
}
}
}

View File

@ -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);
}
}