Merged revisions 703063 via svnmerge from

https://svn.apache.org/repos/asf/poi/trunk

........
  r703063 | josh | 2008-10-08 23:07:36 -0700 (Wed, 08 Oct 2008) | 1 line
  
  Performance optimisation for EvaluationCache.  Changed to use HSSFCell as key into cache entry map.  Reduced dependency information storage for blank cells.  Introduced EvaluationSheet and EvaluationCell interfaces.  Modified HSSFFormulaEvaluator API to have simpler notify~ methods
........


git-svn-id: https://svn.apache.org/repos/asf/poi/branches/ooxml@703083 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Josh Micich 2008-10-09 07:20:46 +00:00
parent 52c6f5aa0c
commit f7acb6bb7c
32 changed files with 1640 additions and 577 deletions

View File

@ -0,0 +1,75 @@
/* ====================================================================
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.hssf.usermodel;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationSheet;
/**
* HSSF wrapper for a cell under evaluation
*
* @author Josh Micich
*/
final class HSSFEvaluationCell implements EvaluationCell {
private final EvaluationSheet _evalSheet;
private final HSSFCell _cell;
public HSSFEvaluationCell(HSSFCell cell, EvaluationSheet evalSheet) {
_cell = cell;
_evalSheet = evalSheet;
}
public HSSFEvaluationCell(HSSFCell cell) {
_cell = cell;
_evalSheet = new HSSFEvaluationSheet(cell.getSheet());
}
// Note - hashCode and equals defined according to underlying cell
public int hashCode() {
return _cell.hashCode();
}
public boolean equals(Object obj) {
return _cell == ((HSSFEvaluationCell)obj)._cell;
}
public HSSFCell getHSSFCell() {
return _cell;
}
public boolean getBooleanCellValue() {
return _cell.getBooleanCellValue();
}
public int getCellType() {
return _cell.getCellType();
}
public int getColumnIndex() {
return _cell.getCellNum();
}
public int getErrorCellValue() {
return _cell.getErrorCellValue();
}
public double getNumericCellValue() {
return _cell.getNumericCellValue();
}
public int getRowIndex() {
return _cell.getRowIndex();
}
public EvaluationSheet getSheet() {
return _evalSheet;
}
public String getStringCellValue() {
return _cell.getRichStringCellValue().getString();
}
}

View File

@ -0,0 +1,50 @@
/* ====================================================================
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.hssf.usermodel;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationSheet;
/**
* HSSF wrapper for a sheet under evaluation
*
* @author Josh Micich
*/
final class HSSFEvaluationSheet implements EvaluationSheet {
private final HSSFSheet _hs;
public HSSFEvaluationSheet(HSSFSheet hs) {
_hs = hs;
}
public HSSFSheet getHSSFSheet() {
return _hs;
}
public EvaluationCell getCell(int rowIndex, int columnIndex) {
HSSFRow row = _hs.getRow(rowIndex);
if (row == null) {
return null;
}
HSSFCell cell = row.getCell(columnIndex);
if (cell == null) {
return null;
}
return new HSSFEvaluationCell(cell, this);
}
}

View File

