diff --git a/src/java/org/apache/poi/hssf/record/formula/eval/LazyAreaEval.java b/src/java/org/apache/poi/hssf/record/formula/eval/LazyAreaEval.java index e43a97e405..e2dc42e056 100644 --- a/src/java/org/apache/poi/hssf/record/formula/eval/LazyAreaEval.java +++ b/src/java/org/apache/poi/hssf/record/formula/eval/LazyAreaEval.java @@ -23,7 +23,6 @@ import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; 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; /** @@ -33,12 +32,12 @@ import org.apache.poi.hssf.util.CellReference; public final class LazyAreaEval extends AreaEvalBase { private final HSSFSheet _sheet; - private HSSFWorkbook _workbook; + private HSSFFormulaEvaluator _evaluator; - public LazyAreaEval(AreaI ptg, HSSFSheet sheet, HSSFWorkbook workbook) { + public LazyAreaEval(AreaI ptg, HSSFSheet sheet, HSSFFormulaEvaluator evaluator) { super(ptg); _sheet = sheet; - _workbook = workbook; + _evaluator = evaluator; } public ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex) { @@ -54,21 +53,21 @@ public final class LazyAreaEval extends AreaEvalBase { if (cell == null) { return BlankEval.INSTANCE; } - return HSSFFormulaEvaluator.getEvalForCell(cell, _sheet, _workbook); + return _evaluator.getEvalForCell(cell, _sheet); } public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) { AreaI area = new OffsetArea(getFirstRow(), getFirstColumn(), relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx); - return new LazyAreaEval(area, _sheet, _workbook); + return new LazyAreaEval(area, _sheet, _evaluator); } public String toString() { CellReference crA = new CellReference(getFirstRow(), getFirstColumn()); CellReference crB = new CellReference(getLastRow(), getLastColumn()); StringBuffer sb = new StringBuffer(); sb.append(getClass().getName()).append("["); - String sheetName = _workbook.getSheetName(_workbook.getSheetIndex(_sheet)); + String sheetName = _evaluator.getSheetName(_sheet); sb.append(sheetName); sb.append('!'); sb.append(crA.formatAsString()); diff --git a/src/java/org/apache/poi/hssf/record/formula/eval/LazyRefEval.java b/src/java/org/apache/poi/hssf/record/formula/eval/LazyRefEval.java index 4841725249..436bb4ccbe 100644 --- a/src/java/org/apache/poi/hssf/record/formula/eval/LazyRefEval.java +++ b/src/java/org/apache/poi/hssf/record/formula/eval/LazyRefEval.java @@ -18,14 +18,13 @@ package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.AreaI; -import org.apache.poi.hssf.record.formula.AreaI.OffsetArea; import org.apache.poi.hssf.record.formula.Ref3DPtg; import org.apache.poi.hssf.record.formula.RefPtg; +import org.apache.poi.hssf.record.formula.AreaI.OffsetArea; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; 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; /** @@ -35,18 +34,18 @@ import org.apache.poi.hssf.util.CellReference; public final class LazyRefEval extends RefEvalBase { private final HSSFSheet _sheet; - private final HSSFWorkbook _workbook; + private final HSSFFormulaEvaluator _evaluator; - public LazyRefEval(RefPtg ptg, HSSFSheet sheet, HSSFWorkbook workbook) { + public LazyRefEval(RefPtg ptg, HSSFSheet sheet, HSSFFormulaEvaluator evaluator) { super(ptg.getRow(), ptg.getColumn()); _sheet = sheet; - _workbook = workbook; + _evaluator = evaluator; } - public LazyRefEval(Ref3DPtg ptg, HSSFSheet sheet, HSSFWorkbook workbook) { + public LazyRefEval(Ref3DPtg ptg, HSSFSheet sheet, HSSFFormulaEvaluator evaluator) { super(ptg.getRow(), ptg.getColumn()); _sheet = sheet; - _workbook = workbook; + _evaluator = evaluator; } public ValueEval getInnerValueEval() { @@ -61,7 +60,7 @@ public final class LazyRefEval extends RefEvalBase { if (cell == null) { return BlankEval.INSTANCE; } - return HSSFFormulaEvaluator.getEvalForCell(cell, _sheet, _workbook); + return _evaluator.getEvalForCell(cell, _sheet); } public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) { @@ -69,14 +68,14 @@ public final class LazyRefEval extends RefEvalBase { AreaI area = new OffsetArea(getRow(), getColumn(), relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx); - return new LazyAreaEval(area, _sheet, _workbook); + return new LazyAreaEval(area, _sheet, _evaluator); } public String toString() { CellReference cr = new CellReference(getRow(), getColumn()); StringBuffer sb = new StringBuffer(); sb.append(getClass().getName()).append("["); - String sheetName = _workbook.getSheetName(_workbook.getSheetIndex(_sheet)); + String sheetName = _evaluator.getSheetName(_sheet); sb.append(sheetName); sb.append('!'); sb.append(cr.formatAsString()); diff --git a/src/java/org/apache/poi/hssf/usermodel/EvaluationCache.java b/src/java/org/apache/poi/hssf/usermodel/EvaluationCache.java new file mode 100644 index 0000000000..97c4d6eed3 --- /dev/null +++ b/src/java/org/apache/poi/hssf/usermodel/EvaluationCache.java @@ -0,0 +1,95 @@ +/* ==================================================================== + 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 java.util.HashMap; +import java.util.Map; + +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +/** + * Performance optimisation for {@link HSSFFormulaEvaluator}. This class stores previously + * calculated values of already visited cells, to avoid unnecessary re-calculation when the + * same cells are referenced multiple times + * + * + * @author Josh Micich + */ +final class EvaluationCache { + private static final class Key { + + private final int _sheetIndex; + private final int _srcRowNum; + private final int _srcColNum; + private final int _hashCode; + + public Key(int sheetIndex, int srcRowNum, int srcColNum) { + _sheetIndex = sheetIndex; + _srcRowNum = srcRowNum; + _srcColNum = srcColNum; + _hashCode = sheetIndex + srcRowNum + srcColNum; + } + + public int hashCode() { + return _hashCode; + } + + public boolean equals(Object obj) { + Key other = (Key) obj; + if (_hashCode != other._hashCode) { + return false; + } + if (_sheetIndex != other._sheetIndex) { + return false; + } + if (_srcRowNum != other._srcRowNum) { + return false; + } + if (_srcColNum != other._srcColNum) { + return false; + } + return true; + } + } + + private final Map _valuesByKey; + + /* package */EvaluationCache() { + _valuesByKey = new HashMap(); + } + + public ValueEval getValue(int sheetIndex, int srcRowNum, int srcColNum) { + Key key = new Key(sheetIndex, srcRowNum, srcColNum); + return (ValueEval) _valuesByKey.get(key); + } + + public void setValue(int sheetIndex, int srcRowNum, int srcColNum, ValueEval value) { + Key key = new Key(sheetIndex, srcRowNum, srcColNum); + if (_valuesByKey.containsKey(key)) { + throw new RuntimeException("Already have cached value for this cell"); + } + _valuesByKey.put(key, value); + } + + /** + * Should be called whenever there are changes to input cells in the evaluated workbook. + */ + public void clear() { + _valuesByKey.clear(); + } +} diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index 55cd04ee11..be0262292f 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -56,19 +56,67 @@ import org.apache.poi.hssf.record.formula.eval.OperationEval; import org.apache.poi.hssf.record.formula.eval.RefEval; import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.util.CellReference; /** + * Evaluates formula cells.
+ * + * For performance reasons, this class keeps a cache of all previously calculated intermediate + * cell values. Be sure to call {@link #clearCache()} if any workbook cells are changed between + * calls to evaluate~ methods on this class. + * * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * + * @author Josh Micich */ public class HSSFFormulaEvaluator { + /** + * used to track the number of evaluations + */ + private static final class Counter { + public int value; + public Counter() { + value = 0; + } + } + private final HSSFSheet _sheet; private final HSSFWorkbook _workbook; + private final EvaluationCache _cache; + + private Counter _evaluationCounter; public HSSFFormulaEvaluator(HSSFSheet sheet, HSSFWorkbook workbook) { + this(sheet, workbook, new EvaluationCache(), new Counter()); + } + + private HSSFFormulaEvaluator(HSSFSheet sheet, HSSFWorkbook workbook, EvaluationCache cache, Counter evaluationCounter) { _sheet = sheet; _workbook = workbook; + _cache = cache; + _evaluationCounter = evaluationCounter; + } + + /** + * for debug use. Used in toString methods + */ + public String getSheetName(HSSFSheet sheet) { + return _workbook.getSheetName(_workbook.getSheetIndex(sheet)); + } + /** + * for debug/test use + */ + /* package */ int getEvaluationCount() { + return _evaluationCounter.value; + } + + private static boolean isDebugLogEnabled() { + return false; + } + private static void logDebug(String s) { + if (isDebugLogEnabled()) { + System.out.println(s); + } } /** @@ -82,6 +130,14 @@ public class HSSFFormulaEvaluator { } } + /** + * 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 + * of the evaluate~ methods of this class + */ + public void clearCache() { + _cache.clear(); + } /** * Returns an underlying FormulaParser, for the specified @@ -118,7 +174,7 @@ public class HSSFFormulaEvaluator { retval.setErrorValue(cell.getErrorCellValue()); break; case HSSFCell.CELL_TYPE_FORMULA: - retval = getCellValueForEval(internalEvaluate(cell, _sheet, _workbook)); + retval = getCellValueForEval(internalEvaluate(cell, _sheet)); break; case HSSFCell.CELL_TYPE_NUMERIC: retval = new CellValue(HSSFCell.CELL_TYPE_NUMERIC); @@ -156,7 +212,7 @@ public class HSSFFormulaEvaluator { if (cell != null) { switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_FORMULA: - CellValue cv = getCellValueForEval(internalEvaluate(cell, _sheet, _workbook)); + CellValue cv = getCellValueForEval(internalEvaluate(cell, _sheet)); switch (cv.getCellType()) { case HSSFCell.CELL_TYPE_BOOLEAN: cell.setCellValue(cv.getBooleanValue()); @@ -201,7 +257,7 @@ public class HSSFFormulaEvaluator { if (cell != null) { switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_FORMULA: - CellValue cv = getCellValueForEval(internalEvaluate(cell, _sheet, _workbook)); + CellValue cv = getCellValueForEval(internalEvaluate(cell, _sheet)); switch (cv.getCellType()) { case HSSFCell.CELL_TYPE_BOOLEAN: cell.setCellType(HSSFCell.CELL_TYPE_BOOLEAN); @@ -299,28 +355,40 @@ public class HSSFFormulaEvaluator { * else a runtime exception will be thrown somewhere inside the method. * (Hence this is a private method.) */ - private static ValueEval internalEvaluate(HSSFCell srcCell, HSSFSheet sheet, HSSFWorkbook workbook) { + private ValueEval internalEvaluate(HSSFCell srcCell, HSSFSheet sheet) { int srcRowNum = srcCell.getRowIndex(); int srcColNum = srcCell.getCellNum(); ValueEval result; + int sheetIndex = _workbook.getSheetIndex(sheet); + result = _cache.getValue(sheetIndex, srcRowNum, srcColNum); + if (result != null) { + return result; + } + _evaluationCounter.value++; + EvaluationCycleDetector tracker = EvaluationCycleDetectorManager.getTracker(); - if(!tracker.startEvaluate(workbook, sheet, srcRowNum, srcColNum)) { + if(!tracker.startEvaluate(_workbook, sheet, srcRowNum, srcColNum)) { return ErrorEval.CIRCULAR_REF_ERROR; } try { - result = evaluateCell(workbook, sheet, srcRowNum, (short)srcColNum, srcCell.getCellFormula()); + result = evaluateCell(srcRowNum, (short)srcColNum, srcCell.getCellFormula()); } finally { - tracker.endEvaluate(workbook, sheet, srcRowNum, srcColNum); + tracker.endEvaluate(_workbook, sheet, srcRowNum, srcColNum); + _cache.setValue(sheetIndex, srcRowNum, srcColNum, result); + } + if (isDebugLogEnabled()) { + String sheetName = _workbook.getSheetName(sheetIndex); + CellReference cr = new CellReference(srcRowNum, srcColNum); + logDebug("Evaluated " + sheetName + "!" + cr.formatAsString() + " to " + result.toString()); } return result; } - private static ValueEval evaluateCell(HSSFWorkbook workbook, HSSFSheet sheet, - int srcRowNum, short srcColNum, String cellFormulaText) { + private ValueEval evaluateCell(int srcRowNum, short srcColNum, String cellFormulaText) { - Ptg[] ptgs = FormulaParser.parse(cellFormulaText, workbook); + Ptg[] ptgs = FormulaParser.parse(cellFormulaText, _workbook); Stack stack = new Stack(); for (int i = 0, iSize = ptgs.length; i < iSize; i++) { @@ -352,13 +420,15 @@ public class HSSFFormulaEvaluator { Eval p = (Eval) stack.pop(); ops[j] = p; } - opResult = invokeOperation(operation, ops, srcRowNum, srcColNum, workbook, sheet); + logDebug("invoke " + operation + " (nAgs=" + numops + ")"); + opResult = invokeOperation(operation, ops, srcRowNum, srcColNum, _workbook, _sheet); } else { - opResult = getEvalForPtg(ptg, sheet, workbook); + opResult = getEvalForPtg(ptg, _sheet); } if (opResult == null) { throw new RuntimeException("Evaluation result must not be null"); } + logDebug("push " + opResult); stack.push(opResult); } @@ -415,30 +485,38 @@ public class HSSFFormulaEvaluator { return operation.evaluate(ops, srcRowNum, srcColNum); } + private HSSFSheet getOtherSheet(int externSheetIndex) { + Workbook wb = _workbook.getWorkbook(); + return _workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(externSheetIndex)); + } + private HSSFFormulaEvaluator createEvaluatorForAnotherSheet(HSSFSheet sheet) { + return new HSSFFormulaEvaluator(sheet, _workbook, _cache, _evaluationCounter); + } + /** * returns an appropriate Eval impl instance for the Ptg. The Ptg must be * one of: Area3DPtg, AreaPtg, ReferencePtg, Ref3DPtg, IntPtg, NumberPtg, * StringPtg, BoolPtg