diff options
32 files changed, 1626 insertions, 563 deletions
diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java new file mode 100644 index 0000000000..e5e7116ae3 --- /dev/null +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java @@ -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(); + } +} diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java new file mode 100644 index 0000000000..166da4751a --- /dev/null +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java @@ -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); + } +}
\ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java index 0b6a954499..359862e477 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java @@ -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;
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.NameXPtg;
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.EvaluationSheet;
import org.apache.poi.ss.formula.EvaluationWorkbook;
import org.apache.poi.ss.formula.FormulaParsingWorkbook;
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
@@ -45,8 +62,8 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return _iBook.getExternalSheetIndex(workbookName, sheetName);
}
- public EvaluationName getName(int index) {
- return new Name(_iBook.getNameRecord(index), index);
+ public NameXPtg getNameXPtg(String name) {
+ return _iBook.getNameXPtg(name);
}
public EvaluationName getName(String name) {
@@ -59,7 +76,8 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return null;
}
- public int getSheetIndex(Sheet sheet) {
+ public int getSheetIndex(EvaluationSheet evalSheet) {
+ HSSFSheet sheet = ((HSSFEvaluationSheet)evalSheet).getHSSFSheet();
return _uBook.getSheetIndex(sheet);
}
public int getSheetIndex(String sheetName) {
@@ -70,16 +88,8 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return _uBook.getSheetName(sheetIndex);
}
- public int getNameIndex(String name) {
- return _uBook.getNameIndex(name);
- }
-
- public NameXPtg getNameXPtg(String name) {
- return _iBook.getNameXPtg(name);
- }
-
- public Sheet getSheet(int sheetIndex) {
- return _uBook.getSheetAt(sheetIndex);
+ public EvaluationSheet getSheet(int sheetIndex) {
+ return new HSSFEvaluationSheet(_uBook.getSheetAt(sheetIndex));
}
public int convertFromExternSheetIndex(int externSheetIndex) {
return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);
@@ -88,10 +98,6 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E public ExternalSheet getExternalSheet(int externSheetIndex) {
return _iBook.getExternalSheet(externSheetIndex);
}
-
- public HSSFWorkbook getWorkbook() {
- return _uBook;
- }
public String resolveNameXText(NameXPtg n) {
return _iBook.resolveNameXText(n.getSheetRefIndex(), n.getNameIndex());
@@ -107,15 +113,15 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E int ix = namePtg.getIndex();
return new Name(_iBook.getNameRecord(ix), ix);
}
- public Ptg[] getFormulaTokens(Cell cell) {
+ public Ptg[] getFormulaTokens(EvaluationCell evalCell) {
+ HSSFCell cell = ((HSSFEvaluationCell)evalCell).getHSSFCell();
if (false) {
// 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
// to make sure that all formulas POI can evaluate can also be parsed.
return HSSFFormulaParser.parse(cell.getCellFormula(), _uBook);
}
- HSSFCell hCell = (HSSFCell) cell;
- FormulaRecord fr = ((FormulaRecordAggregate) hCell.getCellValueRecord()).getFormulaRecord();
+ FormulaRecord fr = ((FormulaRecordAggregate) cell.getCellValueRecord()).getFormulaRecord();
return fr.getParsedExpression();
}
@@ -128,23 +134,18 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E _nameRecord = nameRecord;
_index = index;
}
-
public Ptg[] getNameDefinition() {
return _nameRecord.getNameDefinition();
}
-
public String getNameText() {
return _nameRecord.getNameText();
}
-
public boolean hasFormula() {
return _nameRecord.hasFormula();
}
-
public boolean isFunctionName() {
return _nameRecord.isFunctionName();
}
-
public boolean isRange() {
return _nameRecord.hasFormula(); // TODO - is this right?
}
diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index 0c666847af..ef8300d098 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -19,7 +19,6 @@ package org.apache.poi.hssf.usermodel; import java.util.Iterator;
-import org.apache.poi.hssf.record.formula.eval.BlankEval;
import org.apache.poi.hssf.record.formula.eval.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
import org.apache.poi.hssf.record.formula.eval.NumberEval;
@@ -29,7 +28,6 @@ import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment; import org.apache.poi.ss.formula.WorkbookEvaluator;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue;
-import org.apache.poi.ss.usermodel.Sheet;
/**
* 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
- * 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
* of the evaluate~ methods of this class
*/
@@ -94,23 +94,22 @@ public class HSSFFormulaEvaluator /* almost implements FormulaEvaluator */ { _bookEvaluator.clearAllCachedResultValues();
}
/**
- * Sets the cached value for a plain (non-formula) cell.
- * Should be called whenever there are changes to individual input cells in the evaluated workbook.
+ * Should be called to tell the cell value cache that the specified (value or formula) cell
+ * has changed.
* Failure to call this method after changing cell values will cause incorrect behaviour
* of the evaluate~ methods of this class
- * @param never <code>null</code>. Use {@link BlankEval#INSTANCE} when the cell is being
- * cleared. Otherwise an instance of {@link NumberEval}, {@link StringEval}, {@link BoolEval}
- * or {@link ErrorEval} to represent a plain cell value.
*/
- public void setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value) {
- _bookEvaluator.setCachedPlainValue(sheet, rowIndex, columnIndex, value);
+ public void notifyUpdateCell(HSSFCell cell) {
+ _bookEvaluator.notifyUpdateCell(new HSSFEvaluationCell(cell));
}
/**
- * 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
+ * Should be called to tell the cell value cache that the specified cell has just been
+ * 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) {
- _bookEvaluator.notifySetFormula(sheet, rowIndex, columnIndex);
+ public void notifyDeleteCell(HSSFCell cell) {
+ _bookEvaluator.notifyDeleteCell(new HSSFEvaluationCell(cell));
}
/**
@@ -273,7 +272,7 @@ public class HSSFFormulaEvaluator /* almost implements FormulaEvaluator */ { * @param eval
*/
private CellValue evaluateFormulaCellValue(Cell cell) {
- ValueEval eval = _bookEvaluator.evaluate(cell);
+ ValueEval eval = _bookEvaluator.evaluate(new HSSFEvaluationCell((HSSFCell)cell));
if (eval instanceof NumberEval) {
NumberEval ne = (NumberEval) eval;
return new CellValue(ne.getNumberValue());
diff --git a/src/java/org/apache/poi/ss/formula/CellCacheEntry.java b/src/java/org/apache/poi/ss/formula/CellCacheEntry.java index bd7500023c..1eb7298a50 100644 --- a/src/java/org/apache/poi/ss/formula/CellCacheEntry.java +++ b/src/java/org/apache/poi/ss/formula/CellCacheEntry.java @@ -17,42 +17,44 @@ 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.BoolEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.ss.formula.IEvaluationListener.ICacheEntry; /** * Stores the parameters that identify the evaluation of one cell.<br/> */ -final class CellCacheEntry { +abstract class CellCacheEntry implements ICacheEntry { + public static final CellCacheEntry[] EMPTY_ARRAY = { }; + + private final FormulaCellCacheEntrySet _consumingCells; private ValueEval _value; - private final Set _consumingCells; - private CellLocation[] _usedCells; - private boolean _isPlainValueCell; - public CellCacheEntry() { - _consumingCells = new HashSet(); + + protected CellCacheEntry() { + _consumingCells = new FormulaCellCacheEntrySet(); } - public boolean updatePlainValue(ValueEval value) { - boolean wasChanged = false; - if (!_isPlainValueCell) { - wasChanged = true; - } - if (!areValuesEqual(_value, value)) { - wasChanged = true; + protected final void clearValue() { + _value = null; + } + + public final boolean updateValue(ValueEval value) { + if (value == null) { + throw new IllegalArgumentException("Did not expect to update to null"); } - _isPlainValueCell = true; + boolean result = !areValuesEqual(_value, value); _value = value; - _usedCells = null; - return wasChanged; + return result; + } + public final ValueEval getValue() { + return _value; } - private boolean areValuesEqual(ValueEval a, ValueEval b) { + + private static boolean areValuesEqual(ValueEval a, ValueEval b) { if (a == null) { return false; } @@ -78,41 +80,58 @@ final class CellCacheEntry { } throw new IllegalStateException("Unexpected value class (" + cls.getName() + ")"); } - public void setFormulaResult(ValueEval value, CellLocation[] usedCells) { - _isPlainValueCell = false; - _value = value; - _usedCells = usedCells; - } - public void addConsumingCell(CellLocation cellLoc) { + + public final void addConsumingCell(FormulaCellCacheEntry cellLoc) { _consumingCells.add(cellLoc); } - public CellLocation[] getConsumingCells() { - int nItems = _consumingCells.size(); - if (nItems < 1) { - return CellLocation.EMPTY_ARRAY; - } - CellLocation[] result = new CellLocation[nItems]; - _consumingCells.toArray(result); - return result; + public final FormulaCellCacheEntry[] getConsumingCells() { + return _consumingCells.toArray(); } - public CellLocation[] getUsedCells() { - return _usedCells; + + public final void clearConsumingCell(FormulaCellCacheEntry cce) { + if(!_consumingCells.remove(cce)) { + throw new IllegalStateException("Specified formula cell is not consumed by this cell"); + } } - public void clearConsumingCell(CellLocation fc) { - if(!_consumingCells.remove(fc)) { - throw new IllegalStateException("Cell '" + fc.formatAsString() + "' does not use this cell"); + 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(); + } } - public ValueEval getValue() { - ValueEval result = _value; - if (result == null) { - if (_isPlainValueCell) { - throw new IllegalStateException("Plain value cell should always have a value"); - - } + + /** + * 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); } - return result; } + }
\ No newline at end of file diff --git a/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java b/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java index 1d438527ae..5dbd2339d5 100644 --- a/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java +++ b/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java @@ -20,44 +20,58 @@ package org.apache.poi.ss.formula; import java.util.HashSet;
import java.util.Set;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+
/**
* Stores details about the current evaluation of a cell.<br/>
*/
final class CellEvaluationFrame {
- private final CellLocation _cellLocation;
- private final Set _usedCells;
+ private final FormulaCellCacheEntry _cce;
+ private final Set _sensitiveInputCells;
+ private FormulaUsedBlankCellSet _usedBlankCellGroup;
- public CellEvaluationFrame(CellLocation cellLoc) {
- _cellLocation = cellLoc;
- _usedCells = new HashSet();
+ public CellEvaluationFrame(FormulaCellCacheEntry cce) {
+ _cce = cce;
+ _sensitiveInputCells = new HashSet();
}
- public CellLocation getCoordinates() {
- return _cellLocation;
+ public CellCacheEntry getCCE() {
+ return _cce;
}
public String toString() {
StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" [");
- sb.append(_cellLocation.formatAsString());
sb.append("]");
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
- * evaluating the formula of this frame. For non-formula cells this will always be an empty
- * array;
+ * evaluating the formula of this frame.
*/
- public CellLocation[] getUsedCells() {
- int nItems = _usedCells.size();
+ private CellCacheEntry[] getSensitiveInputCells() {
+ int nItems = _sensitiveInputCells.size();
if (nItems < 1) {
- return CellLocation.EMPTY_ARRAY;
+ return CellCacheEntry.EMPTY_ARRAY;
}
- CellLocation[] result = new CellLocation[nItems];
- _usedCells.toArray(result);
+ CellCacheEntry[] result = new CellCacheEntry[nItems];
+ _sensitiveInputCells.toArray(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);
+ }
}
\ No newline at end of file diff --git a/src/java/org/apache/poi/ss/formula/CellLocation.java b/src/java/org/apache/poi/ss/formula/CellLocation.java deleted file mode 100644 index bb32e3dfa3..0000000000 --- a/src/java/org/apache/poi/ss/formula/CellLocation.java +++ /dev/null @@ -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(); - } -}
\ No newline at end of file diff --git a/src/java/org/apache/poi/ss/formula/EvaluationCache.java b/src/java/org/apache/poi/ss/formula/EvaluationCache.java index a1b06ad17e..81745825c7 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationCache.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationCache.java @@ -17,16 +17,17 @@ package org.apache.poi.ss.formula; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.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 @@ -38,152 +39,138 @@ import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; */ final class EvaluationCache { - private final Map _entriesByLocation; - private final Map _consumingCellsByUsedCells; + private final PlainCellCache _plainCellCache; + private final FormulaCellCache _formulaCellCache; /** only used for testing. <code>null</code> otherwise */ - private final IEvaluationListener _evaluationListener; + final IEvaluationListener _evaluationListener; /* package */EvaluationCache(IEvaluationListener evaluationListener) { _evaluationListener = evaluationListener; - _entriesByLocation = new HashMap(); - _consumingCellsByUsedCells = new HashMap(); - } - - /** - * @param cellLoc never <code>null</code> - * @return only ever <code>null</code> for formula cells that have had their cached value cleared - */ - public ValueEval getValue(CellLocation cellLoc) { - return getEntry(cellLoc).getValue(); + _plainCellCache = new PlainCellCache(); + _formulaCellCache = new FormulaCellCache(); } - /** - * @param cellLoc - * @param usedCells never <code>null</code>, (possibly zero length) array of all cells actually - * directly used when evaluating the formula - * @param isPlainValue pass <code>true</code> if cellLoc refers to a plain value (non-formula) - * cell, <code>false</code> for a formula cell. - * @param value the value of a non-formula cell or the result of evaluating the cell formula - * Pass <code>null</code> to signify clearing the cached result of a formula cell) - */ - public void setValue(CellLocation cellLoc, boolean isPlainValue, - CellLocation[] usedCells, ValueEval value) { + public void notifyUpdateCell(int bookIndex, int sheetIndex, EvaluationCell cell) { + FormulaCellCacheEntry fcce = _formulaCellCache.get(cell); + + Loc loc = new Loc(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex()); + PlainValueCellCacheEntry pcce = _plainCellCache.get(loc); - CellCacheEntry existingEntry = (CellCacheEntry) _entriesByLocation.get(cellLoc); - if (existingEntry != null && existingEntry.getValue() != null) { - if (isPlainValue) { - // can set a plain cell cached value any time - } else if (value == null) { - // or clear the cached value of a formula cell at any time + if (cell.getCellType() == HSSFCell.CELL_TYPE_FORMULA) { + if (fcce == null) { + if (pcce == null) { + updateAnyBlankReferencingFormulas(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex()); + } + fcce = new FormulaCellCacheEntry(); + _formulaCellCache.put(cell, fcce); } else { - // but a formula cached value would only be getting updated if the cache didn't have a value to start with - throw new IllegalStateException("Already have cached value for this cell: " - + cellLoc.formatAsString()); + fcce.recurseClearCachedFormulaResults(_evaluationListener); + fcce.clearFormulaEntry(); } - } - sortCellLocationsForLogging(usedCells); - CellCacheEntry entry = getEntry(cellLoc); - CellLocation[] consumingFormulaCells = entry.getConsumingCells(); - CellLocation[] prevUsedCells = entry.getUsedCells(); - if (isPlainValue) { - - if(!entry.updatePlainValue(value)) { - return; + if (pcce == null) { + // was formula cell before - no change of type + } else { + // changing from plain cell to formula cell + pcce.recurseClearCachedFormulaResults(_evaluationListener); + _plainCellCache.remove(loc); } } else { - entry.setFormulaResult(value, usedCells); - for (int i = 0; i < usedCells.length; i++) { - getEntry(usedCells[i]).addConsumingCell(cellLoc); + ValueEval value = WorkbookEvaluator.getValueFromNonFormulaCell(cell); + if (pcce == null) { + if (fcce == null) { + updateAnyBlankReferencingFormulas(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex()); + } + pcce = new PlainValueCellCacheEntry(value); + _plainCellCache.put(loc, pcce); + if (_evaluationListener != null) { + _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); } } - // need to tell all cells that were previously used, but no longer are, - // that they are not consumed by this cell any more - unlinkConsumingCells(prevUsedCells, usedCells, cellLoc); - - // clear all cells that directly or indirectly depend on this one - recurseClearCachedFormulaResults(consumingFormulaCells, 0); } - /** - * 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 updateAnyBlankReferencingFormulas(int bookIndex, int sheetIndex, + final int rowIndex, final int columnIndex) { + final BookSheetKey bsk = new BookSheetKey(bookIndex, sheetIndex); + _formulaCellCache.applyOperation(new IEntryOperation() { - private void unlinkConsumingCells(CellLocation[] prevUsedCells, CellLocation[] usedCells, - CellLocation cellLoc) { - if (prevUsedCells == null) { - return; - } - int nPrevUsed = prevUsedCells.length; - if (nPrevUsed < 1) { - return; - } - int nUsed = usedCells.length; - Set usedSet; - if (nUsed < 1) { - usedSet = Collections.EMPTY_SET; - } else { - usedSet = new HashSet(nUsed * 3 / 2); - for (int i = 0; i < nUsed; i++) { - usedSet.add(usedCells[i]); - } - } - for (int i = 0; i < nPrevUsed; i++) { - CellLocation prevUsed = prevUsedCells[i]; - if (!usedSet.contains(prevUsed)) { - // previously was used by cellLoc, but not anymore - getEntry(prevUsed).clearConsumingCell(cellLoc); - if (_evaluationListener != null) { - //TODO _evaluationListener.onUnconsume(prevUsed.getSheetIndex(), etc) - } + public void processEntry(FormulaCellCacheEntry entry) { + entry.notifyUpdatedBlankCell(bsk, rowIndex, columnIndex, _evaluationListener); } - } - + }); } - /** - * Calls formulaCell.setFormulaResult(null, null) recursively all the way up the tree of - * dependencies. Calls usedCell.clearConsumingCell(fc) for each child of a cell that is - * cleared along the way. - * @param formulaCells - */ - private void recurseClearCachedFormulaResults(CellLocation[] formulaCells, int depth) { - sortCellLocationsForLogging(formulaCells); - int nextDepth = depth+1; - for (int i = 0; i < formulaCells.length; i++) { - CellLocation fc = formulaCells[i]; - CellCacheEntry formulaCell = getEntry(fc); - CellLocation[] usedCells = formulaCell.getUsedCells(); - if (usedCells != null) { - for (int j = 0; j < usedCells.length; j++) { - CellCacheEntry usedCell = getEntry(usedCells[j]); - usedCell.clearConsumingCell(fc); - } + public PlainValueCellCacheEntry getPlainValueEntry(int bookIndex, int sheetIndex, + int rowIndex, int columnIndex, ValueEval value) { + + Loc loc = new Loc(bookIndex, sheetIndex, rowIndex, columnIndex); + PlainValueCellCacheEntry result = _plainCellCache.get(loc); + 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) { - ValueEval value = formulaCell.getValue(); - _evaluationListener.onClearDependentCachedValue(fc.getSheetIndex(), fc.getRowIndex(), fc.getColumnIndex(), value, nextDepth); + _evaluationListener.onCacheHit(sheetIndex, rowIndex, columnIndex, value); } - formulaCell.setFormulaResult(null, null); - recurseClearCachedFormulaResults(formulaCell.getConsumingCells(), nextDepth); } + return result; } - - private CellCacheEntry getEntry(CellLocation cellLoc) { - CellCacheEntry result = (CellCacheEntry)_entriesByLocation.get(cellLoc); + 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 CellCacheEntry(); - _entriesByLocation.put(cellLoc, result); + + result = new FormulaCellCacheEntry(); + _formulaCellCache.put(cell, result); } return result; } + /** * Should be called whenever there are changes to input cells in the evaluated workbook. */ @@ -191,34 +178,28 @@ final class EvaluationCache { if(_evaluationListener != null) { _evaluationListener.onClearWholeCache(); } - _entriesByLocation.clear(); - _consumingCellsByUsedCells.clear(); + _plainCellCache.clear(); + _formulaCellCache.clear(); } - - - private static final Comparator CellLocationComparator = new Comparator() { - - public int compare(Object a, Object b) { - CellLocation clA = (CellLocation) a; - CellLocation clB = (CellLocation) b; - - int cmp; - cmp = clA.getBookIndex() - clB.getBookIndex(); - if (cmp != 0) { - return cmp; - } - cmp = clA.getSheetIndex() - clB.getSheetIndex(); - if (cmp != 0) { - return cmp; - } - cmp = clA.getRowIndex() - clB.getRowIndex(); - if (cmp != 0) { - return cmp; + public void notifyDeleteCell(int bookIndex, int sheetIndex, EvaluationCell cell) { + + if (cell.getCellType() == HSSFCell.CELL_TYPE_FORMULA) { + FormulaCellCacheEntry fcce = _formulaCellCache.remove(cell); + if (fcce == null) { + // formula cell has not been evaluated yet + } else { + fcce.setSensitiveInputCells(null); + fcce.recurseClearCachedFormulaResults(_evaluationListener); } - cmp = clA.getColumnIndex() - clB.getColumnIndex(); + } else { + Loc loc = new Loc(bookIndex, sheetIndex, cell.getRowIndex(), cell.getColumnIndex()); + PlainValueCellCacheEntry pcce = _plainCellCache.get(loc); - return cmp; + if (pcce == null) { + // cache entry doesn't exist. nothing to do + } else { + pcce.recurseClearCachedFormulaResults(_evaluationListener); + } } - - }; + } } diff --git a/src/java/org/apache/poi/ss/formula/EvaluationCell.java b/src/java/org/apache/poi/ss/formula/EvaluationCell.java new file mode 100644 index 0000000000..c15754b921 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/EvaluationCell.java @@ -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(); +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationSheet.java b/src/java/org/apache/poi/ss/formula/EvaluationSheet.java new file mode 100644 index 0000000000..5dec3aacda --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/EvaluationSheet.java @@ -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); +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationTracker.java b/src/java/org/apache/poi/ss/formula/EvaluationTracker.java index 834941e9be..eda62da8b8 100755 --- a/src/java/org/apache/poi/ss/formula/EvaluationTracker.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationTracker.java @@ -22,7 +22,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.BlankEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.usermodel.HSSFCell; @@ -32,11 +32,11 @@ import org.apache.poi.hssf.usermodel.HSSFCell; * The main purpose of this class is to detect an attempt to evaluate a cell * that is already being evaluated. In other words, it detects circular * references in spreadsheet formulas. - * + * * @author Josh Micich */ final class EvaluationTracker { - + // TODO - consider deleting this class and letting CellEvaluationFrame take care of itself private final List _evaluationFrames; private final Set _currentlyEvaluatingCells; private final EvaluationCache _cache; @@ -50,47 +50,51 @@ final class EvaluationTracker { /** * Notifies this evaluation tracker that evaluation of the specified cell is * about to start.<br/> - * + * * In the case of a <code>true</code> return code, the caller should * continue evaluation of the specified cell, and also be sure to call * <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 - * <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/> - * @return <code>true</code> if the specified cell has not been visited yet in the current - * evaluation. <code>false</code> if the specified cell is already being evaluated. + * @return <code>false</code> if the specified cell is already being evaluated */ - public ValueEval startEvaluate(CellLocation cellLoc) { - if (cellLoc == null) { + public boolean startEvaluate(FormulaCellCacheEntry cce) { + if (cce == null) { throw new IllegalArgumentException("cellLoc must not be null"); } - if (_currentlyEvaluatingCells.contains(cellLoc)) { - return ErrorEval.CIRCULAR_REF_ERROR; + if (_currentlyEvaluatingCells.contains(cce)) { + return false; } - ValueEval result = _cache.getValue(cellLoc); - if (result == null) { - _currentlyEvaluatingCells.add(cellLoc); - _evaluationFrames.add(new CellEvaluationFrame(cellLoc)); + _currentlyEvaluatingCells.add(cce); + _evaluationFrames.add(new CellEvaluationFrame(cce)); + return true; + } + + 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 - * cell is complete. <p/> - * - * Every successful call to <tt>startEvaluate</tt> must be followed by a - * call to <tt>endEvaluate</tt> (recommended in a finally block) to enable + * Notifies this evaluation tracker that the evaluation of the specified cell is complete. <p/> + * + * Every successful call to <tt>startEvaluate</tt> must be followed by a 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/> - * + * * Assuming a well behaved client, parameters to this method would not be * required. However, they have been included to assert correct behaviour, * 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(); if (nFrames < 1) { @@ -99,32 +103,40 @@ final class EvaluationTracker { nFrames--; CellEvaluationFrame frame = (CellEvaluationFrame) _evaluationFrames.get(nFrames); - CellLocation coordinates = frame.getCoordinates(); - if (!coordinates.equals(cellLoc)) { - throw new RuntimeException("Wrong cell specified. " - + "Corresponding startEvaluate() call was for cell {" - + coordinates.formatAsString() + "} this endEvaluate() call is for cell {" - + cellLoc.formatAsString() + "}"); + if (cce != frame.getCCE()) { + throw new IllegalStateException("Wrong cell specified. "); } - // else - no problems so pop current frame + // else - no problems so pop current frame _evaluationFrames.remove(nFrames); - _currentlyEvaluatingCells.remove(coordinates); - - // TODO - don't cache results of volatile formulas - _cache.setValue(coordinates, isPlainValueCell, frame.getUsedCells(), result); + _currentlyEvaluatingCells.remove(cce); } - /** - * Tells the currently evaluating cell frame that it has a dependency on the specified - * <tt>usedCell<tt> - * @param usedCell location of cell which is referenced (and used) by the current cell. - */ - public void acceptDependency(CellLocation usedCell) { + + public void acceptFormulaDependency(CellCacheEntry cce) { + // Tell the currently evaluating cell frame that it has a dependency on the specified int prevFrameIndex = _evaluationFrames.size()-1; 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); + } + } + } } diff --git a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java index 6cf938fe9f..45b0c918f2 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java @@ -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.NameXPtg;
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/>
*
@@ -34,10 +33,10 @@ public interface EvaluationWorkbook { /**
* @return -1 if the specified sheet is from a different book
*/
- int getSheetIndex(Sheet sheet);
+ int getSheetIndex(EvaluationSheet sheet);
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
@@ -46,7 +45,7 @@ public interface EvaluationWorkbook { int convertFromExternSheetIndex(int externSheetIndex);
EvaluationName getName(NamePtg namePtg);
String resolveNameXText(NameXPtg ptg);
- Ptg[] getFormulaTokens(Cell cell);
+ Ptg[] getFormulaTokens(EvaluationCell cell);
class ExternalSheet {
private final String _workbookName;
diff --git a/src/java/org/apache/poi/ss/formula/FormulaCellCache.java b/src/java/org/apache/poi/ss/formula/FormulaCellCache.java new file mode 100644 index 0000000000..4a1fce6b80 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/FormulaCellCache.java @@ -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()); + } + } +} diff --git a/src/java/org/apache/poi/ss/formula/FormulaCellCacheEntry.java b/src/java/org/apache/poi/ss/formula/FormulaCellCacheEntry.java new file mode 100644 index 0000000000..f0fd5c3a76 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/FormulaCellCacheEntry.java @@ -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); + } + } + } +}
\ No newline at end of file diff --git a/src/java/org/apache/poi/ss/formula/FormulaCellCacheEntrySet.java b/src/java/org/apache/poi/ss/formula/FormulaCellCacheEntrySet.java new file mode 100644 index 0000000000..780d2661e2 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/FormulaCellCacheEntrySet.java @@ -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; + } + +} diff --git a/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java b/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java new file mode 100644 index 0000000000..7447edca0f --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java @@ -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); + } +} diff --git a/src/java/org/apache/poi/ss/formula/IEvaluationListener.java b/src/java/org/apache/poi/ss/formula/IEvaluationListener.java index deba4d59b0..caf254edff 100644 --- a/src/java/org/apache/poi/ss/formula/IEvaluationListener.java +++ b/src/java/org/apache/poi/ss/formula/IEvaluationListener.java @@ -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.eval.ValueEval; +import org.apache.poi.hssf.usermodel.HSSFCell; /** * 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 */ 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 onReadPlainValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value); - void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs); - void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result); + void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ICacheEntry entry); + void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs); + void onEndEvaluate(ICacheEntry entry, ValueEval result); void onClearWholeCache(); - void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value); - void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value, int depth); + void onClearCachedValue(ICacheEntry entry); + /** + * 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); } diff --git a/src/java/org/apache/poi/ss/formula/PlainCellCache.java b/src/java/org/apache/poi/ss/formula/PlainCellCache.java new file mode 100644 index 0000000000..2e6476f17d --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/PlainCellCache.java @@ -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); + } +} diff --git a/src/java/org/apache/poi/ss/formula/PlainValueCellCacheEntry.java b/src/java/org/apache/poi/ss/formula/PlainValueCellCacheEntry.java new file mode 100644 index 0000000000..d1ffb51fa3 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/PlainValueCellCacheEntry.java @@ -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); + } +}
\ No newline at end of file diff --git a/src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java b/src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java index 4f46e31a21..bf636a4da3 100644 --- a/src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/SheetRefEvaluator.java @@ -18,7 +18,6 @@ package org.apache.poi.ss.formula;
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 EvaluationTracker _tracker;
- private final Sheet _sheet;
+ private final EvaluationSheet _sheet;
private final int _sheetIndex;
public SheetRefEvaluator(WorkbookEvaluator bookEvaluator, EvaluationTracker tracker,
diff --git a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index 41a7c72fae..ef5975d3e6 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -63,8 +63,6 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.util.CellReference; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; 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/> @@ -131,7 +129,7 @@ public final class WorkbookEvaluator { /* package */ IEvaluationListener getEvaluationListener() { return _evaluationListener; } - + /** * Should be called whenever there are changes to input cells in the evaluated workbook. * Failure to call this method after changing cell values will cause incorrect behaviour @@ -143,29 +141,23 @@ public final class WorkbookEvaluator { } /** - * Sets the cached value for a plain (non-formula) cell. - * @param never <code>null</code>. Use {@link BlankEval#INSTANCE} when the cell is being - * cleared. Otherwise an instance of {@link NumberEval}, {@link StringEval}, {@link BoolEval} - * or {@link ErrorEval} to represent a plain cell value. + * Should be called to tell the cell value cache that the specified (value or formula) cell + * has changed. */ - public void setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value) { - if (value == null) { - throw new IllegalArgumentException("value must not be null"); - } - int sheetIndex = getSheetIndex(sheet); - _cache.setValue(getCellLoc(sheetIndex, rowIndex, columnIndex), true, CellLocation.EMPTY_ARRAY, value); - + public void notifyUpdateCell(EvaluationCell cell) { + int sheetIndex = getSheetIndex(cell.getSheet()); + _cache.notifyUpdateCell(_workbookIx, sheetIndex, cell); } /** - * 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 + * Should be called to tell the cell value cache that the specified cell has just been + * deleted. */ - public void notifySetFormula(Sheet sheet, int rowIndex, int columnIndex) { - int sheetIndex = getSheetIndex(sheet); - _cache.setValue(getCellLoc(sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null); - + public void notifyDeleteCell(EvaluationCell cell) { + int sheetIndex = getSheetIndex(cell.getSheet()); + _cache.notifyDeleteCell(_workbookIx, sheetIndex, cell); } - private int getSheetIndex(Sheet sheet) { + + private int getSheetIndex(EvaluationSheet sheet) { Integer result = (Integer) _sheetIndexesBySheet.get(sheet); if (result == null) { int sheetIndex = _workbook.getSheetIndex(sheet); @@ -178,78 +170,76 @@ public final class WorkbookEvaluator { return result.intValue(); } - public ValueEval evaluate(Cell srcCell) { + public ValueEval evaluate(EvaluationCell srcCell) { int sheetIndex = getSheetIndex(srcCell.getSheet()); - CellLocation cellLoc = getCellLoc(sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum()); - return internalEvaluate(srcCell, cellLoc, new EvaluationTracker(_cache)); + return evaluateAny(srcCell, sheetIndex, srcCell.getRowIndex(), srcCell.getColumnIndex(), new EvaluationTracker(_cache)); } + /** * @return never <code>null</code>, never {@link BlankEval} */ - private ValueEval internalEvaluate(Cell srcCell, CellLocation cellLoc, EvaluationTracker tracker) { - int sheetIndex = cellLoc.getSheetIndex(); - int rowIndex = cellLoc.getRowIndex(); - int columnIndex = cellLoc.getColumnIndex(); + private ValueEval evaluateAny(EvaluationCell srcCell, int sheetIndex, + int rowIndex, int columnIndex, EvaluationTracker tracker) { - ValueEval result; + if (srcCell == null || srcCell.getCellType() != Cell.CELL_TYPE_FORMULA) { + ValueEval result = getValueFromNonFormulaCell(srcCell); + tracker.acceptPlainValueDependency(_workbookIx, sheetIndex, rowIndex, columnIndex, result); + return result; + } - result = tracker.startEvaluate(cellLoc); + FormulaCellCacheEntry cce = _cache.getOrCreateFormulaCellEntry(srcCell); + tracker.acceptFormulaDependency(cce); IEvaluationListener evalListener = _evaluationListener; - if (result != null) { - if(evalListener != null) { - evalListener.onCacheHit(sheetIndex, rowIndex, columnIndex, result); + if (cce.getValue() == null) { + if (!tracker.startEvaluate(cce)) { + return ErrorEval.CIRCULAR_REF_ERROR; } - return result; - } - boolean isPlainFormulaCell = false; - try { - result = getValueFromNonFormulaCell(srcCell); - if (result != null) { - isPlainFormulaCell = true; - if(evalListener != null) { - evalListener.onReadPlainValue(sheetIndex, rowIndex, columnIndex, result); - } - } else { - isPlainFormulaCell = false; + try { + ValueEval result; + Ptg[] ptgs = _workbook.getFormulaTokens(srcCell); - if(evalListener == null) { - result = evaluateFormula(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker); + if (evalListener == null) { + result = evaluateFormula(sheetIndex, rowIndex, columnIndex, ptgs, tracker); } else { - evalListener.onStartEvaluate(sheetIndex, rowIndex, columnIndex, ptgs); - result = evaluateFormula(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker); - evalListener.onEndEvaluate(sheetIndex, rowIndex, columnIndex, result); + 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()); } - } finally { - tracker.endEvaluate(cellLoc, result, isPlainFormulaCell); + return cce.getValue(); } if (isDebugLogEnabled()) { String sheetName = getSheetName(sheetIndex); 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. * @param cell may be <code>null</code> - * @return {@link BlankEval} if cell is <code>null</code> or blank, <code>null</code> if cell - * is a formula cell. + * @return {@link BlankEval} if cell is <code>null</code> or blank, never <code>null</code> */ - private static ValueEval getValueFromNonFormulaCell(Cell cell) { + /* package */ static ValueEval getValueFromNonFormulaCell(EvaluationCell cell) { if (cell == null) { return BlankEval.INSTANCE; } int cellType = cell.getCellType(); switch (cellType) { - case Cell.CELL_TYPE_FORMULA: - return null; case Cell.CELL_TYPE_NUMERIC: return new NumberEval(cell.getNumericCellValue()); case Cell.CELL_TYPE_STRING: - return new StringEval(cell.getRichStringCellValue().getString()); + return new StringEval(cell.getStringCellValue()); case Cell.CELL_TYPE_BOOLEAN: return BoolEval.valueOf(cell.getBooleanCellValue()); case Cell.CELL_TYPE_BLANK: @@ -273,7 +263,7 @@ public final class WorkbookEvaluator { // Excel prefers to encode 'SUM()' as a tAttr token, but this evaluator // expects the equivalent function token byte nArgs = 1; // tAttrSum always has 1 parameter - ptg = new FuncVarPtg("SUM", nArgs); + ptg = new FuncVarPtg("SUM", nArgs); } } if (ptg instanceof ControlPtg) { @@ -381,7 +371,7 @@ public final class WorkbookEvaluator { } int otherSheetIndex = _workbook.convertFromExternSheetIndex(externSheetIndex); return new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); - + } /** @@ -428,7 +418,7 @@ public final class WorkbookEvaluator { if (ptg instanceof MissingArgPtg) { return MissingArgEval.instance; } - if (ptg instanceof AreaErrPtg ||ptg instanceof RefErrorPtg + if (ptg instanceof AreaErrPtg ||ptg instanceof RefErrorPtg || ptg instanceof DeletedArea3DPtg || ptg instanceof DeletedRef3DPtg) { return ErrorEval.REF_INVALID; } @@ -465,25 +455,14 @@ public final class WorkbookEvaluator { } return getEvalForPtg(ptgs[0], sheetIndex, tracker); } - + /** * Used by the lazy ref evals whenever they need to get the value of a contained cell. */ - /* package */ ValueEval evaluateReference(Sheet sheet, int sheetIndex, int rowIndex, + /* package */ ValueEval evaluateReference(EvaluationSheet sheet, int sheetIndex, int rowIndex, int columnIndex, EvaluationTracker tracker) { - - Row row = sheet.getRow(rowIndex); - Cell cell; - 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); + + EvaluationCell cell = sheet.getCell(rowIndex, columnIndex); + return evaluateAny(cell, sheetIndex, rowIndex, columnIndex, tracker); } } diff --git a/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java b/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java index 629d4f5a12..15711195b1 100644 --- a/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java +++ b/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java @@ -45,20 +45,17 @@ public interface FormulaEvaluator { */ void clearAllCachedResultValues(); /** - * Sets the cached value for a plain (non-formula) cell. - * Should be called whenever there are changes to individual input cells in the evaluated workbook. + * Should be called to tell the cell value cache that the specified (value or formula) cell + * has changed. * Failure to call this method after changing cell values will cause incorrect behaviour * of the evaluate~ methods of this class - * @param never <code>null</code>. Use {@link BlankEval#INSTANCE} when the cell is being - * cleared. Otherwise an instance of {@link NumberEval}, {@link StringEval}, {@link BoolEval} - * or {@link ErrorEval} to represent a plain cell value. */ - 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 * 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, * else the CellValue simply copies the appropriate cell value from diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java new file mode 100644 index 0000000000..14378849ce --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java @@ -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(); + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java new file mode 100644 index 0000000000..7778b2175f --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java @@ -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); + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java index ddadf96eb9..add5470814 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java @@ -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;
import org.apache.poi.hssf.record.formula.NamePtg;
import org.apache.poi.hssf.record.formula.NameXPtg;
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.EvaluationSheet;
import org.apache.poi.ss.formula.EvaluationWorkbook;
import org.apache.poi.ss.formula.FormulaParser;
import org.apache.poi.ss.formula.FormulaParsingWorkbook;
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;
/**
@@ -67,7 +83,8 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return null;
}
- public int getSheetIndex(Sheet sheet) {
+ public int getSheetIndex(EvaluationSheet evalSheet) {
+ XSSFSheet sheet = ((XSSFEvaluationSheet)evalSheet).getXSSFSheet();
return _uBook.getSheetIndex(sheet);
}
@@ -80,8 +97,8 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E throw new RuntimeException("Not implemented yet");
}
- public Sheet getSheet(int sheetIndex) {
- return _uBook.getSheetAt(sheetIndex);
+ public EvaluationSheet getSheet(int sheetIndex) {
+ return new XSSFEvaluationSheet(_uBook.getSheetAt(sheetIndex));
}
public ExternalSheet getExternalSheet(int externSheetIndex) {
@@ -95,10 +112,6 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return _uBook.getSheetIndex(sheetName);
}
- public Workbook getWorkbook() {
- return _uBook;
- }
-
/**
* TODO - figure out what the hell this methods does in
* HSSF...
@@ -119,7 +132,8 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E int ix = namePtg.getIndex();
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);
return FormulaParser.parse(cell.getCellFormula(), frBook);
}
diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java index 10fb78caf7..19f75aab4a 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFFormulaEvaluator.java @@ -19,14 +19,12 @@ package org.apache.poi.xssf.usermodel; import java.util.Iterator;
-import org.apache.poi.hssf.record.formula.eval.BlankEval;
import org.apache.poi.hssf.record.formula.eval.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
import org.apache.poi.hssf.record.formula.eval.NumberEval;
import org.apache.poi.hssf.record.formula.eval.StringEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.usermodel.HSSFCell;
-import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.ss.formula.WorkbookEvaluator;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue;
@@ -61,11 +59,11 @@ public class XSSFFormulaEvaluator implements FormulaEvaluator { public void clearAllCachedResultValues() {
_bookEvaluator.clearAllCachedResultValues();
}
- public void setCachedPlainValue(Sheet sheet, int rowIndex, int columnIndex, ValueEval value) {
- _bookEvaluator.setCachedPlainValue(sheet, rowIndex, columnIndex, value);
+ public void notifySetFormula(Cell cell) {
+ _bookEvaluator.notifyUpdateCell(new XSSFEvaluationCell((XSSFCell)cell));
}
- public void notifySetFormula(HSSFSheet sheet, int rowIndex, int columnIndex) {
- _bookEvaluator.notifySetFormula(sheet, rowIndex, columnIndex);
+ public void notifyDeleteCell(Cell cell) {
+ _bookEvaluator.notifyDeleteCell(new XSSFEvaluationCell((XSSFCell)cell));
}
/**
@@ -227,7 +225,7 @@ public class XSSFFormulaEvaluator implements FormulaEvaluator { * @param eval
*/
private CellValue evaluateFormulaCellValue(Cell cell) {
- ValueEval eval = _bookEvaluator.evaluate(cell);
+ ValueEval eval = _bookEvaluator.evaluate(new XSSFEvaluationCell((XSSFCell) cell));
if (eval instanceof NumberEval) {
NumberEval ne = (NumberEval) eval;
return new CellValue(ne.getNumberValue());
diff --git a/src/testcases/org/apache/poi/hssf/usermodel/HSSFEvaluationTestHelper.java b/src/testcases/org/apache/poi/hssf/usermodel/HSSFEvaluationTestHelper.java new file mode 100644 index 0000000000..96196c146e --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/HSSFEvaluationTestHelper.java @@ -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); + } + +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorBugs.java b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorBugs.java index baa72743d0..ce57c28594 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorBugs.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorBugs.java @@ -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.Ptg; 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.WorkbookEvaluator; 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(-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 @@ -309,7 +316,7 @@ public final class TestFormulaEvaluatorBugs extends TestCase { public void onCacheHit(int sheetIndex, int srcRowNum, int srcColNum, ValueEval result) { _countCacheHits++; } - public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) { + public void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs) { _countCacheMisses++; } } @@ -341,7 +348,7 @@ public final class TestFormulaEvaluatorBugs extends TestCase { HSSFCell cell = row.getCell(8); EvalListener evalListener = new EvalListener(); WorkbookEvaluator evaluator = WorkbookEvaluatorTestHelper.createEvaluator(wb, evalListener); - evaluator.evaluate(cell); + evaluator.evaluate(HSSFEvaluationTestHelper.wrapCell(cell)); int evalCount = evalListener.getCountCacheMisses(); if (evalCount > 10) { // Without caching, evaluating cell 'A9' takes 21845 evaluations which consumes diff --git a/src/testcases/org/apache/poi/ss/formula/EvaluationListener.java b/src/testcases/org/apache/poi/ss/formula/EvaluationListener.java index e6ae025a85..46bb061722 100644 --- a/src/testcases/org/apache/poi/ss/formula/EvaluationListener.java +++ b/src/testcases/org/apache/poi/ss/formula/EvaluationListener.java @@ -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.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/> @@ -28,26 +29,28 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval; * @author Josh Micich */ 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 } - public void onReadPlainValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value) { + public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ICacheEntry entry) { // do nothing } - public void onStartEvaluate(int sheetIndex, int rowIndex, int columnIndex, Ptg[] ptgs) { + public void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs) { // do nothing } - public void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) { + public void onEndEvaluate(ICacheEntry entry, ValueEval result) { // do nothing } public void onClearWholeCache() { // do nothing } - public void onClearCachedValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) { + public void onClearCachedValue(ICacheEntry entry) { // do nothing } - public void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, - ValueEval value,int depth) { + public void sortDependentCachedValues(ICacheEntry[] entries) { + // do nothing + } + public void onClearDependentCachedValue(ICacheEntry entry, int depth) { // do nothing } } diff --git a/src/testcases/org/apache/poi/ss/formula/TestCellCacheEntry.java b/src/testcases/org/apache/poi/ss/formula/TestCellCacheEntry.java index e8ff03f8b7..d5e75dd2ab 100644 --- a/src/testcases/org/apache/poi/ss/formula/TestCellCacheEntry.java +++ b/src/testcases/org/apache/poi/ss/formula/TestCellCacheEntry.java @@ -17,13 +17,11 @@ 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.ValueEval; -import junit.framework.AssertionFailedError; -import junit.framework.TestCase; - /** * Tests {@link CellCacheEntry}. * @@ -32,22 +30,14 @@ import junit.framework.TestCase; public class TestCellCacheEntry extends TestCase { public void testBasic() { - CellCacheEntry cce = new CellCacheEntry(); - cce.updatePlainValue(new NumberEval(42.0)); - ValueEval ve = cce.getValue(); + CellCacheEntry pcce = new PlainValueCellCacheEntry(new NumberEval(42.0)); + ValueEval ve = pcce.getValue(); assertEquals(42, ((NumberEval)ve).getNumberValue(), 0.0); - cce.setFormulaResult(new NumberEval(10.0), new CellLocation[] { }); - } - - public void testBlank() { - CellCacheEntry cce = new CellCacheEntry(); - 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"); - } + FormulaCellCacheEntry fcce = new FormulaCellCacheEntry(); + fcce.updateFormulaResult(new NumberEval(10.0), CellCacheEntry.EMPTY_ARRAY, null); + + ve = fcce.getValue(); + assertEquals(10, ((NumberEval)ve).getNumberValue(), 0.0); } } diff --git a/src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java b/src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java index e91793d0c3..068c97a255 100644 --- a/src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java +++ b/src/testcases/org/apache/poi/ss/formula/TestEvaluationCache.java @@ -19,7 +19,11 @@ package org.apache.poi.ss.formula; import java.io.PrintStream; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import junit.framework.AssertionFailedError; 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.ValueEval; 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.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; 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 @@ -46,33 +52,89 @@ import org.apache.poi.hssf.util.CellReference; * @author Josh Micich */ 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 final List _logList; private final HSSFWorkbook _book; + private Map _formulaCellsByCacheEntry; + private Map _plainCellLocsByCacheEntry; public EvalListener(HSSFWorkbook wb) { _book = wb; _logList = new ArrayList(); + _formulaCellsByCacheEntry = new HashMap(); + _plainCellLocsByCacheEntry = new HashMap(); } public void onCacheHit(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) { log("hit", rowIndex, columnIndex, result); } - public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ValueEval value) { - log("value", rowIndex, columnIndex, value); + public void onReadPlainValue(int sheetIndex, int rowIndex, int columnIndex, ICacheEntry entry) { + 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) { - log("start", rowIndex, columnIndex, ptgs); + public void onStartEvaluate(EvaluationCell cell, ICacheEntry entry, Ptg[] ptgs) { + _formulaCellsByCacheEntry.put(entry, cell); + log("start", cell.getRowIndex(), cell.getColumnIndex(), ptgs); } - public void onEndEvaluate(int sheetIndex, int rowIndex, int columnIndex, ValueEval result) { - log("end", rowIndex, columnIndex, result); + public void onEndEvaluate(ICacheEntry entry, ValueEval 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) { - log("clear", rowIndex, columnIndex, value); + public void onClearCachedValue(ICacheEntry entry) { + 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 sortDependentCachedValues(ICacheEntry[] entries) { + Arrays.sort(entries, new FormulaCellCacheEntryComparer(_formulaCellsByCacheEntry)); } - public void onClearDependentCachedValue(int sheetIndex, int rowIndex, int columnIndex, - ValueEval value,int depth) { - 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) { StringBuffer sb = new StringBuffer(64); @@ -139,19 +201,23 @@ public class TestEvaluationCache extends TestCase { _sheet = _wb.createSheet("Sheet1"); } + private static EvaluationCell wrapCell(HSSFCell cell) { + return HSSFEvaluationTestHelper.wrapCell(cell); + } + public void setCellValue(String cellRefText, double value) { HSSFCell cell = getOrCreateCell(cellRefText); // be sure to blank cell, in case it is currently a formula cell.setCellType(HSSFCell.CELL_TYPE_BLANK); // otherwise this line will only set the formula cached result; cell.setCellValue(value); - _evaluator.setCachedPlainValue(_sheet, cell.getRowIndex(), cell.getCellNum(), new NumberEval(value)); + _evaluator.notifyUpdateCell(wrapCell(cell)); } public void setCellFormula(String cellRefText, String formulaText) { HSSFCell cell = getOrCreateCell(cellRefText); cell.setCellFormula(formulaText); - _evaluator.notifySetFormula(_sheet, cell.getRowIndex(), cell.getCellNum()); + _evaluator.notifyUpdateCell(wrapCell(cell)); } private HSSFCell getOrCreateCell(String cellRefText) { @@ -170,7 +236,7 @@ public class TestEvaluationCache extends TestCase { } public ValueEval evaluateCell(String cellRefText) { - return _evaluator.evaluate(getOrCreateCell(cellRefText)); + return _evaluator.evaluate(wrapCell(getOrCreateCell(cellRefText))); } public String[] getAndClearLog() { @@ -243,6 +309,7 @@ public class TestEvaluationCache extends TestCase { // change a low level cell ms.setCellValue("D1", 10); confirmLog(ms, new String[] { + "clear D1 10", "clear1 C1 54", "clear2 B1 8", "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 confirmLog(ms, new String[] { + "clear B3 3", "clear1 B2 46", "clear2 A1 46", }); @@ -296,6 +364,7 @@ public class TestEvaluationCache extends TestCase { ms.getAndClearLog(); ms.setCellFormula("B2", "B3*C2-C3"); // used to be "B3*C1-C2" confirmLog(ms, new String[] { + "clear B2 46", "clear1 A1 46", }); @@ -307,7 +376,8 @@ public class TestEvaluationCache extends TestCase { "hit B3 2", "hit C2 62", "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 B2 91", "end A1 91", @@ -320,6 +390,7 @@ public class TestEvaluationCache extends TestCase { // Now change a value that should no longer affect B2 ms.setCellValue("D1", 11); confirmLog(ms, new String[] { + "clear D1 11", "clear1 C1 54", // note there is no "clear2 B2 91" here because B2 doesn't depend on C1 anymore "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 - * only when the palin cell's value actually changes + * only when the plain cell's value actually changes */ public void testRedundantUpdate() { MySheet ms = new MySheet(); @@ -369,6 +440,7 @@ public class TestEvaluationCache extends TestCase { ms.setCellValue("B1", 11); // value changing confirmLog(ms, new String[] { + "clear B1 11", "clear1 A1 25", // expect consuming formula cached result to get cleared }); confirmEvaluate(ms, "A1", 24); @@ -434,9 +506,55 @@ public class TestEvaluationCache extends TestCase { "hit D1 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) { ValueEval v = ms.evaluateCell(cellRefText); assertEquals(NumberEval.class, v.getClass()); diff --git a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java index 0d3e93ee7a..47ceb2686f 100644 --- a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java +++ b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java @@ -127,18 +127,21 @@ public class TestWorkbookEvaluator extends TestCase { confirmEvaluation(264, evaluatorA, cell); // change [wbB]BSheet1!B3 (from 50 to 60) - bSheet1.getRow(2).getCell(1).setCellValue(60); - evaluatorB.setCachedPlainValue(bSheet1, 2, 1, new NumberEval(60)); + HSSFCell cellB3 = bSheet1.getRow(2).getCell(1); + cellB3.setCellValue(60); + evaluatorB.notifyUpdateCell(cellB3); confirmEvaluation(274, evaluatorA, cell); // change [wbA]ASheet1!A3 (from 100 to 80) - aSheet1.getRow(2).getCell(0).setCellValue(80); - evaluatorA.setCachedPlainValue(aSheet1, 2, 0, new NumberEval(80)); + HSSFCell cellA3 = aSheet1.getRow(2).getCell(0); + cellA3.setCellValue(80); + evaluatorA.notifyUpdateCell(cellA3); confirmEvaluation(234, evaluatorA, cell); // change [wbA]AnotherSheet!A1 (from 2 to 3) - wbA.getSheetAt(1).getRow(0).getCell(0).setCellValue(3); - evaluatorA.setCachedPlainValue(wbA.getSheetAt(1), 0, 0, new NumberEval(3)); + HSSFCell cellA1 = wbA.getSheetAt(1).getRow(0).getCell(0); + cellA1.setCellValue(3); + evaluatorA.notifyUpdateCell(cellA1); confirmEvaluation(235, evaluatorA, cell); } |