@ -1,3 +1,20 @@
/* ====================================================================
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.hssf.usermodel; package org.apache.poi.hssf.usermodel;
import org.apache.poi.hssf.model.HSSFFormulaParser; import org.apache.poi.hssf.model.HSSFFormulaParser;
@ -8,12 +25,12 @@ import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NamePtg;
import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.record.formula.NameXPtg;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationName; import org.apache.poi.ss.formula.EvaluationName;
import org.apache.poi.ss.formula.EvaluationSheet;
import org.apache.poi.ss.formula.EvaluationWorkbook; import org.apache.poi.ss.formula.EvaluationWorkbook;
import org.apache.poi.ss.formula.FormulaParsingWorkbook; import org.apache.poi.ss.formula.FormulaParsingWorkbook;
import org.apache.poi.ss.formula.FormulaRenderingWorkbook; import org.apache.poi.ss.formula.FormulaRenderingWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
/** /**
* Internal POI use only * Internal POI use only
@ -45,8 +62,8 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
return _iBook.getExternalSheetIndex(workbookName, sheetName); return _iBook.getExternalSheetIndex(workbookName, sheetName);
} }
public EvaluationName getName(int index) { public NameXPtg getNameXPtg(String name) {
return new Name(_iBook.getNameRecord(index), index); return _iBook.getNameXPtg(name);
} }
public EvaluationName getName(String name) { public EvaluationName getName(String name) {
@ -59,7 +76,8 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
return null; return null;
} }
public int getSheetIndex(Sheet sheet) { public int getSheetIndex(EvaluationSheet evalSheet) {
HSSFSheet sheet = ((HSSFEvaluationSheet)evalSheet).getHSSFSheet();
return _uBook.getSheetIndex(sheet); return _uBook.getSheetIndex(sheet);
} }
public int getSheetIndex(String sheetName) { public int getSheetIndex(String sheetName) {
@ -70,16 +88,8 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
return _uBook.getSheetName(sheetIndex); return _uBook.getSheetName(sheetIndex);
} }
public int getNameIndex(String name) { public EvaluationSheet getSheet(int sheetIndex) {
return _uBook.getNameIndex(name); return new HSSFEvaluationSheet(_uBook.getSheetAt(sheetIndex));
}
public NameXPtg getNameXPtg(String name) {
return _iBook.getNameXPtg(name);
}
public Sheet getSheet(int sheetIndex) {
return _uBook.getSheetAt(sheetIndex);
} }
public int convertFromExternSheetIndex(int externSheetIndex) { public int convertFromExternSheetIndex(int externSheetIndex) {
return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex); return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);
@ -89,10 +99,6 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
return _iBook.getExternalSheet(externSheetIndex); return _iBook.getExternalSheet(externSheetIndex);
} }
public HSSFWorkbook getWorkbook() {
return _uBook;
}
public String resolveNameXText(NameXPtg n) { public String resolveNameXText(NameXPtg n) {
return _iBook.resolveNameXText(n.getSheetRefIndex(), n.getNameIndex()); return _iBook.resolveNameXText(n.getSheetRefIndex(), n.getNameIndex());
} }
@ -107,15 +113,15 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
int ix = namePtg.getIndex(); int ix = namePtg.getIndex();
return new Name(_iBook.getNameRecord(ix), ix); return new Name(_iBook.getNameRecord(ix), ix);
} }
public Ptg[] getFormulaTokens(Cell cell) { public Ptg[] getFormulaTokens(EvaluationCell evalCell) {
HSSFCell cell = ((HSSFEvaluationCell)evalCell).getHSSFCell();
if (false) { if (false) {
// re-parsing the formula text also works, but is a waste of time // re-parsing the formula text also works, but is a waste of time
// It is useful from time to time to run all unit tests with this code // It is useful from time to time to run all unit tests with this code
// to make sure that all formulas POI can evaluate can also be parsed. // to make sure that all formulas POI can evaluate can also be parsed.
return HSSFFormulaParser.parse(cell.getCellFormula(), _uBook); return HSSFFormulaParser.parse(cell.getCellFormula(), _uBook);
} }
HSSFCell hCell = (HSSFCell) cell; FormulaRecord fr = ((FormulaRecordAggregate) cell.getCellValueRecord()).getFormulaRecord();
FormulaRecord fr = ((FormulaRecordAggregate) hCell.getCellValueRecord()).getFormulaRecord();
return fr.getParsedExpression(); return fr.getParsedExpression();
} }
@ -128,23 +134,18 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
_nameRecord = nameRecord; _nameRecord = nameRecord;
_index = index; _index = index;
} }
public Ptg[] getNameDefinition() { public Ptg[] getNameDefinition() {
return _nameRecord.getNameDefinition(); return _nameRecord.getNameDefinition();
} }
public String getNameText() { public String getNameText() {
return _nameRecord.getNameText(); return _nameRecord.getNameText();
} }
public boolean hasFormula() { public boolean hasFormula() {
return _nameRecord.hasFormula(); return _nameRecord.hasFormula();
} }
public boolean isFunctionName() { public boolean isFunctionName() {
return _nameRecord.isFunctionName(); return _nameRecord.isFunctionName();
} }
public boolean isRange() { public boolean isRange() {
return _nameRecord.hasFormula(); // TODO - is this right? return _nameRecord.hasFormula(); // TODO - is this right?
} }

View File

@ -19,7 +19,6 @@ package org.apache.poi.hssf.usermodel;
import java.util.Iterator; 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.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval; 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.NumberEval;
@ -29,7 +28,6 @@ import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment;
import org.apache.poi.ss.formula.WorkbookEvaluator; import org.apache.poi.ss.formula.WorkbookEvaluator;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.Sheet;
/** /**
* Evaluates formula cells.<p/> * Evaluates formula cells.<p/>
@ -86,7 +84,9 @@ public class HSSFFormulaEvaluator /* almost implements FormulaEvaluator */ {
/** /**
* Should be called whenever there are major changes (e.g. moving sheets) to input cells * Should be called whenever there are major changes (e.g. moving sheets) to input cells
* in the evaluated workbook. * in the evaluated workbook. If performance is not critical, a single call to this method
* may be used instead of many specific calls to the notify~ methods.
*
* Failure to call this method after changing cell values will cause incorrect behaviour * Failure to call this method after changing cell values will cause incorrect behaviour
* of the evaluate~ methods of this class * of the evaluate~ methods of this class
*/ */
@ -94,23 +94,22 @@ public class HSSFFormulaEvaluator /* almost implements FormulaEvaluator */ {
_bookEvaluator.clearAllCachedResultValues(); _bookEvaluator.clearAllCachedResultValues();
} }
/** /**
* Sets the cached value for a plain (non-formula) cell. * Should be called to tell the cell value cache that the specified (value or formula) cell
* Should be called whenever there are changes to individual input cells in the evaluated workbook. * has changed.
* Failure to call this method after changing cell values will cause incorrect behaviour * Failure to call this method after changing cell values will cause incorrect behaviour
* of the evaluate~ methods of this class * 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 setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value) { public void notifyUpdateCell(HSSFCell cell) {
_bookEvaluator.setCachedPlainValue(sheet, rowIndex, columnIndex, value); _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell(cell));
} }
/** /**
* Should be called to tell the cell value cache that the specified cell has just become a * Should be called to tell the cell value cache that the specified cell has just been
* formula cell, or the formula text has changed * deleted.
* Failure to call this method after changing cell values will cause incorrect behaviour
* of the evaluate~ methods of this class
*/ */
public void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex) { public void notifyDeleteCell(HSSFCell cell) {
_bookEvaluator.notifySetFormula(sheet, rowIndex, columnIndex); _bookEvaluator.notifyDeleteCell(new HSSFEvaluationCell(cell));
} }
/** /**
@ -273,7 +272,7 @@ public class HSSFFormulaEvaluator /* almost implements FormulaEvaluator */ {
* @param eval * @param eval
*/ */
private CellValue evaluateFormulaCellValue(Cell cell) { private CellValue evaluateFormulaCellValue(Cell cell) {
ValueEval eval = _bookEvaluator.evaluate(cell); ValueEval eval = _bookEvaluator.evaluate(new HSSFEvaluationCell((HSSFCell)cell));
if (eval instanceof NumberEval) { if (eval instanceof NumberEval) {
NumberEval ne = (NumberEval) eval; NumberEval ne = (NumberEval) eval;
return new CellValue(ne.getNumberValue()); return new CellValue(ne.getNumberValue());

View File

@ -17,42 +17,44 @@
package org.apache.poi.ss.formula; package org.apache.poi.ss.formula;
import java.util.HashSet;
import java.util.Set;
import org.apache.poi.hssf.record.formula.eval.BlankEval; 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.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval; 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.NumberEval;
import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.StringEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.ss.formula.IEvaluationListener.ICacheEntry;
/** /**
* Stores the parameters that identify the evaluation of one cell.<br/> * Stores the parameters that identify the evaluation of one cell.<br/>
*/ */
final class CellCacheEntry { abstract class CellCacheEntry implements ICacheEntry {
private ValueEval _value; public static final CellCacheEntry[] EMPTY_ARRAY = { };
private final Set _consumingCells;
private CellLocation[] _usedCells;
private boolean _isPlainValueCell;
public CellCacheEntry() { private final FormulaCellCacheEntrySet _consumingCells;
_consumingCells = new HashSet(); private ValueEval _value;
protected CellCacheEntry() {
_consumingCells = new FormulaCellCacheEntrySet();
} }
public boolean updatePlainValue(ValueEval value) { protected final void clearValue() {
boolean wasChanged = false; _value = null;
if (!_isPlainValueCell) { }
wasChanged = true;
public final boolean updateValue(ValueEval value) {
if (value == null) {
throw new IllegalArgumentException("Did not expect to update to null");
} }
if (!areValuesEqual(_value, value)) { boolean result = !areValuesEqual(_value, value);
wasChanged = true;
}
_isPlainValueCell = true;
_value = value; _value = value;
_usedCells = null; return result;
return wasChanged;
} }
private boolean areValuesEqual(ValueEval a, ValueEval b) { public final ValueEval getValue() {
return _value;
}
private static boolean areValuesEqual(ValueEval a, ValueEval b) {
if (a == null) { if (a == null) {
return false; return false;
} }
@ -78,41 +80,58 @@ final class CellCacheEntry {
} }
throw new IllegalStateException("Unexpected value class (" + cls.getName() + ")"); throw new IllegalStateException("Unexpected value class (" + cls.getName() + ")");
} }
public void setFormulaResult(ValueEval value, CellLocation[] usedCells) {
_isPlainValueCell = false; public final void addConsumingCell(FormulaCellCacheEntry cellLoc) {
_value = value;
_usedCells = usedCells;
}
public void addConsumingCell(CellLocation cellLoc) {
_consumingCells.add(cellLoc); _consumingCells.add(cellLoc);
} }
public CellLocation[] getConsumingCells() { public final FormulaCellCacheEntry[] getConsumingCells() {
int nItems = _consumingCells.size(); return _consumingCells.toArray();
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 final void clearConsumingCell(FormulaCellCacheEntry cce) {
public ValueEval getValue() { if(!_consumingCells.remove(cce)) {
ValueEval result = _value; throw new IllegalStateException("Specified formula cell is not consumed by this cell");
if (result == null) {
if (_isPlainValueCell) {
throw new IllegalStateException("Plain value cell should always have a value");
}
} }
return result;
} }
public final void recurseClearCachedFormulaResults(IEvaluationListener listener) {
if (listener == null) {
recurseClearCachedFormulaResults();
} else {
listener.onClearCachedValue(this);
recurseClearCachedFormulaResults(listener, 1);
}
}
/**
* 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
*/
protected final void recurseClearCachedFormulaResults() {
FormulaCellCacheEntry[] formulaCells = getConsumingCells();
for (int i = 0; i < formulaCells.length; i++) {
FormulaCellCacheEntry fc = formulaCells[i];
fc.clearFormulaEntry();
fc.recurseClearCachedFormulaResults();
}
}
/**
* Identical to {@link #recurseClearCachedFormulaResults()} except for the listener call-backs
*/
protected final void recurseClearCachedFormulaResults(IEvaluationListener listener, int depth) {
FormulaCellCacheEntry[] formulaCells = getConsumingCells();
listener.sortDependentCachedValues(formulaCells);
for (int i = 0; i < formulaCells.length; i++) {
FormulaCellCacheEntry fc = formulaCells[i];
listener.onClearDependentCachedValue(fc, depth);
fc.clearFormulaEntry();
fc.recurseClearCachedFormulaResults(listener, depth+1);
}
}
} }

View File

@ -20,44 +20,58 @@ package org.apache.poi.ss.formula;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
/** /**
* Stores details about the current evaluation of a cell.<br/> * Stores details about the current evaluation of a cell.<br/>
*/ */
final class CellEvaluationFrame { final class CellEvaluationFrame {
private final CellLocation _cellLocation; private final FormulaCellCacheEntry _cce;
private final Set _usedCells; private final Set _sensitiveInputCells;
private FormulaUsedBlankCellSet _usedBlankCellGroup;
public CellEvaluationFrame(CellLocation cellLoc) { public CellEvaluationFrame(FormulaCellCacheEntry cce) {
_cellLocation = cellLoc; _cce = cce;
_usedCells = new HashSet(); _sensitiveInputCells = new HashSet();
} }
public CellLocation getCoordinates() { public CellCacheEntry getCCE() {
return _cellLocation; return _cce;
} }
public String toString() { public String toString() {
StringBuffer sb = new StringBuffer(64); StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" ["); sb.append(getClass().getName()).append(" [");
sb.append(_cellLocation.formatAsString());
sb.append("]"); sb.append("]");
return sb.toString(); return sb.toString();
} }
public void addUsedCell(CellLocation coordinates) { /**
_usedCells.add(coordinates); * @param inputCell a cell directly used by the formula of this evaluation frame
*/
public void addSensitiveInputCell(CellCacheEntry inputCell) {
_sensitiveInputCells.add(inputCell);
} }
/** /**
* @return never <code>null</code>, (possibly empty) array of all cells directly used while * @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 * evaluating the formula of this frame.
* array;
*/ */
public CellLocation[] getUsedCells() { private CellCacheEntry[] getSensitiveInputCells() {
int nItems = _usedCells.size(); int nItems = _sensitiveInputCells.size();
if (nItems < 1) { if (nItems < 1) {
return CellLocation.EMPTY_ARRAY; return CellCacheEntry.EMPTY_ARRAY;
} }
CellLocation[] result = new CellLocation[nItems]; CellCacheEntry[] result = new CellCacheEntry[nItems];
_usedCells.toArray(result); _sensitiveInputCells.toArray(result);
return result; return result;
} }
public void addUsedBlankCell(int bookIndex, int sheetIndex, int rowIndex, int columnIndex) {
if (_usedBlankCellGroup == null) {
_usedBlankCellGroup = new FormulaUsedBlankCellSet();
}
_usedBlankCellGroup.addCell(bookIndex, sheetIndex, rowIndex, columnIndex);
}
public void updateFormulaResult(ValueEval result) {
_cce.updateFormulaResult(result, getSensitiveInputCells(), _usedBlankCellGroup);
}
} }

View File

@ -1,92 +0,0 @@
/* ====================================================================
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.util.CellReference;
/**
* Stores the parameters that identify the evaluation of one cell.<br/>
*/
final class CellLocation {
public static final CellLocation[] EMPTY_ARRAY = { };
private final int _bookIx;
private final int _sheetIndex;
private final int _rowIndex;
private final int _columnIndex;
private final int _hashCode;
public CellLocation(int bookIx, int sheetIndex, int rowIndex, int columnIndex) {
if (sheetIndex < 0) {
throw new IllegalArgumentException("sheetIndex must not be negative");
}
_bookIx = bookIx;
_sheetIndex = sheetIndex;
_rowIndex = rowIndex;
_columnIndex = columnIndex;
_hashCode = _bookIx + 17 * (sheetIndex + 17 * (rowIndex + 17 * columnIndex));
}
public int getBookIndex() {
return _bookIx;
}
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 (getRowIndex() != other.getRowIndex()) {
return false;
}
if (getColumnIndex() != other.getColumnIndex()) {
return false;
}
if (getSheetIndex() != other.getSheetIndex()) {
return false;
}
if (getBookIndex() != other.getBookIndex()) {
return false;
}
return true;
}
public int hashCode() {
return _hashCode;
}
/**
* @return human readable string for debug purposes
*/
public String formatAsString() {
CellReference cr = new CellReference(_rowIndex, _columnIndex, false, false);
return "ShIx=" + getSheetIndex() + " " + cr.formatAsString();
}
public String toString() {
StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" [");
sb.append(formatAsString());
sb.append("]");
return sb.toString();
}
}

View File

@ -17,16 +17,17 @@
package org.apache.poi.ss.formula; package org.apache.poi.ss.formula;
import java.util.Arrays; import org.apache.poi.hssf.record.formula.eval.BlankEval;
import java.util.Collections; import org.apache.poi.hssf.record.formula.eval.BoolEval;
import java.util.Comparator; import org.apache.poi.hssf.record.formula.eval.ErrorEval;
import java.util.HashMap; import org.apache.poi.hssf.record.formula.eval.NumberEval;
import java.util.HashSet; import org.apache.poi.hssf.record.formula.eval.StringEval;
import java.util.Map;
import java.util.Set;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
import org.apache.poi.ss.formula.FormulaCellCache.IEntryOperation;
import org.apache.poi.ss.formula.FormulaUsedBlankCellSet.BookSheetKey;
import org.apache.poi.ss.formula.PlainCellCache.Loc;
/** /**
* Performance optimisation for {@link HSSFFormulaEvaluator}. This class stores previously * Performance optimisation for {@link HSSFFormulaEvaluator}. This class stores previously
@ -38,152 +39,138 @@ import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
*/ */
final class EvaluationCache { final class EvaluationCache {
private final Map _entriesByLocation; private final PlainCellCache _plainCellCache;
private final Map _consumingCellsByUsedCells; private final FormulaCellCache _formulaCellCache;
/** only used for testing. <code>null</code> otherwise */ /** only used for testing. <code>null</code> otherwise */
private final IEvaluationListener _evaluationListener; final IEvaluationListener _evaluationListener;
/* package */EvaluationCache(IEvaluationListener evaluationListener) { /* package */EvaluationCache(IEvaluationListener evaluationListener) {
_evaluationListener = evaluationListener; _evaluationListener = evaluationListener;
_entriesByLocation = new HashMap(); _plainCellCache = new PlainCellCache();
_consumingCellsByUsedCells = new HashMap(); _formulaCellCache = new FormulaCellCache();
} }
/** public void notifyUpdateCell(int bookIndex, int sheetIndex, EvaluationCell cell) {
* @param cellLoc never <code>null</code> FormulaCellCacheEntry fcce = _formulaCellCache.get(cell);
* @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();
}
/** Loc loc = new Loc(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex());
* @param cellLoc PlainValueCellCacheEntry pcce = _plainCellCache.get(loc);
* @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 (cell.getCellType() == HSSFCell.CELL_TYPE_FORMULA) {
if (existingEntry != null && existingEntry.getValue() != null) { if (fcce == null) {
if (isPlainValue) { if (pcce == null) {
// can set a plain cell cached value any time updateAnyBlankReferencingFormulas(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex());
} else if (value == null) { }
// or clear the cached value of a formula cell at any time fcce = new FormulaCellCacheEntry();
_formulaCellCache.put(cell, fcce);
} else { } else {
// but a formula cached value would only be getting updated if the cache didn't have a value to start with fcce.recurseClearCachedFormulaResults(_evaluationListener);
throw new IllegalStateException("Already have cached value for this cell: " fcce.clearFormulaEntry();
+ cellLoc.formatAsString());
} }
} if (pcce == null) {
sortCellLocationsForLogging(usedCells); // was formula cell before - no change of type
CellCacheEntry entry = getEntry(cellLoc); } else {
CellLocation[] consumingFormulaCells = entry.getConsumingCells(); // changing from plain cell to formula cell
CellLocation[] prevUsedCells = entry.getUsedCells(); pcce.recurseClearCachedFormulaResults(_evaluationListener);
if (isPlainValue) { _plainCellCache.remove(loc);
if(!entry.updatePlainValue(value)) {
return;
} }
} else { } else {
entry.setFormulaResult(value, usedCells); ValueEval value = WorkbookEvaluator.getValueFromNonFormulaCell(cell);
for (int i = 0; i < usedCells.length; i++) { if (pcce == null) {
getEntry(usedCells[i]).addConsumingCell(cellLoc); if (fcce == null) {
} updateAnyBlankReferencingFormulas(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex());
} }
// need to tell all cells that were previously used, but no longer are, pcce = new PlainValueCellCacheEntry(value);
// that they are not consumed by this cell any more _plainCellCache.put(loc, pcce);
unlinkConsumingCells(prevUsedCells, usedCells, cellLoc);
// clear all cells that directly or indirectly depend on this one
recurseClearCachedFormulaResults(consumingFormulaCells, 0);
}
/**
* This method sorts the supplied cellLocs so that the order of call-backs to the evaluation
* listener is more deterministic
*/
private void sortCellLocationsForLogging(CellLocation[] cellLocs) {
if (_evaluationListener == null) {
// optimisation - don't bother sorting if there is no listener.
} else {
Arrays.sort(cellLocs, CellLocationComparator);
}
}
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) { if (_evaluationListener != null) {
//TODO _evaluationListener.onUnconsume(prevUsed.getSheetIndex(), etc) _evaluationListener.onReadPlainValue(sheetIndex, cell.getRowIndex(), cell.getColumnIndex(), pcce);
}
} else {
if(pcce.updateValue(value)) {
pcce.recurseClearCachedFormulaResults(_evaluationListener);
} }
} }
if (fcce == null) {
// was plain cell before - no change of type
} else {
// was formula cell before - now a plain value
_formulaCellCache.remove(cell);
fcce.setSensitiveInputCells(null);
fcce.recurseClearCachedFormulaResults(_evaluationListener);
}
} }
} }
/** private void updateAnyBlankReferencingFormulas(int bookIndex, int sheetIndex,
* Calls formulaCell.setFormulaResult(null, null) recursively all the way up the tree of final int rowIndex, final int columnIndex) {
* dependencies. Calls usedCell.clearConsumingCell(fc) for each child of a cell that is final BookSheetKey bsk = new BookSheetKey(bookIndex, sheetIndex);
* cleared along the way. _formulaCellCache.applyOperation(new IEntryOperation() {
* @param formulaCells
*/ public void processEntry(FormulaCellCacheEntry entry) {
private void recurseClearCachedFormulaResults(CellLocation[] formulaCells, int depth) { entry.notifyUpdatedBlankCell(bsk, rowIndex, columnIndex, _evaluationListener);
sortCellLocationsForLogging(formulaCells); }
int nextDepth = depth+1; });
for (int i = 0; i < formulaCells.length; i++) { }
CellLocation fc = formulaCells[i];
CellCacheEntry formulaCell = getEntry(fc); public PlainValueCellCacheEntry getPlainValueEntry(int bookIndex, int sheetIndex,
CellLocation[] usedCells = formulaCell.getUsedCells(); int rowIndex, int columnIndex, ValueEval value) {
if (usedCells != null) {
for (int j = 0; j < usedCells.length; j++) { Loc loc = new Loc(bookIndex, sheetIndex, rowIndex, columnIndex);
CellCacheEntry usedCell = getEntry(usedCells[j]); PlainValueCellCacheEntry result = _plainCellCache.get(loc);
usedCell.clearConsumingCell(fc); if (result == null) {
} result = new PlainValueCellCacheEntry(value);
_plainCellCache.put(loc, result);
if (_evaluationListener != null) {
_evaluationListener.onReadPlainValue(sheetIndex, rowIndex, columnIndex, result);
}
} else {
// TODO - if we are confident that this sanity check is not required, we can remove 'value' from plain value cache entry
if (!areValuesEqual(result.getValue(), value)) {
throw new IllegalStateException("value changed");
} }
if (_evaluationListener != null) { if (_evaluationListener != null) {
ValueEval value = formulaCell.getValue(); _evaluationListener.onCacheHit(sheetIndex, rowIndex, columnIndex, value);
_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; return result;
} }
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 (a == BlankEval.INSTANCE) {
return b == a;
}
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 FormulaCellCacheEntry getOrCreateFormulaCellEntry(EvaluationCell cell) {
FormulaCellCacheEntry result = _formulaCellCache.get(cell);
if (result == null) {
result = new FormulaCellCacheEntry();
_formulaCellCache.put(cell, result);
}
return result;
}
/** /**
* Should be called whenever there are changes to input cells in the evaluated workbook. * Should be called whenever there are changes to input cells in the evaluated workbook.
*/ */
@ -191,34 +178,28 @@ final class EvaluationCache {
if(_evaluationListener != null) { if(_evaluationListener != null) {
_evaluationListener.onClearWholeCache(); _evaluationListener.onClearWholeCache();
} }
_entriesByLocation.clear(); _plainCellCache.clear();
_consumingCellsByUsedCells.clear(); _formulaCellCache.clear();
} }
public void notifyDeleteCell(int bookIndex, int sheetIndex, EvaluationCell cell) {
if (cell.getCellType() == HSSFCell.CELL_TYPE_FORMULA) {
private static final Comparator CellLocationComparator = new Comparator() { FormulaCellCacheEntry fcce = _formulaCellCache.remove(cell);
if (fcce == null) {
public int compare(Object a, Object b) { // formula cell has not been evaluated yet
CellLocation clA = (CellLocation) a; } else {
CellLocation clB = (CellLocation) b; fcce.setSensitiveInputCells(null);
fcce.recurseClearCachedFormulaResults(_evaluationListener);
int cmp;
cmp = clA.getBookIndex() - clB.getBookIndex();
if (cmp != 0) {
return cmp;
} }
cmp = clA.getSheetIndex() - clB.getSheetIndex(); } else {
if (cmp != 0) { Loc loc = new Loc(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex());
return cmp; PlainValueCellCacheEntry pcce = _plainCellCache.get(loc);
}
cmp = clA.getRowIndex() - clB.getRowIndex();
if (cmp != 0) {
return cmp;
}
cmp = clA.getColumnIndex() - clB.getColumnIndex();
return cmp; if (pcce == null) {
// cache entry doesn't exist. nothing to do
} else {
pcce.recurseClearCachedFormulaResults(_evaluationListener);
}
} }
}
};
} }

