| /* ==================================================================== |
| 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.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.IdentityHashMap; |
| import java.util.Map; |
| import java.util.Stack; |
| import java.util.TreeSet; |
| |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.message.SimpleMessage; |
| import org.apache.poi.ss.SpreadsheetVersion; |
| import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment.WorkbookNotFoundException; |
| import org.apache.poi.ss.formula.atp.AnalysisToolPak; |
| import org.apache.poi.ss.formula.eval.*; |
| import org.apache.poi.ss.formula.function.FunctionMetadataRegistry; |
| import org.apache.poi.ss.formula.functions.*; |
| import org.apache.poi.ss.formula.ptg.*; |
| import org.apache.poi.ss.formula.udf.AggregatingUDFFinder; |
| import org.apache.poi.ss.formula.udf.UDFFinder; |
| import org.apache.poi.ss.usermodel.CellType; |
| import org.apache.poi.ss.util.CellRangeAddressBase; |
| import org.apache.poi.ss.util.CellReference; |
| import org.apache.poi.util.Internal; |
| |
| import static org.apache.logging.log4j.util.Unbox.box; |
| |
| /** |
| * Evaluates formula cells.<p/> |
| * |
| * For performance reasons, this class keeps a cache of all previously calculated intermediate |
| * cell values. Be sure to call {@link #clearAllCachedResultValues()} if any workbook cells are changed between |
| * calls to evaluate~ methods on this class.<br/> |
| * |
| * For POI internal use only |
| * |
| * @author Josh Micich |
| * @author Thies Wellpott (debug output enhancements) |
| */ |
| @Internal |
| public final class WorkbookEvaluator { |
| |
| private static final Logger LOG = LogManager.getLogger(WorkbookEvaluator.class); |
| |
| private final EvaluationWorkbook _workbook; |
| private EvaluationCache _cache; |
| /** part of cache entry key (useful when evaluating multiple workbooks) */ |
| private int _workbookIx; |
| |
| private final IEvaluationListener _evaluationListener; |
| private final Map<EvaluationSheet, Integer> _sheetIndexesBySheet; |
| private final Map<String, Integer> _sheetIndexesByName; |
| private CollaboratingWorkbooksEnvironment _collaboratingWorkbookEnvironment; |
| private final IStabilityClassifier _stabilityClassifier; |
| private final AggregatingUDFFinder _udfFinder; |
| |
| private boolean _ignoreMissingWorkbooks; |
| |
| /** |
| * whether print detailed messages about the next formula evaluation |
| */ |
| private boolean dbgEvaluationOutputForNextEval; |
| |
| // special logger for formula evaluation output (because of possibly very large output) |
| private final Logger EVAL_LOG = LogManager.getLogger("POI.FormulaEval"); |
| // current indent level for evalution; negative value for no output |
| private int dbgEvaluationOutputIndent = -1; |
| |
| /** |
| * @param udfFinder pass <code>null</code> for default (AnalysisToolPak only) |
| */ |
| public WorkbookEvaluator(EvaluationWorkbook workbook, IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { |
| this (workbook, null, stabilityClassifier, udfFinder); |
| } |
| /* package */ WorkbookEvaluator(EvaluationWorkbook workbook, IEvaluationListener evaluationListener, |
| IStabilityClassifier stabilityClassifier, UDFFinder udfFinder) { |
| _workbook = workbook; |
| _evaluationListener = evaluationListener; |
| _cache = new EvaluationCache(evaluationListener); |
| _sheetIndexesBySheet = new IdentityHashMap<>(); |
| _sheetIndexesByName = new IdentityHashMap<>(); |
| _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY; |
| _workbookIx = 0; |
| _stabilityClassifier = stabilityClassifier; |
| |
| AggregatingUDFFinder defaultToolkit = // workbook can be null in unit tests |
| workbook == null ? null : (AggregatingUDFFinder)workbook.getUDFFinder(); |
| if(defaultToolkit != null && udfFinder != null) { |
| defaultToolkit.add(udfFinder); |
| } |
| _udfFinder = defaultToolkit; |
| } |
| |
| /** |
| * also for debug use. Used in toString methods |
| */ |
| /* package */ String getSheetName(int sheetIndex) { |
| return _workbook.getSheetName(sheetIndex); |
| } |
| |
| /* package */ EvaluationSheet getSheet(int sheetIndex) { |
| return _workbook.getSheet(sheetIndex); |
| } |
| |
| /* package */ EvaluationWorkbook getWorkbook() { |
| return _workbook; |
| } |
| |
| /* package */ EvaluationName getName(String name, int sheetIndex) { |
| return _workbook.getName(name, sheetIndex); |
| } |
| |
| /* package */ void attachToEnvironment(CollaboratingWorkbooksEnvironment collaboratingWorkbooksEnvironment, EvaluationCache cache, int workbookIx) { |
| _collaboratingWorkbookEnvironment = collaboratingWorkbooksEnvironment; |
| _cache = cache; |
| _workbookIx = workbookIx; |
| } |
| /* package */ CollaboratingWorkbooksEnvironment getEnvironment() { |
| return _collaboratingWorkbookEnvironment; |
| } |
| |
| /** |
| * Discards the current workbook environment and attaches to the default 'empty' environment. |
| * Also resets evaluation cache. |
| */ |
| /* package */ void detachFromEnvironment() { |
| _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY; |
| _cache = new EvaluationCache(_evaluationListener); |
| _workbookIx = 0; |
| } |
| /** |
| * @return the evaluator for another workbook which is part of the same {@link CollaboratingWorkbooksEnvironment} |
| */ |
| /* package */ WorkbookEvaluator getOtherWorkbookEvaluator(String workbookName) throws WorkbookNotFoundException { |
| return _collaboratingWorkbookEnvironment.getWorkbookEvaluator(workbookName); |
| } |
| |
| /* 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 |
| * of the evaluate~ methods of this class |
| */ |
| public void clearAllCachedResultValues() { |
| _cache.clear(); |
| _sheetIndexesBySheet.clear(); |
| _workbook.clearAllCachedResultValues(); |
| } |
| |
| /** |
| * Should be called to tell the cell value cache that the specified (value or formula) cell |
| * has changed. |
| */ |
| 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 been |
| * deleted. |
| */ |
| public void notifyDeleteCell(EvaluationCell cell) { |
| int sheetIndex = getSheetIndex(cell.getSheet()); |
| _cache.notifyDeleteCell(_workbookIx, sheetIndex, cell); |
| } |
| |
| private int getSheetIndex(EvaluationSheet sheet) { |
| Integer result = _sheetIndexesBySheet.get(sheet); |
| if (result == null) { |
| int sheetIndex = _workbook.getSheetIndex(sheet); |
| if (sheetIndex < 0) { |
| throw new RuntimeException("Specified sheet from a different book"); |
| } |
| result = Integer.valueOf(sheetIndex); |
| _sheetIndexesBySheet.put(sheet, result); |
| } |
| return result.intValue(); |
| } |
| |
| public ValueEval evaluate(EvaluationCell srcCell) { |
| int sheetIndex = getSheetIndex(srcCell.getSheet()); |
| return evaluateAny(srcCell, sheetIndex, srcCell.getRowIndex(), srcCell.getColumnIndex(), new EvaluationTracker(_cache)); |
| } |
| |
| /** |
| * Case-insensitive. |
| * @return -1 if sheet with specified name does not exist |
| */ |
| /* package */ int getSheetIndex(String sheetName) { |
| Integer result = _sheetIndexesByName.get(sheetName); |
| if (result == null) { |
| int sheetIndex = _workbook.getSheetIndex(sheetName); |
| if (sheetIndex < 0) { |
| return -1; |
| } |
| result = Integer.valueOf(sheetIndex); |
| _sheetIndexesByName.put(sheetName, result); |
| } |
| return result.intValue(); |
| } |
| |
| /* package */ int getSheetIndexByExternIndex(int externSheetIndex) { |
| return _workbook.convertFromExternSheetIndex(externSheetIndex); |
| } |
| |
| |
| /** |
| * @return never <code>null</code>, never {@link BlankEval} |
| */ |
| private ValueEval evaluateAny(EvaluationCell srcCell, int sheetIndex, |
| int rowIndex, int columnIndex, EvaluationTracker tracker) { |
| |
| // avoid tracking dependencies to cells that have constant definition |
| boolean shouldCellDependencyBeRecorded = _stabilityClassifier == null ? true |
| : !_stabilityClassifier.isCellFinal(sheetIndex, rowIndex, columnIndex); |
| if (srcCell == null || srcCell.getCellType() != CellType.FORMULA) { |
| ValueEval result = getValueFromNonFormulaCell(srcCell); |
| if (shouldCellDependencyBeRecorded) { |
| tracker.acceptPlainValueDependency(_workbook, _workbookIx, sheetIndex, rowIndex, columnIndex, result); |
| } |
| return result; |
| } |
| |
| FormulaCellCacheEntry cce = _cache.getOrCreateFormulaCellEntry(srcCell); |
| if (shouldCellDependencyBeRecorded || cce.isInputSensitive()) { |
| tracker.acceptFormulaDependency(cce); |
| } |
| IEvaluationListener evalListener = _evaluationListener; |
| ValueEval result; |
| if (cce.getValue() == null) { |
| if (!tracker.startEvaluate(cce)) { |
| return ErrorEval.CIRCULAR_REF_ERROR; |
| } |
| |
| try { |
| |
| Ptg[] ptgs = _workbook.getFormulaTokens(srcCell); |
| OperationEvaluationContext ec = new OperationEvaluationContext |
| (this, _workbook, sheetIndex, rowIndex, columnIndex, tracker); |
| if (evalListener == null) { |
| result = evaluateFormula(ec, ptgs); |
| } else { |
| evalListener.onStartEvaluate(srcCell, cce); |
| result = evaluateFormula(ec, ptgs); |
| evalListener.onEndEvaluate(cce, result); |
| } |
| |
| tracker.updateCacheResult(result); |
| } |
| catch (NotImplementedException e) { |
| throw addExceptionInfo(e, sheetIndex, rowIndex, columnIndex); |
| } catch (RuntimeException re) { |
| if (re.getCause() instanceof WorkbookNotFoundException && _ignoreMissingWorkbooks) { |
| LOG.atInfo().log("{} - Continuing with cached value!", re.getCause().getMessage()); |
| switch(srcCell.getCachedFormulaResultType()) { |
| case NUMERIC: |
| result = new NumberEval(srcCell.getNumericCellValue()); |
| break; |
| case STRING: |
| result = new StringEval(srcCell.getStringCellValue()); |
| break; |
| case BLANK: |
| result = BlankEval.instance; |
| break; |
| case BOOLEAN: |
| result = BoolEval.valueOf(srcCell.getBooleanCellValue()); |
| break; |
| case ERROR: |
| result = ErrorEval.valueOf(srcCell.getErrorCellValue()); |
| break; |
| case FORMULA: |
| default: |
| throw new RuntimeException("Unexpected cell type '" + srcCell.getCellType()+"' found!"); |
| } |
| } else { |
| throw re; |
| } |
| } finally { |
| tracker.endEvaluate(cce); |
| } |
| } else { |
| if(evalListener != null) { |
| evalListener.onCacheHit(sheetIndex, rowIndex, columnIndex, cce.getValue()); |
| } |
| return cce.getValue(); |
| } |
| final ValueEval resultForLogging = result; |
| LOG.atDebug().log(()->{ |
| String sheetName = getSheetName(sheetIndex); |
| CellReference cr = new CellReference(rowIndex, columnIndex); |
| return new SimpleMessage("Evaluated " + sheetName + "!" + cr.formatAsString() + " to " + resultForLogging); |
| }); |
| // Usually (result === cce.getValue()) |
| // But sometimes: (result==ErrorEval.CIRCULAR_REF_ERROR, cce.getValue()==null) |
| // When circular references are detected, the cache entry is only updated for |
| // the top evaluation frame |
| return result; |
| } |
| |
| /** |
| * Adds the current cell reference to the exception for easier debugging. |
| * Would be nice to get the formula text as well, but that seems to require |
| * too much digging around and casting to get the FormulaRenderingWorkbook. |
| */ |
| private NotImplementedException addExceptionInfo(NotImplementedException inner, int sheetIndex, int rowIndex, int columnIndex) { |
| |
| try { |
| String sheetName = _workbook.getSheetName(sheetIndex); |
| CellReference cr = new CellReference(sheetName, rowIndex, columnIndex, false, false); |
| String msg = "Error evaluating cell " + cr.formatAsString(); |
| return new NotImplementedException(msg, inner); |
| } catch (Exception e) { |
| // avoid bombing out during exception handling |
| LOG.atError().withThrowable(e).log("Can't add exception info"); |
| return inner; // preserve original exception |
| } |
| } |
| /** |
| * 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, never <code>null</code> |
| */ |
| /* package */ static ValueEval getValueFromNonFormulaCell(EvaluationCell cell) { |
| if (cell == null) { |
| return BlankEval.instance; |
| } |
| CellType cellType = cell.getCellType(); |
| switch (cellType) { |
| case NUMERIC: |
| return new NumberEval(cell.getNumericCellValue()); |
| case STRING: |
| return new StringEval(cell.getStringCellValue()); |
| case BOOLEAN: |
| return BoolEval.valueOf(cell.getBooleanCellValue()); |
| case BLANK: |
| return BlankEval.instance; |
| case ERROR: |
| return ErrorEval.valueOf(cell.getErrorCellValue()); |
| default: |
| throw new RuntimeException("Unexpected cell type (" + cellType + ")"); |
| } |
| |
| } |
| |
| |
| // visibility raised for testing |
| @Internal |
| /* package */ ValueEval evaluateFormula(OperationEvaluationContext ec, Ptg[] ptgs) { |
| |
| String dbgIndentStr = ""; // always init. to non-null just for defensive avoiding NPE |
| if (dbgEvaluationOutputForNextEval) { |
| // first evaluation call when ouput is desired, so iit. this evaluator instance |
| dbgEvaluationOutputIndent = 1; |
| dbgEvaluationOutputForNextEval = false; |
| } |
| if (dbgEvaluationOutputIndent > 0) { |
| // init. indent string to needed spaces (create as substring from very long space-only string; |
| // limit indentation for deep recursions) |
| dbgIndentStr = " "; |
| dbgIndentStr = dbgIndentStr.substring(0, Math.min(dbgIndentStr.length(), dbgEvaluationOutputIndent*2)); |
| String finalDbgIndentStr = dbgIndentStr; |
| EVAL_LOG.atWarn().log(() -> { |
| String message = finalDbgIndentStr |
| + "- evaluateFormula('" + ec.getRefEvaluatorForCurrentSheet().getSheetNameRange() |
| + "'/" + new CellReference(ec.getRowIndex(), ec.getColumnIndex()).formatAsString() |
| + "): " + Arrays.toString(ptgs).replaceAll("\\Qorg.apache.poi.ss.formula.ptg.\\E", ""); |
| return new SimpleMessage(message); |
| }); |
| dbgEvaluationOutputIndent++; |
| } |
| |
| EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex()); |
| EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex()); |
| |
| Stack<ValueEval> stack = new Stack<>(); |
| for (int i = 0, iSize = ptgs.length; i < iSize; i++) { |
| // since we don't know how to handle these yet :( |
| Ptg ptg = ptgs[i]; |
| if (dbgEvaluationOutputIndent > 0) { |
| EVAL_LOG.atInfo().log("{} * ptg {}: {}, stack: {}", dbgIndentStr, box(i),ptg, stack); |
| } |
| if (ptg instanceof AttrPtg) { |
| AttrPtg attrPtg = (AttrPtg) ptg; |
| if (attrPtg.isSum()) { |
| // Excel prefers to encode 'SUM()' as a tAttr token, but this evaluator |
| // expects the equivalent function token |
| ptg = FuncVarPtg.SUM; |
| } |
| if (attrPtg.isOptimizedChoose()) { |
| ValueEval arg0 = stack.pop(); |
| int[] jumpTable = attrPtg.getJumpTable(); |
| int dist; |
| int nChoices = jumpTable.length; |
| try { |
| int switchIndex = Choose.evaluateFirstArg(arg0, ec.getRowIndex(), ec.getColumnIndex()); |
| if (switchIndex<1 || switchIndex > nChoices) { |
| stack.push(ErrorEval.VALUE_INVALID); |
| dist = attrPtg.getChooseFuncOffset() + 4; // +4 for tFuncFar(CHOOSE) |
| } else { |
| dist = jumpTable[switchIndex-1]; |
| } |
| } catch (EvaluationException e) { |
| stack.push(e.getErrorEval()); |
| dist = attrPtg.getChooseFuncOffset() + 4; // +4 for tFuncFar(CHOOSE) |
| } |
| // Encoded dist for tAttrChoose includes size of jump table, but |
| // countTokensToBeSkipped() does not (it counts whole tokens). |
| dist -= nChoices*2+2; // subtract jump table size |
| i+= countTokensToBeSkipped(ptgs, i, dist); |
| continue; |
| } |
| if (attrPtg.isOptimizedIf()) { |
| if(!evalCell.isPartOfArrayFormulaGroup()) { |
| ValueEval arg0 = stack.pop(); |
| boolean evaluatedPredicate; |
| |
| try { |
| evaluatedPredicate = IfFunc.evaluateFirstArg(arg0, ec.getRowIndex(), ec.getColumnIndex()); |
| } catch (EvaluationException e) { |
| stack.push(e.getErrorEval()); |
| int dist = attrPtg.getData(); |
| i += countTokensToBeSkipped(ptgs, i, dist); |
| attrPtg = (AttrPtg) ptgs[i]; |
| dist = attrPtg.getData() + 1; |
| i += countTokensToBeSkipped(ptgs, i, dist); |
| continue; |
| } |
| if (evaluatedPredicate) { |
| // nothing to skip - true param follows |
| } else { |
| int dist = attrPtg.getData(); |
| i += countTokensToBeSkipped(ptgs, i, dist); |
| Ptg nextPtg = ptgs[i + 1]; |
| if (ptgs[i] instanceof AttrPtg && nextPtg instanceof FuncVarPtg && |
| // in order to verify that there is no third param, we need to check |
| // if we really have the IF next or some other FuncVarPtg as third param, e.g. ROW()/COLUMN()! |
| ((FuncVarPtg) nextPtg).getFunctionIndex() == FunctionMetadataRegistry.FUNCTION_INDEX_IF) { |
| // this is an if statement without a false param (as opposed to MissingArgPtg as the false param) |
| //i++; |
| stack.push(arg0); |
| stack.push(BoolEval.FALSE); |
| } |
| } |
| } |
| continue; |
| } |
| if (attrPtg.isSkip() && !evalCell.isPartOfArrayFormulaGroup()) { |
| int dist = attrPtg.getData()+1; |
| i+= countTokensToBeSkipped(ptgs, i, dist); |
| if (stack.peek() == MissingArgEval.instance) { |
| stack.pop(); |
| stack.push(BlankEval.instance); |
| } |
| continue; |
| } |
| } |
| if (ptg instanceof ControlPtg) { |
| // skip Parentheses, Attr, etc |
| continue; |
| } |
| if (ptg instanceof MemFuncPtg || ptg instanceof MemAreaPtg) { |
| // can ignore, rest of tokens for this expression are in OK RPN order |
| continue; |
| } |
| if (ptg instanceof MemErrPtg) { |
| continue; |
| } |
| |
| if (ptg instanceof UnionPtg) { |
| ValueEval v2 = stack.pop(); |
| ValueEval v1 = stack.pop(); |
| stack.push(new RefListEval(v1, v2)); |
| continue; |
| } |
| |
| ValueEval opResult; |
| if (ptg instanceof OperationPtg) { |
| OperationPtg optg = (OperationPtg) ptg; |
| |
| int numops = optg.getNumberOfOperands(); |
| ValueEval[] ops = new ValueEval[numops]; |
| |
| // storing the ops in reverse order since they are popping |
| boolean areaArg = false; // whether one of the operands is an area |
| for (int j = numops - 1; j >= 0; j--) { |
| ValueEval p = stack.pop(); |
| ops[j] = p; |
| if(p instanceof AreaEval){ |
| areaArg = true; |
| } |
| } |
| |
| boolean arrayMode = false; |
| if(areaArg) for (int ii = i; ii < iSize; ii++) { |
| if(ptgs[ii] instanceof FuncVarPtg){ |
| FuncVarPtg f = (FuncVarPtg)ptgs[ii]; |
| try { |
| Function func = FunctionEval.getBasicFunction(f.getFunctionIndex()); |
| if (func != null && func instanceof ArrayMode) { |
| arrayMode = true; |
| } |
| } catch (NotImplementedException ne){ |
| //FunctionEval.getBasicFunction can throw NotImplementedException |
| // if the fucntion is not yet supported. |
| } |
| break; |
| } |
| } |
| ec.setArrayMode(arrayMode); |
| |
| // logDebug("invoke " + operation + " (nAgs=" + numops + ")"); |
| opResult = OperationEvaluatorFactory.evaluate(optg, ops, ec); |
| |
| ec.setArrayMode(false); |
| |
| } else { |
| opResult = getEvalForPtg(ptg, ec); |
| } |
| if (opResult == null) { |
| throw new RuntimeException("Evaluation result must not be null"); |
| } |
| // logDebug("push " + opResult); |
| stack.push(opResult); |
| if (dbgEvaluationOutputIndent > 0) { |
| EVAL_LOG.atInfo().log("{} = {}", dbgIndentStr, opResult); |
| } |
| } |
| |
| ValueEval value = stack.pop(); |
| if (!stack.isEmpty()) { |
| throw new IllegalStateException("evaluation stack not empty"); |
| } |
| |
| ValueEval result; |
| |
| if (ec.isSingleValue()) { |
| result = dereferenceResult(value, ec); |
| } |
| else { |
| result = value; |
| } |
| |
| if (dbgEvaluationOutputIndent > 0) { |
| EVAL_LOG.atInfo().log("{}finished eval of {}: {}", dbgIndentStr, new CellReference(ec.getRowIndex(), ec.getColumnIndex()).formatAsString(), result); |
| dbgEvaluationOutputIndent--; |
| if (dbgEvaluationOutputIndent == 1) { |
| // this evaluation is done, reset indent to stop logging |
| dbgEvaluationOutputIndent = -1; |
| } |
| } // if |
| return result; |
| |
| } |
| |
| /** |
| * Calculates the number of tokens that the evaluator should skip upon reaching a tAttrSkip. |
| * |
| * @return the number of tokens (starting from <tt>startIndex+1</tt>) that need to be skipped |
| * to achieve the specified <tt>distInBytes</tt> skip distance. |
| */ |
| private static int countTokensToBeSkipped(Ptg[] ptgs, int startIndex, int distInBytes) { |
| int remBytes = distInBytes; |
| int index = startIndex; |
| while (remBytes != 0) { |
| index++; |
| remBytes -= ptgs[index].getSize(); |
| if (remBytes < 0) { |
| throw new RuntimeException("Bad skip distance (wrong token size calculation)."); |
| } |
| if (index >= ptgs.length) { |
| throw new RuntimeException("Skip distance too far (ran out of formula tokens)."); |
| } |
| } |
| return index-startIndex; |
| } |
| |
| /** |
| * Dereferences a single value from any AreaEval or RefEval evaluation |
| * result. If the supplied evaluationResult is just a plain value, it is |
| * returned as-is. |
| * |
| * @return a {@link NumberEval}, {@link StringEval}, {@link BoolEval}, or |
| * {@link ErrorEval}. Never <code>null</code>. {@link BlankEval} is |
| * converted to {@link NumberEval#ZERO} |
| */ |
| private static ValueEval dereferenceResult(ValueEval evaluationResult, OperationEvaluationContext ec) { |
| ValueEval value; |
| |
| if (ec == null) { |
| throw new IllegalArgumentException("OperationEvaluationContext ec is null"); |
| } |
| if (ec.getWorkbook() == null) { |
| throw new IllegalArgumentException("OperationEvaluationContext ec.getWorkbook() is null"); |
| } |
| |
| EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex()); |
| EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex()); |
| |
| if (evalCell != null && evalCell.isPartOfArrayFormulaGroup() && evaluationResult instanceof AreaEval) { |
| value = OperandResolver.getElementFromArray((AreaEval) evaluationResult, evalCell); |
| } |
| else { |
| value = dereferenceResult(evaluationResult, ec.getRowIndex(), ec.getColumnIndex()); |
| } |
| if (value == BlankEval.instance) { |
| // Note Excel behaviour here. A blank final final value is converted to zero. |
| return NumberEval.ZERO; |
| // Formulas _never_ evaluate to blank. If a formula appears to have evaluated to |
| // blank, the actual value is empty string. This can be verified with ISBLANK(). |
| } |
| |
| return value; |
| } |
| |
| /** |
| * Dereferences a single value from any AreaEval or RefEval evaluation |
| * result. If the supplied evaluationResult is just a plain value, it is |
| * returned as-is. |
| * |
| * @return a {@link NumberEval}, {@link StringEval}, {@link BoolEval}, or |
| * {@link ErrorEval}. Never <code>null</code>. {@link BlankEval} is |
| * converted to {@link NumberEval#ZERO} |
| */ |
| public static ValueEval dereferenceResult(ValueEval evaluationResult, int srcRowNum, int srcColNum) { |
| ValueEval value; |
| try { |
| value = OperandResolver.getSingleValue(evaluationResult, srcRowNum, srcColNum); |
| } catch (EvaluationException e) { |
| return e.getErrorEval(); |
| } |
| if (value == BlankEval.instance) { |
| // Note Excel behaviour here. A blank final final value is converted to zero. |
| return NumberEval.ZERO; |
| // Formulas _never_ evaluate to blank. If a formula appears to have evaluated to |
| // blank, the actual value is empty string. This can be verified with ISBLANK(). |
| } |
| return value; |
| } |
| |
| |
| /** |
| * returns an appropriate Eval impl instance for the Ptg. The Ptg must be |
| * one of: Area3DPtg, AreaPtg, ReferencePtg, Ref3DPtg, IntPtg, NumberPtg, |
| * StringPtg, BoolPtg <br/>special Note: OperationPtg subtypes cannot be |
| * passed here! |
| */ |
| private ValueEval getEvalForPtg(Ptg ptg, OperationEvaluationContext ec) { |
| // consider converting all these (ptg instanceof XxxPtg) expressions to (ptg.getClass() == XxxPtg.class) |
| |
| if (ptg instanceof NamePtg) { |
| // Named ranges, macro functions |
| NamePtg namePtg = (NamePtg) ptg; |
| EvaluationName nameRecord = _workbook.getName(namePtg); |
| return getEvalForNameRecord(nameRecord, ec); |
| } |
| if (ptg instanceof NameXPtg) { |
| // Externally defined named ranges or macro functions |
| return processNameEval(ec.getNameXEval((NameXPtg)ptg), ec); |
| } |
| if (ptg instanceof NameXPxg) { |
| // Externally defined named ranges or macro functions |
| return processNameEval(ec.getNameXEval((NameXPxg)ptg), ec); |
| } |
| |
| if (ptg instanceof IntPtg) { |
| return new NumberEval(((IntPtg)ptg).getValue()); |
| } |
| if (ptg instanceof NumberPtg) { |
| return new NumberEval(((NumberPtg)ptg).getValue()); |
| } |
| if (ptg instanceof StringPtg) { |
| return new StringEval(((StringPtg) ptg).getValue()); |
| } |
| if (ptg instanceof BoolPtg) { |
| return BoolEval.valueOf(((BoolPtg) ptg).getValue()); |
| } |
| if (ptg instanceof ErrPtg) { |
| return ErrorEval.valueOf(((ErrPtg) ptg).getErrorCode()); |
| } |
| if (ptg instanceof MissingArgPtg) { |
| return MissingArgEval.instance; |
| } |
| if (ptg instanceof AreaErrPtg ||ptg instanceof RefErrorPtg |
| || ptg instanceof DeletedArea3DPtg || ptg instanceof DeletedRef3DPtg) { |
| return ErrorEval.REF_INVALID; |
| } |
| if (ptg instanceof Ref3DPtg) { |
| return ec.getRef3DEval((Ref3DPtg)ptg); |
| } |
| if (ptg instanceof Ref3DPxg) { |
| return ec.getRef3DEval((Ref3DPxg)ptg); |
| } |
| if (ptg instanceof Area3DPtg) { |
| return ec.getArea3DEval((Area3DPtg)ptg); |
| } |
| if (ptg instanceof Area3DPxg) { |
| return ec.getArea3DEval((Area3DPxg)ptg); |
| } |
| if (ptg instanceof RefPtg) { |
| RefPtg rptg = (RefPtg) ptg; |
| return ec.getRefEval(rptg.getRow(), rptg.getColumn()); |
| } |
| if (ptg instanceof AreaPtg) { |
| AreaPtg aptg = (AreaPtg) ptg; |
| return ec.getAreaEval(aptg.getFirstRow(), aptg.getFirstColumn(), aptg.getLastRow(), aptg.getLastColumn()); |
| } |
| |
| if (ptg instanceof ArrayPtg) { |
| ArrayPtg aptg = (ArrayPtg) ptg; |
| return ec.getAreaValueEval(0, 0, aptg.getRowCount() - 1, aptg.getColumnCount() - 1, aptg.getTokenArrayValues()); |
| } |
| |
| if (ptg instanceof UnknownPtg) { |
| // POI uses UnknownPtg when the encoded Ptg array seems to be corrupted. |
| // This seems to occur in very rare cases (e.g. unused name formulas in bug 44774, attachment 21790) |
| // In any case, formulas are re-parsed before execution, so UnknownPtg should not get here |
| throw new RuntimeException("UnknownPtg not allowed"); |
| } |
| if (ptg instanceof ExpPtg) { |
| // ExpPtg is used for array formulas and shared formulas. |
| // it is currently unsupported, and may not even get implemented here |
| throw new RuntimeException("ExpPtg currently not supported"); |
| } |
| |
| throw new RuntimeException("Unexpected ptg class (" + ptg.getClass().getName() + ")"); |
| } |
| |
| private ValueEval processNameEval(ValueEval eval, OperationEvaluationContext ec) { |
| if (eval instanceof ExternalNameEval) { |
| EvaluationName name = ((ExternalNameEval)eval).getName(); |
| return getEvalForNameRecord(name, ec); |
| } |
| return eval; |
| } |
| |
| private ValueEval getEvalForNameRecord(EvaluationName nameRecord, OperationEvaluationContext ec) { |
| if (nameRecord.isFunctionName()) { |
| return new FunctionNameEval(nameRecord.getNameText()); |
| } |
| if (nameRecord.hasFormula()) { |
| return evaluateNameFormula(nameRecord.getNameDefinition(), ec); |
| } |
| |
| throw new RuntimeException("Don't know how to evaluate name '" + nameRecord.getNameText() + "'"); |
| } |
| |
| /** |
| * YK: Used by OperationEvaluationContext to resolve indirect names. |
| */ |
| /*package*/ ValueEval evaluateNameFormula(Ptg[] ptgs, OperationEvaluationContext ec) { |
| if (ptgs.length == 1 && !(ptgs[0] instanceof FuncVarPtg)) { |
| return getEvalForPtg(ptgs[0], ec); |
| } |
| |
| OperationEvaluationContext anyValueContext = new OperationEvaluationContext(this, ec.getWorkbook(), ec.getSheetIndex(), ec.getRowIndex(), ec.getColumnIndex(), new EvaluationTracker(_cache), false); |
| return evaluateFormula(anyValueContext, ptgs); |
| } |
| |
| /** |
| * Used by the lazy ref evals whenever they need to get the value of a contained cell. |
| */ |
| /* package */ ValueEval evaluateReference( |
| EvaluationSheet sheet, int sheetIndex, int rowIndex, |
| int columnIndex, EvaluationTracker tracker) { |
| |
| EvaluationCell cell = sheet.getCell(rowIndex, columnIndex); |
| return evaluateAny(cell, sheetIndex, rowIndex, columnIndex, tracker); |
| } |
| public FreeRefFunction findUserDefinedFunction(String functionName) { |
| return _udfFinder.findFunction(functionName); |
| } |
| |
| /** |
| * Evaluate a formula outside a cell value, e.g. conditional format rules or data validation expressions |
| * |
| * @param formula to evaluate |
| * @param ref defines the optional sheet and row/column base for the formula, if it is relative |
| * @return value |
| */ |
| public ValueEval evaluate(String formula, CellReference ref) { |
| final String sheetName = ref == null ? null : ref.getSheetName(); |
| int sheetIndex; |
| if (sheetName == null) { |
| sheetIndex = -1; // workbook scope only |
| } else { |
| sheetIndex = getWorkbook().getSheetIndex(sheetName); |
| } |
| int rowIndex = ref == null ? -1 : ref.getRow(); |
| short colIndex = ref == null ? -1 : ref.getCol(); |
| final OperationEvaluationContext ec = new OperationEvaluationContext( |
| this, |
| getWorkbook(), |
| sheetIndex, |
| rowIndex, |
| colIndex, |
| new EvaluationTracker(_cache) |
| ); |
| Ptg[] ptgs = FormulaParser.parse(formula, (FormulaParsingWorkbook) getWorkbook(), FormulaType.CELL, sheetIndex, rowIndex); |
| return evaluateNameFormula(ptgs, ec); |
| } |
| |
| /** |
| * Some expressions need to be evaluated in terms of an offset from the top left corner of a region, |
| * such as some data validation and conditional format expressions, when those constraints apply |
| * to contiguous cells. When a relative formula is used, it must be evaluated by shifting by the target |
| * offset position relative to the top left of the range. |
| * <p> |
| * Returns a single value e.g. a cell formula result or boolean value for conditional formatting. |
| * |
| * @param formula The formula to evaluate |
| * @param target cell context for the operation |
| * @param region containing the cell |
| * @return value |
| * @throws IllegalArgumentException if target does not define a sheet name to evaluate the formula on. |
| */ |
| public ValueEval evaluate(String formula, CellReference target, CellRangeAddressBase region) { |
| return evaluate(formula, target, region, FormulaType.CELL); |
| } |
| |
| /** |
| * Some expressions need to be evaluated in terms of an offset from the top left corner of a region, |
| * such as some data validation and conditional format expressions, when those constraints apply |
| * to contiguous cells. When a relative formula is used, it must be evaluated by shifting by the target |
| * offset position relative to the top left of the range. |
| * <p> |
| * Returns a ValueEval that may be one or more values, such as the allowed values for a data validation constraint. |
| * |
| * @param formula The formula to evaluate |
| * @param target cell context for the operation |
| * @param region containing the cell |
| * @return ValueEval for one or more values |
| * @throws IllegalArgumentException if target does not define a sheet name to evaluate the formula on. |
| */ |
| public ValueEval evaluateList(String formula, CellReference target, CellRangeAddressBase region) { |
| return evaluate(formula, target, region, FormulaType.DATAVALIDATION_LIST); |
| } |
| |
| private ValueEval evaluate(String formula, CellReference target, CellRangeAddressBase region, FormulaType formulaType) { |
| final String sheetName = target == null ? null : target.getSheetName(); |
| if (sheetName == null) throw new IllegalArgumentException("Sheet name is required"); |
| |
| final int sheetIndex = getWorkbook().getSheetIndex(sheetName); |
| Ptg[] ptgs = FormulaParser.parse(formula, (FormulaParsingWorkbook) getWorkbook(), formulaType, sheetIndex, target.getRow()); |
| |
| adjustRegionRelativeReference(ptgs, target, region); |
| |
| final OperationEvaluationContext ec = new OperationEvaluationContext(this, getWorkbook(), sheetIndex, target.getRow(), target.getCol(), new EvaluationTracker(_cache), formulaType.isSingleValue()); |
| return evaluateNameFormula(ptgs, ec); |
| } |
| |
| /** |
| * Adjust formula relative references by the offset between the start of the given region and the given target cell. |
| * That is, treat the region top-left cell as "A1" for the purposes of evaluating relative reference components (row and/or column), |
| * and further move references by the position of the target within the region. |
| * <p><pre>formula ref + range top-left + current cell range offset </pre></p> |
| * which simplifies to |
| * <p><pre>formula ref + current cell ref</pre></p> |
| * @param ptgs |
| * @param target cell within the region to use. |
| * @param region containing the cell, OR, for conditional format rules with multiple ranges, the region with the top-left-most cell |
| * @return true if any Ptg references were shifted |
| * @throws IndexOutOfBoundsException if the resulting shifted row/column indexes are over the document format limits |
| * @throws IllegalArgumentException if target is not within region. |
| */ |
| protected boolean adjustRegionRelativeReference(Ptg[] ptgs, CellReference target, CellRangeAddressBase region) { |
| // region may not be the one that contains the target, if a conditional formatting rule applies to multiple regions |
| |
| int deltaRow = target.getRow() - region.getFirstRow(); |
| int deltaColumn = target.getCol() - region.getFirstColumn(); |
| |
| boolean shifted = false; |
| for (Ptg ptg : ptgs) { |
| // base class for cell reference "things" |
| if (ptg instanceof RefPtgBase) { |
| RefPtgBase ref = (RefPtgBase) ptg; |
| // re-calculate cell references |
| final SpreadsheetVersion version = _workbook.getSpreadsheetVersion(); |
| if (ref.isRowRelative() && deltaRow > 0) { |
| final int rowIndex = ref.getRow() + deltaRow; |
| if (rowIndex > version.getMaxRows()) { |
| throw new IndexOutOfBoundsException(version.name() + " files can only have " + version.getMaxRows() + " rows, but row " + rowIndex + " was requested."); |
| } |
| ref.setRow(rowIndex); |
| shifted = true; |
| } |
| if (ref.isColRelative() && deltaColumn > 0) { |
| final int colIndex = ref.getColumn() + deltaColumn; |
| if (colIndex > version.getMaxColumns()) { |
| throw new IndexOutOfBoundsException(version.name() + " files can only have " + version.getMaxColumns() + " columns, but column " + colIndex + " was requested."); |
| } |
| ref.setColumn(colIndex); |
| shifted = true; |
| } |
| } |
| } |
| return shifted; |
| } |
| |
| /** |
| * Whether to ignore missing references to external workbooks and |
| * use cached formula results in the main workbook instead. |
| * <p> |
| * In some cases exetrnal workbooks referenced by formulas in the main workbook are not avaiable. |
| * With this method you can control how POI handles such missing references: |
| * <ul> |
| * <li>by default ignoreMissingWorkbooks=false and POI throws {@link WorkbookNotFoundException} |
| * if an external reference cannot be resolved</li> |
| * <li>if ignoreMissingWorkbooks=true then POI uses cached formula result |
| * that already exists in the main workbook</li> |
| * </ul> |
| * |
| * @param ignore whether to ignore missing references to external workbooks |
| * @see <a href="https://issues.apache.org/bugzilla/show_bug.cgi?id=52575">Bug 52575 for details</a> |
| */ |
| public void setIgnoreMissingWorkbooks(boolean ignore){ |
| _ignoreMissingWorkbooks = ignore; |
| } |
| public boolean isIgnoreMissingWorkbooks(){ |
| return _ignoreMissingWorkbooks; |
| } |
| |
| /** |
| * Return a collection of functions that POI can evaluate |
| * |
| * @return names of functions supported by POI |
| */ |
| public static Collection<String> getSupportedFunctionNames(){ |
| Collection<String> lst = new TreeSet<>(); |
| lst.addAll(FunctionEval.getSupportedFunctionNames()); |
| lst.addAll(AnalysisToolPak.getSupportedFunctionNames()); |
| return Collections.unmodifiableCollection(lst); |
| } |
| |
| /** |
| * Return a collection of functions that POI does not support |
| * |
| * @return names of functions NOT supported by POI |
| */ |
| public static Collection<String> getNotSupportedFunctionNames(){ |
| Collection<String> lst = new TreeSet<>(); |
| lst.addAll(FunctionEval.getNotSupportedFunctionNames()); |
| lst.addAll(AnalysisToolPak.getNotSupportedFunctionNames()); |
| return Collections.unmodifiableCollection(lst); |
| } |
| |
| /** |
| * Register a ATP function in runtime. |
| * |
| * @param name the function name |
| * @param func the functoin to register |
| * @throws IllegalArgumentException if the function is unknown or already registered. |
| * @since 3.8 beta6 |
| */ |
| public static void registerFunction(String name, FreeRefFunction func){ |
| AnalysisToolPak.registerFunction(name, func); |
| } |
| |
| /** |
| * Register a function in runtime. |
| * |
| * @param name the function name |
| * @param func the functoin to register |
| * @throws IllegalArgumentException if the function is unknown or already registered. |
| * @since 3.8 beta6 |
| */ |
| public static void registerFunction(String name, Function func){ |
| FunctionEval.registerFunction(name, func); |
| } |
| |
| public void setDebugEvaluationOutputForNextEval(boolean value){ |
| dbgEvaluationOutputForNextEval = value; |
| } |
| public boolean isDebugEvaluationOutputForNextEval(){ |
| return dbgEvaluationOutputForNextEval; |
| } |
| } |