View File

@ -0,0 +1,42 @@
/* ====================================================================
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;
/**
* Abstracts a cell for the purpose of formula evaluation. This interface represents both formula
* and non-formula cells.<br/>
*
* Implementors of this class must implement {@link #hashCode()} and {@link #equals(Object)}
* to provide an <em>identity</em> relationship based on the underlying HSSF or XSSF cell <p/>
*
* For POI internal use only
*
* @author Josh Micich
*/
public interface EvaluationCell {
// consider method Object getUnderlyingCell() to reduce memory consumption in formula cell cache
EvaluationSheet getSheet();
int getRowIndex();
int getColumnIndex();
int getCellType();
double getNumericCellValue();
String getStringCellValue();
boolean getBooleanCellValue();
int getErrorCellValue();
}

View File

@ -0,0 +1,33 @@
/* ====================================================================
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;
/**
* Abstracts a sheet for the purpose of formula evaluation.<br/>
*
* For POI internal use only
*
* @author Josh Micich
*/
public interface EvaluationSheet {
/**
* @return <code>null</code> if there is no cell at the specified coordinates
*/
EvaluationCell getCell(int rowIndex, int columnIndex);
}

View File

@ -22,7 +22,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.BlankEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFCell;
@ -36,7 +36,7 @@ import org.apache.poi.hssf.usermodel.HSSFCell;
* @author Josh Micich * @author Josh Micich
*/ */
final class EvaluationTracker { final class EvaluationTracker {
// TODO - consider deleting this class and letting CellEvaluationFrame take care of itself
private final List _evaluationFrames; private final List _evaluationFrames;
private final Set _currentlyEvaluatingCells; private final Set _currentlyEvaluatingCells;
private final EvaluationCache _cache; private final EvaluationCache _cache;
@ -55,42 +55,46 @@ final class EvaluationTracker {
* continue evaluation of the specified cell, and also be sure to call * continue evaluation of the specified cell, and also be sure to call
* <tt>endEvaluate()</tt> when complete.<br/> * <tt>endEvaluate()</tt> when complete.<br/>
* *
* In the case of a <code>false</code> return code, the caller should * In the case of a <code>null</code> return code, the caller should
* return an evaluation result of * return an evaluation result of
* <tt>ErrorEval.CIRCULAR_REF_ERROR<tt>, and not call <tt>endEvaluate()</tt>. * <tt>ErrorEval.CIRCULAR_REF_ERROR<tt>, and not call <tt>endEvaluate()</tt>.
* <br/> * <br/>
* @return <code>true</code> if the specified cell has not been visited yet in the current * @return <code>false</code> if the specified cell is already being evaluated
* evaluation. <code>false</code> if the specified cell is already being evaluated.
*/ */
public ValueEval startEvaluate(CellLocation cellLoc) { public boolean startEvaluate(FormulaCellCacheEntry cce) {
if (cellLoc == null) { if (cce == null) {
throw new IllegalArgumentException("cellLoc must not be null"); throw new IllegalArgumentException("cellLoc must not be null");
} }
if (_currentlyEvaluatingCells.contains(cellLoc)) { if (_currentlyEvaluatingCells.contains(cce)) {
return ErrorEval.CIRCULAR_REF_ERROR; return false;
} }
ValueEval result = _cache.getValue(cellLoc); _currentlyEvaluatingCells.add(cce);
if (result == null) { _evaluationFrames.add(new CellEvaluationFrame(cce));
_currentlyEvaluatingCells.add(cellLoc); return true;
_evaluationFrames.add(new CellEvaluationFrame(cellLoc)); }
public void updateCacheResult(ValueEval result) {
int nFrames = _evaluationFrames.size();
if (nFrames < 1) {
throw new IllegalStateException("Call to endEvaluate without matching call to startEvaluate");
} }
return result; CellEvaluationFrame frame = (CellEvaluationFrame) _evaluationFrames.get(nFrames-1);
frame.updateFormulaResult(result);
} }
/** /**
* Notifies this evaluation tracker that the evaluation of the specified * Notifies this evaluation tracker that the evaluation of the specified cell is complete. <p/>
* cell is complete. <p/>
* *
* Every successful call to <tt>startEvaluate</tt> must be followed by a * Every successful call to <tt>startEvaluate</tt> must be followed by a call to <tt>endEvaluate</tt> (recommended in a finally block) to enable
* call to <tt>endEvaluate</tt> (recommended in a finally block) to enable
* proper tracking of which cells are being evaluated at any point in time.<p/> * proper tracking of which cells are being evaluated at any point in time.<p/>
* *
* Assuming a well behaved client, parameters to this method would not be * Assuming a well behaved client, parameters to this method would not be
* required. However, they have been included to assert correct behaviour, * required. However, they have been included to assert correct behaviour,
* and form more meaningful error messages. * and form more meaningful error messages.
* @param result
*/ */
public void endEvaluate(CellLocation cellLoc, ValueEval result, boolean isPlainValueCell) { public void endEvaluate(CellCacheEntry cce) {
int nFrames = _evaluationFrames.size(); int nFrames = _evaluationFrames.size();
if (nFrames < 1) { if (nFrames < 1) {
@ -99,32 +103,40 @@ final class EvaluationTracker {
nFrames--; nFrames--;
CellEvaluationFrame frame = (CellEvaluationFrame) _evaluationFrames.get(nFrames); CellEvaluationFrame frame = (CellEvaluationFrame) _evaluationFrames.get(nFrames);
CellLocation coordinates = frame.getCoordinates(); if (cce != frame.getCCE()) {
if (!coordinates.equals(cellLoc)) { throw new IllegalStateException("Wrong cell specified. ");
throw new RuntimeException("Wrong cell specified. "
+ "Corresponding startEvaluate() call was for cell {"
+ coordinates.formatAsString() + "} this endEvaluate() call is for cell {"
+ cellLoc.formatAsString() + "}");
} }
// else - no problems so pop current frame // else - no problems so pop current frame
_evaluationFrames.remove(nFrames); _evaluationFrames.remove(nFrames);
_currentlyEvaluatingCells.remove(coordinates); _currentlyEvaluatingCells.remove(cce);
// 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 public void acceptFormulaDependency(CellCacheEntry cce) {
* <tt>usedCell<tt> // Tell the currently evaluating cell frame that it has a dependency on the specified
* @param usedCell location of cell which is referenced (and used) by the current cell.
*/
public void acceptDependency(CellLocation usedCell) {
int prevFrameIndex = _evaluationFrames.size()-1; int prevFrameIndex = _evaluationFrames.size()-1;
if (prevFrameIndex < 0) { if (prevFrameIndex < 0) {
throw new IllegalStateException("Call to acceptDependency without prior call to startEvaluate"); // Top level frame, there is no 'cell' above this frame that is using the current cell
} else {
CellEvaluationFrame consumingFrame = (CellEvaluationFrame) _evaluationFrames.get(prevFrameIndex);
consumingFrame.addSensitiveInputCell(cce);
} }
CellEvaluationFrame consumingFrame = (CellEvaluationFrame) _evaluationFrames.get(prevFrameIndex);
consumingFrame.addUsedCell(usedCell);
} }
public void acceptPlainValueDependency(int bookIndex, int sheetIndex,
int rowIndex, int columnIndex, ValueEval value) {
// Tell the currently evaluating cell frame that it has a dependency on the specified
int prevFrameIndex = _evaluationFrames.size() - 1;
if (prevFrameIndex < 0) {
// Top level frame, there is no 'cell' above this frame that is using the current cell
} else {
CellEvaluationFrame consumingFrame = (CellEvaluationFrame) _evaluationFrames.get(prevFrameIndex);
if (value == BlankEval.INSTANCE) {
consumingFrame.addUsedBlankCell(bookIndex, sheetIndex, rowIndex, columnIndex);
} else {
PlainValueCellCacheEntry cce = _cache.getPlainValueEntry(bookIndex, sheetIndex,
rowIndex, columnIndex, value);
consumingFrame.addSensitiveInputCell(cce);
}
}
}
} }

View File

@ -20,8 +20,7 @@ package org.apache.poi.ss.formula;
import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NamePtg;
import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.record.formula.NameXPtg;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
/** /**
* Abstracts a workbook for the purpose of formula evaluation.<br/> * Abstracts a workbook for the purpose of formula evaluation.<br/>
* *
@ -34,10 +33,10 @@ public interface EvaluationWorkbook {
/** /**
* @return -1 if the specified sheet is from a different book * @return -1 if the specified sheet is from a different book
*/ */
int getSheetIndex(Sheet sheet); int getSheetIndex(EvaluationSheet sheet);
int getSheetIndex(String sheetName); int getSheetIndex(String sheetName);
Sheet getSheet(int sheetIndex); EvaluationSheet getSheet(int sheetIndex);
/** /**
* @return <code>null</code> if externSheetIndex refers to a sheet inside the current workbook * @return <code>null</code> if externSheetIndex refers to a sheet inside the current workbook
@ -46,7 +45,7 @@ public interface EvaluationWorkbook {
int convertFromExternSheetIndex(int externSheetIndex); int convertFromExternSheetIndex(int externSheetIndex);
EvaluationName getName(NamePtg namePtg); EvaluationName getName(NamePtg namePtg);
String resolveNameXText(NameXPtg ptg); String resolveNameXText(NameXPtg ptg);
Ptg[] getFormulaTokens(Cell cell); Ptg[] getFormulaTokens(EvaluationCell cell);
class ExternalSheet { class ExternalSheet {
private final String _workbookName; private final String _workbookName;

View File

@ -0,0 +1,73 @@
/* ====================================================================
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.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
*
* @author Josh Micich
*/
final class FormulaCellCache {
static interface IEntryOperation {
void processEntry(FormulaCellCacheEntry entry);
}
private Map _formulaEntriesByCell;
public FormulaCellCache() {
// assumes HSSFCell does not override hashCode or equals, otherwise we need IdentityHashMap
_formulaEntriesByCell = new HashMap();
}
public CellCacheEntry[] getCacheEntries() {
FormulaCellCacheEntry[] result = new FormulaCellCacheEntry[_formulaEntriesByCell.size()];
_formulaEntriesByCell.values().toArray(result);
return result;
}
public void clear() {
_formulaEntriesByCell.clear();
}
/**
* @return <code>null</code> if not found
*/
public FormulaCellCacheEntry get(EvaluationCell cell) {
return (FormulaCellCacheEntry) _formulaEntriesByCell.get(cell);
}
public void put(EvaluationCell cell, FormulaCellCacheEntry entry) {
_formulaEntriesByCell.put(cell, entry);
}
public FormulaCellCacheEntry remove(EvaluationCell cell) {
return (FormulaCellCacheEntry) _formulaEntriesByCell.remove(cell);
}
public void applyOperation(IEntryOperation operation) {
Iterator i = _formulaEntriesByCell.values().iterator();
while (i.hasNext()) {
operation.processEntry((FormulaCellCacheEntry) i.next());
}
}
}

View File

@ -0,0 +1,113 @@
/* ====================================================================
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.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.ss.formula.FormulaUsedBlankCellSet.BookSheetKey;
/**
* Stores the cached result of a formula evaluation, along with the set of sensititive input cells
*
* @author Josh Micich
*/
final class FormulaCellCacheEntry extends CellCacheEntry {
public static final FormulaCellCacheEntry[] EMPTY_ARRAY = { };
/**
* Cells 'used' in the current evaluation of the formula corresponding to this cache entry
*
* If any of the following cells change, this cache entry needs to be cleared
*/
private CellCacheEntry[] _sensitiveInputCells;
private FormulaUsedBlankCellSet _usedBlankCellGroup;
public FormulaCellCacheEntry() {
}
public void setSensitiveInputCells(CellCacheEntry[] sensitiveInputCells) {
// need to tell all cells that were previously used, but no longer are,
// that they are not consumed by this cell any more
changeConsumingCells(sensitiveInputCells == null ? CellCacheEntry.EMPTY_ARRAY : sensitiveInputCells);
_sensitiveInputCells = sensitiveInputCells;
}
public void clearFormulaEntry() {
CellCacheEntry[] usedCells = _sensitiveInputCells;
if (usedCells != null) {
for (int i = usedCells.length-1; i>=0; i--) {
usedCells[i].clearConsumingCell(this);
}
}
_sensitiveInputCells = null;
clearValue();
}
private void changeConsumingCells(CellCacheEntry[] usedCells) {
CellCacheEntry[] prevUsedCells = _sensitiveInputCells;
int nUsed = usedCells.length;
for (int i = 0; i < nUsed; i++) {
usedCells[i].addConsumingCell(this);
}
if (prevUsedCells == null) {
return;
}
int nPrevUsed = prevUsedCells.length;
if (nPrevUsed < 1) {
return;
}
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++) {
CellCacheEntry prevUsed = prevUsedCells[i];
if (!usedSet.contains(prevUsed)) {
// previously was used by cellLoc, but not anymore
prevUsed.clearConsumingCell(this);
}
}
}
public void updateFormulaResult(ValueEval result, CellCacheEntry[] sensitiveInputCells, FormulaUsedBlankCellSet usedBlankAreas) {
updateValue(result);
setSensitiveInputCells(sensitiveInputCells);
_usedBlankCellGroup = usedBlankAreas;
}
public void notifyUpdatedBlankCell(BookSheetKey bsk, int rowIndex, int columnIndex, IEvaluationListener evaluationListener) {
if (_usedBlankCellGroup != null) {
if (_usedBlankCellGroup.containsCell(bsk, rowIndex, columnIndex)) {
clearFormulaEntry();
recurseClearCachedFormulaResults(evaluationListener);
}
}
}
}

View File

@ -0,0 +1,156 @@
/* ====================================================================
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;
/**
* A custom implementation of {@link java.util.HashSet} in order to reduce memory consumption.
*
* Profiling tests (Oct 2008) have shown that each element {@link FormulaCellCacheEntry} takes
* around 32 bytes to store in a HashSet, but around 6 bytes to store here. For Spreadsheets with
* thousands of formula cells with multiple interdependencies, the savings can be very significant.
*
* @author Josh Micich
*/
final class FormulaCellCacheEntrySet {
private int _size;
private FormulaCellCacheEntry[] _arr;
public FormulaCellCacheEntrySet() {
_arr = FormulaCellCacheEntry.EMPTY_ARRAY;
}
public FormulaCellCacheEntry[] toArray() {
int nItems = _size;
if (nItems < 1) {
return FormulaCellCacheEntry.EMPTY_ARRAY;
}
FormulaCellCacheEntry[] result = new FormulaCellCacheEntry[nItems];
int j=0;
for(int i=0; i<_arr.length; i++) {
FormulaCellCacheEntry cce = _arr[i];
if (cce != null) {
result[j++] = cce;
}
}
if (j!= nItems) {
throw new IllegalStateException("size mismatch");
}
return result;
}
public void add(CellCacheEntry cce) {
if (_size * 3 >= _arr.length * 2) {
// re-hash
FormulaCellCacheEntry[] prevArr = _arr;
FormulaCellCacheEntry[] newArr = new FormulaCellCacheEntry[4 + _arr.length * 3 / 2]; // grow 50%
for(int i=0; i<prevArr.length; i++) {
FormulaCellCacheEntry prevCce = _arr[i];
if (prevCce != null) {
addInternal(newArr, prevCce);
}
}
_arr = newArr;
}
if (addInternal(_arr, cce)) {
_size++;
}
}
private static boolean addInternal(CellCacheEntry[] arr, CellCacheEntry cce) {
int startIx = cce.hashCode() % arr.length;
for(int i=startIx; i<arr.length; i++) {
CellCacheEntry item = arr[i];
if (item == cce) {
// already present
return false;
}
if (item == null) {
arr[i] = cce;
return true;
}
}
for(int i=0; i<startIx; i++) {
CellCacheEntry item = arr[i];
if (item == cce) {
// already present
return false;
}
if (item == null) {
arr[i] = cce;
return true;
}
}
throw new IllegalStateException("No empty space found");
}
public boolean remove(CellCacheEntry cce) {
FormulaCellCacheEntry[] arr = _arr;
if (_size * 3 < _arr.length && _arr.length > 8) {
// re-hash
boolean found = false;
FormulaCellCacheEntry[] prevArr = _arr;
FormulaCellCacheEntry[] newArr = new FormulaCellCacheEntry[_arr.length / 2]; // shrink 50%
for(int i=0; i<prevArr.length; i++) {
FormulaCellCacheEntry prevCce = _arr[i];
if (prevCce != null) {
if (prevCce == cce) {
found=true;
_size--;
// skip it
continue;
}
addInternal(newArr, prevCce);
}
}
_arr = newArr;
return found;
}
// else - usual case
// delete single element (without re-hashing)
int startIx = cce.hashCode() % arr.length;
// note - can't exit loops upon finding null because of potential previous deletes
for(int i=startIx; i<arr.length; i++) {
FormulaCellCacheEntry item = arr[i];
if (item == cce) {
// found it
arr[i] = null;
_size--;
return true;
}
}
for(int i=0; i<startIx; i++) {
FormulaCellCacheEntry item = arr[i];
if (item == cce) {
// found it
arr[i] = null;
_size--;
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,189 @@
/* ====================================================================
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.poi.hssf.util.CellReference;
/**
* Optimisation - compacts many blank cell references used by a single formula.
*
* @author Josh Micich
*/
final class FormulaUsedBlankCellSet {
public static final class BookSheetKey {
private final int _bookIndex;
private final int _sheetIndex;
public BookSheetKey(int bookIndex, int sheetIndex) {
_bookIndex = bookIndex;
_sheetIndex = sheetIndex;
}
public int hashCode() {
return _bookIndex * 17 + _sheetIndex;
}
public boolean equals(Object obj) {
BookSheetKey other = (BookSheetKey) obj;
return _bookIndex == other._bookIndex && _sheetIndex == other._sheetIndex;
}
}
private static final class BlankCellSheetGroup {
private final List _rectangleGroups;
private int _currentRowIndex;
private int _firstColumnIndex;
private int _lastColumnIndex;
private BlankCellRectangleGroup _currentRectangleGroup;
public BlankCellSheetGroup() {
_rectangleGroups = new ArrayList();
_currentRowIndex = -1;
}
public void addCell(int rowIndex, int columnIndex) {
if (_currentRowIndex == -1) {
_currentRowIndex = rowIndex;
_firstColumnIndex = columnIndex;
_lastColumnIndex = columnIndex;
} else {
if (_currentRowIndex == rowIndex && _lastColumnIndex+1 == columnIndex) {
_lastColumnIndex = columnIndex;
} else {
// cell does not fit on end of current row
if (_currentRectangleGroup == null) {
_currentRectangleGroup = new BlankCellRectangleGroup(_currentRowIndex, _firstColumnIndex, _lastColumnIndex);
} else {
if (!_currentRectangleGroup.acceptRow(_currentRowIndex, _firstColumnIndex, _lastColumnIndex)) {
_rectangleGroups.add(_currentRectangleGroup);
_currentRectangleGroup = new BlankCellRectangleGroup(_currentRowIndex, _firstColumnIndex, _lastColumnIndex);
}
}
_currentRowIndex = rowIndex;
_firstColumnIndex = columnIndex;
_lastColumnIndex = columnIndex;
}
}
}
public boolean containsCell(int rowIndex, int columnIndex) {
for (int i=_rectangleGroups.size()-1; i>=0; i--) {
BlankCellRectangleGroup bcrg = (BlankCellRectangleGroup) _rectangleGroups.get(i);
if (bcrg.containsCell(rowIndex, columnIndex)) {
return true;
}
}
if(_currentRectangleGroup != null && _currentRectangleGroup.containsCell(rowIndex, columnIndex)) {
return true;
}
if (_currentRowIndex != -1 && _currentRowIndex == rowIndex) {
if (_firstColumnIndex <= columnIndex && columnIndex <= _lastColumnIndex) {
return true;
}
}
return false;
}
}
private static final class BlankCellRectangleGroup {
private final int _firstRowIndex;
private final int _firstColumnIndex;
private final int _lastColumnIndex;
private int _lastRowIndex;
public BlankCellRectangleGroup(int firstRowIndex, int firstColumnIndex, int lastColumnIndex) {
_firstRowIndex = firstRowIndex;
_firstColumnIndex = firstColumnIndex;
_lastColumnIndex = lastColumnIndex;
_lastRowIndex = firstRowIndex;
}
public boolean containsCell(int rowIndex, int columnIndex) {
if (columnIndex < _firstColumnIndex) {
return false;
}
if (columnIndex > _lastColumnIndex) {
return false;
}
if (rowIndex < _firstRowIndex) {
return false;
}
if (rowIndex > _lastRowIndex) {
return false;
}
return true;
}
public boolean acceptRow(int rowIndex, int firstColumnIndex, int lastColumnIndex) {
if (firstColumnIndex != _firstColumnIndex) {
return false;
}
if (lastColumnIndex != _lastColumnIndex) {
return false;
}
if (rowIndex != _lastRowIndex+1) {
return false;
}
_lastRowIndex = rowIndex;
return true;
}
public String toString() {
StringBuffer sb = new StringBuffer(64);
CellReference crA = new CellReference(_firstRowIndex, _firstColumnIndex, false, false);
CellReference crB = new CellReference(_lastRowIndex, _lastColumnIndex, false, false);
sb.append(getClass().getName());
sb.append(" [").append(crA.formatAsString()).append(':').append(crB.formatAsString()).append("]");
return sb.toString();
}
}
private final Map _sheetGroupsByBookSheet;
public FormulaUsedBlankCellSet() {
_sheetGroupsByBookSheet = new HashMap();
}
public void addCell(int bookIndex, int sheetIndex, int rowIndex, int columnIndex) {
BlankCellSheetGroup sbcg = getSheetGroup(bookIndex, sheetIndex);
sbcg.addCell(rowIndex, columnIndex);
}
private BlankCellSheetGroup getSheetGroup(int bookIndex, int sheetIndex) {
BookSheetKey key = new BookSheetKey(bookIndex, sheetIndex);
BlankCellSheetGroup result = (BlankCellSheetGroup) _sheetGroupsByBookSheet.get(key);
if (result == null) {
result = new BlankCellSheetGroup();
_sheetGroupsByBookSheet.put(key, result);
}
return result;
}
public boolean containsCell(BookSheetKey key, int rowIndex, int columnIndex) {
BlankCellSheetGroup bcsg = (BlankCellSheetGroup) _sheetGroupsByBookSheet.get(key);
if (bcsg == null) {
return false;
}
return bcsg.containsCell(rowIndex, columnIndex);
}
}

View File

@ -19,6 +19,7 @@ package org.apache.poi.ss.formula;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell;
/** /**
* Tests can implement this class to track the internal working of the {@link WorkbookEvaluator}.<br/> * Tests can implement this class to track the internal working of the {@link WorkbookEvaluator}.<br/>
@ -28,12 +29,26 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval;
* @author Josh Micich * @author Josh Micich
*/ */
interface IEvaluationListener { interface IEvaluationListener {
/**
* A (mostly) opaque interface to allow test clients to trace cache values
* Each spreadsheet cell gets one unique cache entry instance. These objects
* are safe to use as keys in {@link java.util.HashMap}s
*/
interface ICacheEntry {
ValueEval getValue();
}
void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result); void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result);
void onReadPlainValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value); void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ICacheEntry entry);
void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs); void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs);
void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result); void onEndEvaluate(ICacheEntry entry, ValueEval result);
void onClearWholeCache(); void onClearWholeCache();
void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value); void onClearCachedValue(ICacheEntry entry);
void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value, int depth); /**
* Internally, formula {@link ICacheEntry}s are stored in sets which may change ordering due
* to seemingly trivial changes. This method is provided to make the order of call-backs to
* {@link #onClearDependentCachedValue(ICacheEntry, int)} more deterministic.
*/
void sortDependentCachedValues(ICacheEntry[] formulaCells);
void onClearDependentCachedValue(ICacheEntry formulaCell, int depth);
} }

View File

@ -0,0 +1,84 @@
/* ====================================================================
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.HashMap;
import java.util.Map;
/**
*
* @author Josh Micich
*/
final class PlainCellCache {
public static final class Loc {
private final int _bookSheetColumn;
private final int _rowIndex;
public Loc(int bookIndex, int sheetIndex, int rowIndex, int columnIndex) {
_bookSheetColumn = toBookSheetColumn(bookIndex, sheetIndex, columnIndex);
_rowIndex = rowIndex;
}
public static int toBookSheetColumn(int bookIndex, int sheetIndex, int columnIndex) {
return ((bookIndex & 0x00FF) << 24) + ((sheetIndex & 0x00FF) << 16)
+ ((columnIndex & 0xFFFF) << 0);
}
public Loc(int bookSheetColumn, int rowIndex) {
_bookSheetColumn = bookSheetColumn;
_rowIndex = rowIndex;
}
public int hashCode() {
return _bookSheetColumn + 17 * _rowIndex;
}
public boolean equals(Object obj) {
Loc other = (Loc) obj;
return _bookSheetColumn == other._bookSheetColumn && _rowIndex == other._rowIndex;
}
public int getRowIndex() {
return _rowIndex;
}
public int getColumnIndex() {
return _bookSheetColumn & 0x000FFFF;
}
}
private Map _plainValueEntriesByLoc;
public PlainCellCache() {
_plainValueEntriesByLoc = new HashMap();
}
public void put(Loc key, PlainValueCellCacheEntry cce) {
_plainValueEntriesByLoc.put(key, cce);
}
public void clear() {
_plainValueEntriesByLoc.clear();
}
public PlainValueCellCacheEntry get(Loc key) {
return (PlainValueCellCacheEntry) _plainValueEntriesByLoc.get(key);
}
public void remove(Loc key) {
_plainValueEntriesByLoc.remove(key);
}
}

View File

@ -0,0 +1,31 @@
/* ====================================================================
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;
/**
* Used for non-formula cells, primarily to keep track of the referencing (formula) cells.
*
* @author Josh Micich
*/
final class PlainValueCellCacheEntry extends CellCacheEntry {
public PlainValueCellCacheEntry(ValueEval value) {
updateValue(value);
}
}

View File

@ -18,7 +18,6 @@
package org.apache.poi.ss.formula; package org.apache.poi.ss.formula;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.ss.usermodel.Sheet;
/** /**
* *
* *
@ -28,7 +27,7 @@ final class SheetRefEvaluator {
private final WorkbookEvaluator _bookEvaluator; private final WorkbookEvaluator _bookEvaluator;
private final EvaluationTracker _tracker; private final EvaluationTracker _tracker;
private final Sheet _sheet; private final EvaluationSheet _sheet;
private final int _sheetIndex; private final int _sheetIndex;
public SheetRefEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker, public SheetRefEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker,

View File

@ -63,8 +63,6 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.util.CellReference; import org.apache.poi.hssf.util.CellReference;
import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
/** /**
* Evaluates formula cells.<p/> * Evaluates formula cells.<p/>
@ -143,29 +141,23 @@ public final class WorkbookEvaluator {
} }
/** /**
* Sets the cached value for a plain (non-formula) cell. * Should be called to tell the cell value cache that the specified (value or formula) cell
* @param never <code>null</code>. Use {@link BlankEval#INSTANCE} when the cell is being * has changed.
* cleared. Otherwise an instance of {@link NumberEval}, {@link StringEval}, {@link BoolEval}
* or {@link ErrorEval} to represent a plain cell value.
*/ */
public void setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value) { public void notifyUpdateCell(EvaluationCell cell) {
if (value == null) { int sheetIndex = getSheetIndex(cell.getSheet());
throw new IllegalArgumentException("value must not be null"); _cache.notifyUpdateCell(_workbookIx, sheetIndex, cell);
}
int sheetIndex = getSheetIndex(sheet);
_cache.setValue(getCellLoc(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 * Should be called to tell the cell value cache that the specified cell has just been
* formula cell, or the formula text has changed * deleted.
*/ */
public void notifySetFormula(Sheet sheet, int rowIndex, int columnIndex) { public void notifyDeleteCell(EvaluationCell cell) {
int sheetIndex = getSheetIndex(sheet); int sheetIndex = getSheetIndex(cell.getSheet());
_cache.setValue(getCellLoc(sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null); _cache.notifyDeleteCell(_workbookIx, sheetIndex, cell);
} }
private int getSheetIndex(Sheet sheet) {
private int getSheetIndex(EvaluationSheet sheet) {
Integer result = (Integer) _sheetIndexesBySheet.get(sheet); Integer result = (Integer) _sheetIndexesBySheet.get(sheet);
if (result == null) { if (result == null) {
int sheetIndex = _workbook.getSheetIndex(sheet); int sheetIndex = _workbook.getSheetIndex(sheet);
@ -178,78 +170,76 @@ public final class WorkbookEvaluator {
return result.intValue(); return result.intValue();
} }
public ValueEval evaluate(Cell srcCell) { public ValueEval evaluate(EvaluationCell srcCell) {
int sheetIndex = getSheetIndex(srcCell.getSheet()); int sheetIndex = getSheetIndex(srcCell.getSheet());
CellLocation cellLoc = getCellLoc(sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum()); return evaluateAny(srcCell, sheetIndex, srcCell.getRowIndex(), srcCell.getColumnIndex(), new EvaluationTracker(_cache));
return internalEvaluate(srcCell, cellLoc, new EvaluationTracker(_cache));
} }
/** /**
* @return never <code>null</code>, never {@link BlankEval} * @return never <code>null</code>, never {@link BlankEval}
*/ */
private ValueEval internalEvaluate(Cell srcCell, CellLocation cellLoc, EvaluationTracker tracker) { private ValueEval evaluateAny(EvaluationCell srcCell, int sheetIndex,
int sheetIndex = cellLoc.getSheetIndex(); int rowIndex, int columnIndex, EvaluationTracker tracker) {
int rowIndex = cellLoc.getRowIndex();
int columnIndex = cellLoc.getColumnIndex();
ValueEval result; if (srcCell == null || srcCell.getCellType() != Cell.CELL_TYPE_FORMULA) {
ValueEval result = getValueFromNonFormulaCell(srcCell);
result = tracker.startEvaluate(cellLoc); tracker.acceptPlainValueDependency(_workbookIx, sheetIndex, rowIndex, columnIndex, result);
IEvaluationListener evalListener = _evaluationListener;
if (result != null) {
if(evalListener != null) {
evalListener.onCacheHit(sheetIndex, rowIndex, columnIndex, result);
}
return result; return result;
} }
boolean isPlainFormulaCell = false; FormulaCellCacheEntry cce = _cache.getOrCreateFormulaCellEntry(srcCell);
try { tracker.acceptFormulaDependency(cce);
result = getValueFromNonFormulaCell(srcCell); IEvaluationListener evalListener = _evaluationListener;
if (result != null) { if (cce.getValue() == null) {
isPlainFormulaCell = true; if (!tracker.startEvaluate(cce)) {
if(evalListener != null) { return ErrorEval.CIRCULAR_REF_ERROR;
evalListener.onReadPlainValue(sheetIndex, rowIndex, columnIndex, result);
}
} else {
isPlainFormulaCell = false;
Ptg[] ptgs = _workbook.getFormulaTokens(srcCell);
if(evalListener == null) {
result = evaluateFormula(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker);
} else {
evalListener.onStartEvaluate(sheetIndex, rowIndex, columnIndex, ptgs);
result = evaluateFormula(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker);
evalListener.onEndEvaluate(sheetIndex, rowIndex, columnIndex, result);
}
} }
} finally {
tracker.endEvaluate(cellLoc, result, isPlainFormulaCell); try {
ValueEval result;
Ptg[] ptgs = _workbook.getFormulaTokens(srcCell);
if (evalListener == null) {
result = evaluateFormula(sheetIndex, rowIndex, columnIndex, ptgs, tracker);
} else {
evalListener.onStartEvaluate(srcCell, cce, ptgs);
result = evaluateFormula(sheetIndex, rowIndex, columnIndex, ptgs, tracker);
evalListener.onEndEvaluate(cce, result);
}
tracker.updateCacheResult(result);
} finally {
tracker.endEvaluate(cce);
}
} else {
if(evalListener != null) {
evalListener.onCacheHit(sheetIndex, rowIndex, columnIndex, cce.getValue());
}
return cce.getValue();
} }
if (isDebugLogEnabled()) { if (isDebugLogEnabled()) {
String sheetName = getSheetName(sheetIndex); String sheetName = getSheetName(sheetIndex);
CellReference cr = new CellReference(rowIndex, columnIndex); CellReference cr = new CellReference(rowIndex, columnIndex);
logDebug("Evaluated " + sheetName + "!" + cr.formatAsString() + " to " + result.toString()); logDebug("Evaluated " + sheetName + "!" + cr.formatAsString() + " to " + cce.getValue().toString());
} }
return result; return cce.getValue();
} }
/** /**
* Gets the value from a non-formula cell. * Gets the value from a non-formula cell.
* @param cell may be <code>null</code> * @param cell may be <code>null</code>
* @return {@link BlankEval} if cell is <code>null</code> or blank, <code>null</code> if cell * @return {@link BlankEval} if cell is <code>null</code> or blank, never <code>null</code>
* is a formula cell.
*/ */
private static ValueEval getValueFromNonFormulaCell(Cell cell) { /* package */ static ValueEval getValueFromNonFormulaCell(EvaluationCell cell) {
if (cell == null) { if (cell == null) {
return BlankEval.INSTANCE; return BlankEval.INSTANCE;
} }
int cellType = cell.getCellType(); int cellType = cell.getCellType();
switch (cellType) { switch (cellType) {
case Cell.CELL_TYPE_FORMULA:
return null;
case Cell.CELL_TYPE_NUMERIC: case Cell.CELL_TYPE_NUMERIC:
return new NumberEval(cell.getNumericCellValue()); return new NumberEval(cell.getNumericCellValue());
case Cell.CELL_TYPE_STRING: case Cell.CELL_TYPE_STRING:
return new StringEval(cell.getRichStringCellValue().getString()); return new StringEval(cell.getStringCellValue());
case Cell.CELL_TYPE_BOOLEAN: case Cell.CELL_TYPE_BOOLEAN:
return BoolEval.valueOf(cell.getBooleanCellValue()); return BoolEval.valueOf(cell.getBooleanCellValue());
case Cell.CELL_TYPE_BLANK: case Cell.CELL_TYPE_BLANK:
@ -469,21 +459,10 @@ public final class WorkbookEvaluator {
/** /**
* Used by the lazy ref evals whenever they need to get the value of a contained cell. * Used by the lazy ref evals whenever they need to get the value of a contained cell.
*/ */
/* package */ ValueEval evaluateReference(Sheet sheet, int sheetIndex, int rowIndex, /* package */ ValueEval evaluateReference(EvaluationSheet sheet, int sheetIndex, int rowIndex,
int columnIndex, EvaluationTracker tracker) { int columnIndex, EvaluationTracker tracker) {
Row row = sheet.getRow(rowIndex); EvaluationCell cell = sheet.getCell(rowIndex, columnIndex);
Cell cell; return evaluateAny(cell, sheetIndex, rowIndex, columnIndex, tracker);
if (row == null) {
cell = null;
} else {
cell = row.getCell(columnIndex);
}
CellLocation cellLoc = getCellLoc(sheetIndex, rowIndex, columnIndex);
tracker.acceptDependency(cellLoc);
return internalEvaluate(cell, cellLoc, tracker);
}
private CellLocation getCellLoc(int sheetIndex, int rowIndex, int columnIndex) {
return new CellLocation(_workbookIx, sheetIndex, rowIndex, columnIndex);
} }
} }

View File

@ -45,20 +45,17 @@ public interface FormulaEvaluator {
*/ */
void clearAllCachedResultValues(); void clearAllCachedResultValues();
/** /**
* Sets the cached value for a plain (non-formula) cell. * Should be called to tell the cell value cache that the specified (value or formula) cell
* Should be called whenever there are changes to individual input cells in the evaluated workbook. * has changed.
* Failure to call this method after changing cell values will cause incorrect behaviour * Failure to call this method after changing cell values will cause incorrect behaviour
* of the evaluate~ methods of this class * 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.
*/ */
void setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value); void notifySetFormula(Cell cell);
/** /**
* Should be called to tell the cell value cache that the specified cell has just become a * 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 * formula cell, or the formula text has changed
*/ */
void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex); void notifyDeleteCell(Cell cell);
/** /**
* If cell contains a formula, the formula is evaluated and returned, * If cell contains a formula, the formula is evaluated and returned,
* else the CellValue simply copies the appropriate cell value from * else the CellValue simply copies the appropriate cell value from

View File

@ -0,0 +1,78 @@
/* ====================================================================
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.xssf.usermodel;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationSheet;
/**
* XSSF wrapper for a cell under evaluation
*
* @author Josh Micich
*/
final class XSSFEvaluationCell implements EvaluationCell {
private final EvaluationSheet _evalSheet;
private final XSSFCell _cell;
public XSSFEvaluationCell(XSSFCell cell) {
_cell = cell;
_evalSheet = new XSSFEvaluationSheet((XSSFSheet)cell.getSheet());
}
public XSSFEvaluationCell(XSSFCell cell, XSSFEvaluationSheet evaluationSheet) {
_cell = cell;
_evalSheet = evaluationSheet;
}
// Note - hashCode and equals defined according to underlying cell
public int hashCode() {
return _cell.hashCode();
}
public boolean equals(Object obj) {
return _cell == ((XSSFEvaluationCell)obj)._cell;
}
public XSSFCell getXSSFCell() {
return _cell;
}
public boolean getBooleanCellValue() {
return _cell.getBooleanCellValue();
}
public int getCellType() {
return _cell.getCellType();
}
public int getColumnIndex() {
return _cell.getCellNum();
}
public int getErrorCellValue() {
return _cell.getErrorCellValue();
}
public double getNumericCellValue() {
return _cell.getNumericCellValue();
}
public int getRowIndex() {
return _cell.getRowIndex();
}
public EvaluationSheet getSheet() {
return _evalSheet;
}
public String getStringCellValue() {
return _cell.getRichStringCellValue().getString();
}
}

View File

@ -0,0 +1,50 @@
/* ====================================================================
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.xssf.usermodel;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationSheet;
/**
* XSSF wrapper for a sheet under evaluation
*
* @author Josh Micich
*/
final class XSSFEvaluationSheet implements EvaluationSheet {
private final XSSFSheet _xs;
public XSSFEvaluationSheet(XSSFSheet sheet) {
_xs = sheet;
}
public XSSFSheet getXSSFSheet() {
return _xs;
}
public EvaluationCell getCell(int rowIndex, int columnIndex) {
XSSFRow row = _xs.getRow(rowIndex);
if (row == null) {
return null;
}
XSSFCell cell = row.getCell(columnIndex);
if (cell == null) {
return null;
}
return new XSSFEvaluationCell(cell, this);
}
}

View File

@ -1,16 +1,32 @@
/* ====================================================================
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.xssf.usermodel; package org.apache.poi.xssf.usermodel;
import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NamePtg;
import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.record.formula.NameXPtg;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationName; import org.apache.poi.ss.formula.EvaluationName;
import org.apache.poi.ss.formula.EvaluationSheet;
import org.apache.poi.ss.formula.EvaluationWorkbook; import org.apache.poi.ss.formula.EvaluationWorkbook;
import org.apache.poi.ss.formula.FormulaParser; import org.apache.poi.ss.formula.FormulaParser;
import org.apache.poi.ss.formula.FormulaParsingWorkbook; import org.apache.poi.ss.formula.FormulaParsingWorkbook;
import org.apache.poi.ss.formula.FormulaRenderingWorkbook; import org.apache.poi.ss.formula.FormulaRenderingWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTDefinedName; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTDefinedName;
/** /**
@ -67,7 +83,8 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
return null; return null;
} }
public int getSheetIndex(Sheet sheet) { public int getSheetIndex(EvaluationSheet evalSheet) {
XSSFSheet sheet = ((XSSFEvaluationSheet)evalSheet).getXSSFSheet();
return _uBook.getSheetIndex(sheet); return _uBook.getSheetIndex(sheet);
} }
@ -80,8 +97,8 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
throw new RuntimeException("Not implemented yet"); throw new RuntimeException("Not implemented yet");
} }
public Sheet getSheet(int sheetIndex) { public EvaluationSheet getSheet(int sheetIndex) {
return _uBook.getSheetAt(sheetIndex); return new XSSFEvaluationSheet(_uBook.getSheetAt(sheetIndex));
} }
public ExternalSheet getExternalSheet(int externSheetIndex) { public ExternalSheet getExternalSheet(int externSheetIndex) {
@ -95,10 +112,6 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
return _uBook.getSheetIndex(sheetName); return _uBook.getSheetIndex(sheetName);
} }
public Workbook getWorkbook() {
return _uBook;
}
/** /**
* TODO - figure out what the hell this methods does in * TODO - figure out what the hell this methods does in
* HSSF... * HSSF...
@ -119,7 +132,8 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E
int ix = namePtg.getIndex(); int ix = namePtg.getIndex();
return new Name(_uBook.getNameAt(ix), ix, this); return new Name(_uBook.getNameAt(ix), ix, this);
} }
public Ptg[] getFormulaTokens(Cell cell) { public Ptg[] getFormulaTokens(EvaluationCell evalCell) {
XSSFCell cell = ((XSSFEvaluationCell)evalCell).getXSSFCell();
XSSFEvaluationWorkbook frBook = XSSFEvaluationWorkbook.create(_uBook); XSSFEvaluationWorkbook frBook = XSSFEvaluationWorkbook.create(_uBook);
return FormulaParser.parse(cell.getCellFormula(), frBook); return FormulaParser.parse(cell.getCellFormula(), frBook);
} }

View File

@ -19,14 +19,12 @@ package org.apache.poi.xssf.usermodel;
import java.util.Iterator; 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.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval; 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.NumberEval;
import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.StringEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.ss.formula.WorkbookEvaluator; import org.apache.poi.ss.formula.WorkbookEvaluator;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.CellValue;
@ -61,11 +59,11 @@ public class XSSFFormulaEvaluator implements FormulaEvaluator {
public void clearAllCachedResultValues() { public void clearAllCachedResultValues() {
_bookEvaluator.clearAllCachedResultValues(); _bookEvaluator.clearAllCachedResultValues();
} }
public void setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value) { public void notifySetFormula(Cell cell) {
_bookEvaluator.setCachedPlainValue(sheet, rowIndex, columnIndex, value); _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell));
} }
public void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex) { public void notifyDeleteCell(Cell cell) {
_bookEvaluator.notifySetFormula(sheet, rowIndex, columnIndex); _bookEvaluator.notifyDeleteCell(new XSSFEvaluationCell((XSSFCell)cell));
} }
/** /**
@ -227,7 +225,7 @@ public class XSSFFormulaEvaluator implements FormulaEvaluator {
* @param eval * @param eval
*/ */
private CellValue evaluateFormulaCellValue(Cell cell) { private CellValue evaluateFormulaCellValue(Cell cell) {
ValueEval eval = _bookEvaluator.evaluate(cell); ValueEval eval = _bookEvaluator.evaluate(new XSSFEvaluationCell((XSSFCell) cell));
if (eval instanceof NumberEval) { if (eval instanceof NumberEval) {
NumberEval ne = (NumberEval) eval; NumberEval ne = (NumberEval) eval;
return new CellValue(ne.getNumberValue()); return new CellValue(ne.getNumberValue());

View File

@ -0,0 +1,33 @@
/* ====================================================================
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.hssf.usermodel;
import org.apache.poi.ss.formula.EvaluationCell;
/**
* Raises visibility of some internal functionality for test purposes
*
* @author Josh Micich
*/
public final class HSSFEvaluationTestHelper {
public static EvaluationCell wrapCell(HSSFCell cell) {
return new HSSFEvaluationCell(cell);
}
}

View File

@ -32,6 +32,7 @@ import org.apache.poi.hssf.record.formula.AreaPtg;
import org.apache.poi.hssf.record.formula.FuncVarPtg; import org.apache.poi.hssf.record.formula.FuncVarPtg;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationListener; import org.apache.poi.ss.formula.EvaluationListener;
import org.apache.poi.ss.formula.WorkbookEvaluator; import org.apache.poi.ss.formula.WorkbookEvaluator;
import org.apache.poi.ss.formula.WorkbookEvaluatorTestHelper; import org.apache.poi.ss.formula.WorkbookEvaluatorTestHelper;
@ -168,6 +169,12 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
assertEquals("-1000000.0-3000000.0", cell.getCellFormula()); assertEquals("-1000000.0-3000000.0", cell.getCellFormula());
assertEquals(-4000000, eva.evaluate(cell).getNumberValue(), 0); assertEquals(-4000000, eva.evaluate(cell).getNumberValue(), 0);
} }
// public static void main(String[] args) {
// new TestFormulaEvaluatorBugs().test44410();
// new TestFormulaEvaluatorBugs().testSlowEvaluate45376();
// new HSSFWorkbook();
// System.out.println("done");
// }
/** /**
* Bug 44410: SUM(C:C) is valid in excel, and means a sum * Bug 44410: SUM(C:C) is valid in excel, and means a sum
@ -309,7 +316,7 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
public void onCacheHit(int sheetIndex, int srcRowNum, int srcColNum, ValueEval result) { public void onCacheHit(int sheetIndex, int srcRowNum, int srcColNum, ValueEval result) {
_countCacheHits++; _countCacheHits++;
} }
public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) { public void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs) {
_countCacheMisses++; _countCacheMisses++;
} }
} }
@ -341,7 +348,7 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
HSSFCell cell = row.getCell(8); HSSFCell cell = row.getCell(8);
EvalListener evalListener = new EvalListener(); EvalListener evalListener = new EvalListener();
WorkbookEvaluator evaluator = WorkbookEvaluatorTestHelper.createEvaluator(wb, evalListener); WorkbookEvaluator evaluator = WorkbookEvaluatorTestHelper.createEvaluator(wb, evalListener);
evaluator.evaluate(cell); evaluator.evaluate(HSSFEvaluationTestHelper.wrapCell(cell));
int evalCount = evalListener.getCountCacheMisses(); int evalCount = evalListener.getCountCacheMisses();
if (evalCount > 10) { if (evalCount > 10) {
// Without caching, evaluating cell 'A9' takes 21845 evaluations which consumes // Without caching, evaluating cell 'A9' takes 21845 evaluations which consumes

View File

@ -19,6 +19,7 @@ package org.apache.poi.ss.formula;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell;
/** /**
* Tests should extend this class if they need to track the internal working of the {@link WorkbookEvaluator}.<br/> * Tests should extend this class if they need to track the internal working of the {@link WorkbookEvaluator}.<br/>
@ -28,26 +29,28 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval;
* @author Josh Micich * @author Josh Micich
*/ */
public abstract class EvaluationListener implements IEvaluationListener { public abstract class EvaluationListener implements IEvaluationListener {
public void onCacheHit(int sheetIndex, int rowIndex, int srcColNum, ValueEval result) { public void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) {
// do nothing // do nothing
} }
public void onReadPlainValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value) { public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ICacheEntry entry) {
// do nothing // do nothing
} }
public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) { public void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs) {
// do nothing // do nothing
} }
public void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) { public void onEndEvaluate(ICacheEntry entry, ValueEval result) {
// do nothing // do nothing
} }
public void onClearWholeCache() { public void onClearWholeCache() {
// do nothing // do nothing
} }
public void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) { public void onClearCachedValue(ICacheEntry entry) {
// do nothing // do nothing
} }
public void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, public void sortDependentCachedValues(ICacheEntry[] entries) {
ValueEval value,int depth) { // do nothing
}
public void onClearDependentCachedValue(ICacheEntry entry, int depth) {
// do nothing // do nothing
} }
} }

View File

@ -17,13 +17,11 @@
package org.apache.poi.ss.formula; package org.apache.poi.ss.formula;
import org.apache.poi.hssf.record.formula.eval.BlankEval; import junit.framework.TestCase;
import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.NumberEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
/** /**
* Tests {@link CellCacheEntry}. * Tests {@link CellCacheEntry}.
* *
@ -32,22 +30,14 @@ import junit.framework.TestCase;
public class TestCellCacheEntry extends TestCase { public class TestCellCacheEntry extends TestCase {
public void testBasic() { public void testBasic() {
CellCacheEntry cce = new CellCacheEntry(); CellCacheEntry pcce = new PlainValueCellCacheEntry(new NumberEval(42.0));
cce.updatePlainValue(new NumberEval(42.0)); ValueEval ve = pcce.getValue();
ValueEval ve = cce.getValue();
assertEquals(42, ((NumberEval)ve).getNumberValue(), 0.0); assertEquals(42, ((NumberEval)ve).getNumberValue(), 0.0);
cce.setFormulaResult(new NumberEval(10.0), new CellLocation[] { }); FormulaCellCacheEntry fcce = new FormulaCellCacheEntry();
} fcce.updateFormulaResult(new NumberEval(10.0), CellCacheEntry.EMPTY_ARRAY, null);
public void testBlank() { ve = fcce.getValue();
CellCacheEntry cce = new CellCacheEntry(); assertEquals(10, ((NumberEval)ve).getNumberValue(), 0.0);
cce.updatePlainValue(BlankEval.INSTANCE);
try {
cce.updatePlainValue(BlankEval.INSTANCE);
} catch (IllegalStateException e) {
// bug was visible around svn r700356
throw new AssertionFailedError("cache entry does not handle blank values properly");
}
} }
} }

View File

@ -19,7 +19,11 @@ package org.apache.poi.ss.formula;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -33,10 +37,12 @@ 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.StringEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFEvaluationTestHelper;
import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.CellReference; import org.apache.poi.hssf.util.CellReference;
import org.apache.poi.ss.formula.PlainCellCache.Loc;
/** /**
* Tests {@link EvaluationCache}. Makes sure that where possible (previously calculated) cached * Tests {@link EvaluationCache}. Makes sure that where possible (previously calculated) cached
@ -46,33 +52,89 @@ import org.apache.poi.hssf.util.CellReference;
* @author Josh Micich * @author Josh Micich
*/ */
public class TestEvaluationCache extends TestCase { public class TestEvaluationCache extends TestCase {
private static final class FormulaCellCacheEntryComparer implements Comparator {
private final Map _formulaCellsByCacheEntry;
public FormulaCellCacheEntryComparer(Map formulaCellsByCacheEntry) {
_formulaCellsByCacheEntry = formulaCellsByCacheEntry;
}
private EvaluationCell getCell(Object a) {
return (EvaluationCell)_formulaCellsByCacheEntry.get(a);
}
public int compare(Object oa, Object ob) {
EvaluationCell a = getCell(oa);
EvaluationCell b = getCell(ob);
int cmp;
cmp = a.getRowIndex() - b.getRowIndex();
if (cmp != 0) {
return cmp;
}
cmp = a.getColumnIndex() - b.getColumnIndex();
if (cmp != 0) {
return cmp;
}
if (a.getSheet() == b.getSheet()) {
return 0;
}
throw new RuntimeException("Incomplete code - don't know how to order sheets");
}
}
private static final class EvalListener extends EvaluationListener { private static final class EvalListener extends EvaluationListener {
private final List _logList; private final List _logList;
private final HSSFWorkbook _book; private final HSSFWorkbook _book;
private Map _formulaCellsByCacheEntry;
private Map _plainCellLocsByCacheEntry;
public EvalListener(HSSFWorkbook wb) { public EvalListener(HSSFWorkbook wb) {
_book = wb; _book = wb;
_logList = new ArrayList(); _logList = new ArrayList();
_formulaCellsByCacheEntry = new HashMap();
_plainCellLocsByCacheEntry = new HashMap();
} }
public void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) { public void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) {
log("hit", rowIndex, columnIndex, result); log("hit", rowIndex, columnIndex, result);
} }
public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) { public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ICacheEntry entry) {
log("value", rowIndex, columnIndex, value); Loc loc = new Loc(0, sheetIndex, rowIndex, columnIndex);
_plainCellLocsByCacheEntry.put(entry, loc);
log("value", rowIndex, columnIndex, entry.getValue());
} }
public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) { public void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs) {
log("start", rowIndex, columnIndex, ptgs); _formulaCellsByCacheEntry.put(entry, cell);
log("start", cell.getRowIndex(), cell.getColumnIndex(), ptgs);
} }
public void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) { public void onEndEvaluate(ICacheEntry entry, ValueEval result) {
log("end", rowIndex, columnIndex, result); EvaluationCell cell = (EvaluationCell) _formulaCellsByCacheEntry.get(entry);
log("end", cell.getRowIndex(), cell.getColumnIndex(), result);
} }
public void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) { public void onClearCachedValue(ICacheEntry entry) {
log("clear", rowIndex, columnIndex, value); int rowIndex;
int columnIndex;
EvaluationCell cell = (EvaluationCell) _formulaCellsByCacheEntry.get(entry);
if (cell == null) {
Loc loc = (Loc)_plainCellLocsByCacheEntry.get(entry);
if (loc == null) {
throw new IllegalStateException("can't find cell or location");
}
rowIndex = loc.getRowIndex();
columnIndex = loc.getColumnIndex();
} else {
rowIndex = cell.getRowIndex();
columnIndex = cell.getColumnIndex();
}
log("clear", rowIndex, columnIndex, entry.getValue());
} }
public void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, public void sortDependentCachedValues(ICacheEntry[] entries) {
ValueEval value,int depth) { Arrays.sort(entries, new FormulaCellCacheEntryComparer(_formulaCellsByCacheEntry));
log("clear" + depth, rowIndex, columnIndex, value); }
public void onClearDependentCachedValue(ICacheEntry entry, int depth) {
EvaluationCell cell = (EvaluationCell) _formulaCellsByCacheEntry.get(entry);
log("clear" + depth, cell.getRowIndex(), cell.getColumnIndex(), entry.getValue());
} }
private void log(String tag, int rowIndex, int columnIndex, Object value) { private void log(String tag, int rowIndex, int columnIndex, Object value) {
StringBuffer sb = new StringBuffer(64); StringBuffer sb = new StringBuffer(64);
@ -139,19 +201,23 @@ public class TestEvaluationCache extends TestCase {
_sheet = _wb.createSheet("Sheet1"); _sheet = _wb.createSheet("Sheet1");
} }
private static EvaluationCell wrapCell(HSSFCell cell) {
return HSSFEvaluationTestHelper.wrapCell(cell);
}
public void setCellValue(String cellRefText, double value) { public void setCellValue(String cellRefText, double value) {
HSSFCell cell = getOrCreateCell(cellRefText); HSSFCell cell = getOrCreateCell(cellRefText);
// be sure to blank cell, in case it is currently a formula // be sure to blank cell, in case it is currently a formula
cell.setCellType(HSSFCell.CELL_TYPE_BLANK); cell.setCellType(HSSFCell.CELL_TYPE_BLANK);
// otherwise this line will only set the formula cached result; // otherwise this line will only set the formula cached result;
cell.setCellValue(value); cell.setCellValue(value);
_evaluator.setCachedPlainValue(_sheet, cell.getRowIndex(), cell.getCellNum(), new NumberEval(value)); _evaluator.notifyUpdateCell(wrapCell(cell));
} }
public void setCellFormula(String cellRefText, String formulaText) { public void setCellFormula(String cellRefText, String formulaText) {
HSSFCell cell = getOrCreateCell(cellRefText); HSSFCell cell = getOrCreateCell(cellRefText);
cell.setCellFormula(formulaText); cell.setCellFormula(formulaText);
_evaluator.notifySetFormula(_sheet, cell.getRowIndex(), cell.getCellNum()); _evaluator.notifyUpdateCell(wrapCell(cell));
} }
private HSSFCell getOrCreateCell(String cellRefText) { private HSSFCell getOrCreateCell(String cellRefText) {
@ -170,7 +236,7 @@ public class TestEvaluationCache extends TestCase {
} }
public ValueEval evaluateCell(String cellRefText) { public ValueEval evaluateCell(String cellRefText) {
return _evaluator.evaluate(getOrCreateCell(cellRefText)); return _evaluator.evaluate(wrapCell(getOrCreateCell(cellRefText)));
} }
public String[] getAndClearLog() { public String[] getAndClearLog() {
@ -243,6 +309,7 @@ public class TestEvaluationCache extends TestCase {
// change a low level cell // change a low level cell
ms.setCellValue("D1", 10); ms.setCellValue("D1", 10);
confirmLog(ms, new String[] { confirmLog(ms, new String[] {
"clear D1 10",
"clear1 C1 54", "clear1 C1 54",
"clear2 B1 8", "clear2 B1 8",
"clear3 A1 46", "clear3 A1 46",
@ -272,6 +339,7 @@ public class TestEvaluationCache extends TestCase {
ms.setCellValue("B3", 3); // B3 is in the middle of the dependency tree ms.setCellValue("B3", 3); // B3 is in the middle of the dependency tree
confirmLog(ms, new String[] { confirmLog(ms, new String[] {
"clear B3 3",
"clear1 B2 46", "clear1 B2 46",
"clear2 A1 46", "clear2 A1 46",
}); });
@ -296,6 +364,7 @@ public class TestEvaluationCache extends TestCase {
ms.getAndClearLog(); ms.getAndClearLog();
ms.setCellFormula("B2", "B3*C2-C3"); // used to be "B3*C1-C2" ms.setCellFormula("B2", "B3*C2-C3"); // used to be "B3*C1-C2"
confirmLog(ms, new String[] { confirmLog(ms, new String[] {
"clear B2 46",
"clear1 A1 46", "clear1 A1 46",
}); });
@ -307,7 +376,8 @@ public class TestEvaluationCache extends TestCase {
"hit B3 2", "hit B3 2",
"hit C2 62", "hit C2 62",
"start C3 SUM(D3:E4)", "start C3 SUM(D3:E4)",
"hit D3 16", "hit E3 17", "value D4 #BLANK#", "value E4 #BLANK#", "hit D3 16", "hit E3 17",
// "value D4 #BLANK#", "value E4 #BLANK#",
"end C3 33", "end C3 33",
"end B2 91", "end B2 91",
"end A1 91", "end A1 91",
@ -320,6 +390,7 @@ public class TestEvaluationCache extends TestCase {
// Now change a value that should no longer affect B2 // Now change a value that should no longer affect B2
ms.setCellValue("D1", 11); ms.setCellValue("D1", 11);
confirmLog(ms, new String[] { confirmLog(ms, new String[] {
"clear D1 11",
"clear1 C1 54", "clear1 C1 54",
// note there is no "clear2 B2 91" here because B2 doesn't depend on C1 anymore // note there is no "clear2 B2 91" here because B2 doesn't depend on C1 anymore
"clear2 B1 8", "clear2 B1 8",
@ -342,7 +413,7 @@ public class TestEvaluationCache extends TestCase {
/** /**
* verifies that when updating a plain cell, depending (formula) cell cached values are cleared * verifies that when updating a plain cell, depending (formula) cell cached values are cleared
* only when the palin cell's value actually changes * only when the plain cell's value actually changes
*/ */
public void testRedundantUpdate() { public void testRedundantUpdate() {
MySheet ms = new MySheet(); MySheet ms = new MySheet();
@ -369,6 +440,7 @@ public class TestEvaluationCache extends TestCase {
ms.setCellValue("B1", 11); // value changing ms.setCellValue("B1", 11); // value changing
confirmLog(ms, new String[] { confirmLog(ms, new String[] {
"clear B1 11",
"clear1 A1 25", // expect consuming formula cached result to get cleared "clear1 A1 25", // expect consuming formula cached result to get cleared
}); });
confirmEvaluate(ms, "A1", 24); confirmEvaluate(ms, "A1", 24);
@ -434,7 +506,53 @@ public class TestEvaluationCache extends TestCase {
"hit D1 25", "hit D1 25",
"end A1 25", "end A1 25",
}); });
}
public void testBlankCells() {
MySheet ms = new MySheet();
ms.setCellFormula("A1", "sum(B1:D4,B5:E6)");
ms.setCellValue("B1", 12);
ms.clearAllCachedResultValues();
ms.getAndClearLog();
confirmEvaluate(ms, "A1", 12);
confirmLog(ms, new String[] {
"start A1 SUM(B1:D4,B5:E6)",
"value B1 12",
"end A1 12",
});
ms.setCellValue("B6", 2);
ms.getAndClearLog();
confirmEvaluate(ms, "A1", 14);
confirmLog(ms, new String[] {
"start A1 SUM(B1:D4,B5:E6)",
"hit B1 12",
"hit B6 2",
"end A1 14",
});
ms.setCellValue("E4", 2);
ms.getAndClearLog();
confirmEvaluate(ms, "A1", 14);
confirmLog(ms, new String[] {
"hit A1 14",
});
ms.setCellValue("D1", 1);
ms.getAndClearLog();
confirmEvaluate(ms, "A1", 15);
confirmLog(ms, new String[] {
"start A1 SUM(B1:D4,B5:E6)",
"hit B1 12",
"hit D1 1",
"hit B6 2",
"end A1 15",
});
} }
private static void confirmEvaluate(MySheet ms, String cellRefText, double expectedValue) { private static void confirmEvaluate(MySheet ms, String cellRefText, double expectedValue) {

View File

@ -127,18 +127,21 @@ public class TestWorkbookEvaluator extends TestCase {
confirmEvaluation(264, evaluatorA, cell); confirmEvaluation(264, evaluatorA, cell);
// change [wbB]BSheet1!B3 (from 50 to 60) // change [wbB]BSheet1!B3 (from 50 to 60)
bSheet1.getRow(2).getCell(1).setCellValue(60); HSSFCell cellB3 = bSheet1.getRow(2).getCell(1);
evaluatorB.setCachedPlainValue(bSheet1, 2, 1, new NumberEval(60)); cellB3.setCellValue(60);
evaluatorB.notifyUpdateCell(cellB3);
confirmEvaluation(274, evaluatorA, cell); confirmEvaluation(274, evaluatorA, cell);
// change [wbA]ASheet1!A3 (from 100 to 80) // change [wbA]ASheet1!A3 (from 100 to 80)
aSheet1.getRow(2).getCell(0).setCellValue(80); HSSFCell cellA3 = aSheet1.getRow(2).getCell(0);
evaluatorA.setCachedPlainValue(aSheet1, 2, 0, new NumberEval(80)); cellA3.setCellValue(80);
evaluatorA.notifyUpdateCell(cellA3);
confirmEvaluation(234, evaluatorA, cell); confirmEvaluation(234, evaluatorA, cell);
// change [wbA]AnotherSheet!A1 (from 2 to 3) // change [wbA]AnotherSheet!A1 (from 2 to 3)
wbA.getSheetAt(1).getRow(0).getCell(0).setCellValue(3); HSSFCell cellA1 = wbA.getSheetAt(1).getRow(0).getCell(0);
evaluatorA.setCachedPlainValue(wbA.getSheetAt(1), 0, 0, new NumberEval(3)); cellA1.setCellValue(3);
evaluatorA.notifyUpdateCell(cellA1);
confirmEvaluation(235, evaluatorA, cell); confirmEvaluation(235, evaluatorA, cell);
} }