poi/src/java/org/apache/poi/ss/usermodel/FormulaEvaluator.java

574 lines
23 KiB
Java

/* ====================================================================
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.usermodel;
import java.util.Iterator;
import java.util.Stack;
import org.apache.poi.hssf.model.FormulaParser;
import org.apache.poi.hssf.record.formula.Area3DPtg;
import org.apache.poi.hssf.record.formula.AreaPtg;
import org.apache.poi.hssf.record.formula.BoolPtg;
import org.apache.poi.hssf.record.formula.ControlPtg;
import org.apache.poi.hssf.record.formula.ErrPtg;
import org.apache.poi.hssf.record.formula.IntPtg;
import org.apache.poi.hssf.record.formula.MemErrPtg;
import org.apache.poi.hssf.record.formula.MissingArgPtg;
import org.apache.poi.hssf.record.formula.NamePtg;
import org.apache.poi.hssf.record.formula.NameXPtg;
import org.apache.poi.hssf.record.formula.NumberPtg;
import org.apache.poi.hssf.record.formula.OperationPtg;
import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.hssf.record.formula.Ref3DPtg;
import org.apache.poi.hssf.record.formula.RefPtg;
import org.apache.poi.hssf.record.formula.StringPtg;
import org.apache.poi.hssf.record.formula.UnionPtg;
import org.apache.poi.hssf.record.formula.UnknownPtg;
import org.apache.poi.hssf.record.formula.eval.AreaEval;
import org.apache.poi.hssf.record.formula.eval.BlankEval;
import org.apache.poi.hssf.record.formula.eval.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
import org.apache.poi.hssf.record.formula.eval.Eval;
import org.apache.poi.hssf.record.formula.eval.FunctionEval;
import org.apache.poi.hssf.record.formula.eval.LazyAreaEval;
import org.apache.poi.hssf.record.formula.eval.LazyRefEval;
import org.apache.poi.hssf.record.formula.eval.NameEval;
import org.apache.poi.hssf.record.formula.eval.NameXEval;
import org.apache.poi.hssf.record.formula.eval.NumberEval;
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;
/**
* @author Amol S. Deshmukh < amolweb at ya hoo dot com >
*
*/
public class FormulaEvaluator {
protected Sheet _sheet;
protected Workbook _workbook;
public FormulaEvaluator(Sheet sheet, Workbook workbook) {
_sheet = sheet;
_workbook = workbook;
}
/**
* Does nothing
* @deprecated (Aug 2008) - not needed, since the current row can be derived from the cell
*/
public void setCurrentRow(Row row) {
// do nothing
}
/**
* If cell contains a formula, the formula is evaluated and returned,
* else the CellValue simply copies the appropriate cell value from
* the cell and also its cell type. This method should be preferred over
* evaluateInCell() when the call should not modify the contents of the
* original cell.
* @param cell
*/
public CellValue evaluate(Cell cell) {
CellValue retval = null;
if (cell != null) {
switch (cell.getCellType()) {
case Cell.CELL_TYPE_BLANK:
retval = new CellValue(Cell.CELL_TYPE_BLANK, _workbook.getCreationHelper());
break;
case Cell.CELL_TYPE_BOOLEAN:
retval = new CellValue(Cell.CELL_TYPE_BOOLEAN, _workbook.getCreationHelper());
retval.setBooleanValue(cell.getBooleanCellValue());
break;
case Cell.CELL_TYPE_ERROR:
retval = new CellValue(Cell.CELL_TYPE_ERROR, _workbook.getCreationHelper());
retval.setErrorValue(cell.getErrorCellValue());
break;
case Cell.CELL_TYPE_FORMULA:
retval = getCellValueForEval(internalEvaluate(cell, _sheet, _workbook), _workbook.getCreationHelper());
break;
case Cell.CELL_TYPE_NUMERIC:
retval = new CellValue(Cell.CELL_TYPE_NUMERIC, _workbook.getCreationHelper());
retval.setNumberValue(cell.getNumericCellValue());
break;
case Cell.CELL_TYPE_STRING:
retval = new CellValue(Cell.CELL_TYPE_STRING, _workbook.getCreationHelper());
retval.setRichTextStringValue(cell.getRichStringCellValue());
break;
}
}
return retval;
}
/**
* If cell contains formula, it evaluates the formula,
* and saves the result of the formula. The cell
* remains as a formula cell.
* Else if cell does not contain formula, this method leaves
* the cell unchanged.
* Note that the type of the formula result is returned,
* so you know what kind of value is also stored with
* the formula.
* <pre>
* int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
* </pre>
* Be aware that your cell will hold both the formula,
* and the result. If you want the cell replaced with
* the result of the formula, use {@link #evaluateInCell(Cell)}
* @param cell The cell to evaluate
* @return The type of the formula result (the cell's type remains as Cell.CELL_TYPE_FORMULA however)
*/
public int evaluateFormulaCell(Cell cell) {
if (cell != null) {
switch (cell.getCellType()) {
case Cell.CELL_TYPE_FORMULA:
CellValue cv = getCellValueForEval(internalEvaluate(cell, _sheet, _workbook), _workbook.getCreationHelper());
switch (cv.getCellType()) {
case Cell.CELL_TYPE_BOOLEAN:
cell.setCellValue(cv.getBooleanValue());
break;
case Cell.CELL_TYPE_ERROR:
cell.setCellValue(cv.getErrorValue());
break;
case Cell.CELL_TYPE_NUMERIC:
cell.setCellValue(cv.getNumberValue());
break;
case Cell.CELL_TYPE_STRING:
cell.setCellValue(cv.getRichTextStringValue());
break;
case Cell.CELL_TYPE_BLANK:
break;
case Cell.CELL_TYPE_FORMULA: // this will never happen, we have already evaluated the formula
break;
}
return cv.getCellType();
}
}
return -1;
}
/**
* If cell contains formula, it evaluates the formula, and
* puts the formula result back into the cell, in place
* of the old formula.
* Else if cell does not contain formula, this method leaves
* the cell unchanged.
* Note that the same instance of Cell is returned to
* allow chained calls like:
* <pre>
* int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
* </pre>
* Be aware that your cell value will be changed to hold the
* result of the formula. If you simply want the formula
* value computed for you, use {@link #evaluateFormulaCell(Cell)}
* @param cell
*/
public Cell evaluateInCell(Cell cell) {
if (cell != null) {
switch (cell.getCellType()) {
case Cell.CELL_TYPE_FORMULA:
CellValue cv = getCellValueForEval(internalEvaluate(cell, _sheet, _workbook), _workbook.getCreationHelper());
switch (cv.getCellType()) {
case Cell.CELL_TYPE_BOOLEAN:
cell.setCellType(Cell.CELL_TYPE_BOOLEAN);
cell.setCellValue(cv.getBooleanValue());
break;
case Cell.CELL_TYPE_ERROR:
cell.setCellErrorValue(cv.getErrorValue());
break;
case Cell.CELL_TYPE_NUMERIC:
cell.setCellType(Cell.CELL_TYPE_NUMERIC);
cell.setCellValue(cv.getNumberValue());
break;
case Cell.CELL_TYPE_STRING:
cell.setCellType(Cell.CELL_TYPE_STRING);
cell.setCellValue(cv.getRichTextStringValue());
break;
case Cell.CELL_TYPE_BLANK:
break;
case Cell.CELL_TYPE_FORMULA: // this will never happen, we have already evaluated the formula
break;
}
}
}
return cell;
}
/**
* Loops over all cells in all sheets of the supplied
* workbook.
* For cells that contain formulas, their formulas are
* evaluated, and the results are saved. These cells
* remain as formula cells.
* For cells that do not contain formulas, no changes
* are made.
* This is a helpful wrapper around looping over all
* cells, and calling evaluateFormulaCell on each one.
*/
public static void evaluateAllFormulaCells(Workbook wb) {
for(int i=0; i<wb.getNumberOfSheets(); i++) {
Sheet sheet = wb.getSheetAt(i);
FormulaEvaluator evaluator = new FormulaEvaluator(sheet, wb);
for (Iterator rit = sheet.rowIterator(); rit.hasNext();) {
Row r = (Row)rit.next();
for (Iterator cit = r.cellIterator(); cit.hasNext();) {
Cell c = (Cell)cit.next();
if (c.getCellType() == Cell.CELL_TYPE_FORMULA)
evaluator.evaluateFormulaCell(c);
}
}
}
}
/**
* Returns a CellValue wrapper around the supplied ValueEval instance.
* @param eval
*/
private static CellValue getCellValueForEval(ValueEval eval, CreationHelper cHelper) {
CellValue retval = null;
if (eval != null) {
if (eval instanceof NumberEval) {
NumberEval ne = (NumberEval) eval;
retval = new CellValue(Cell.CELL_TYPE_NUMERIC, cHelper);
retval.setNumberValue(ne.getNumberValue());
}
else if (eval instanceof BoolEval) {
BoolEval be = (BoolEval) eval;
retval = new CellValue(Cell.CELL_TYPE_BOOLEAN, cHelper);
retval.setBooleanValue(be.getBooleanValue());
}
else if (eval instanceof StringEval) {
StringEval ne = (StringEval) eval;
retval = new CellValue(Cell.CELL_TYPE_STRING, cHelper);
retval.setStringValue(ne.getStringValue());
}
else if (eval instanceof BlankEval) {
retval = new CellValue(Cell.CELL_TYPE_BLANK, cHelper);
}
else if (eval instanceof ErrorEval) {
retval = new CellValue(Cell.CELL_TYPE_ERROR, cHelper);
retval.setErrorValue((byte)((ErrorEval)eval).getErrorCode());
// retval.setRichTextStringValue(new RichTextString("#An error occurred. check cell.getErrorCode()"));
}
else {
retval = new CellValue(Cell.CELL_TYPE_ERROR, cHelper);
}
}
return retval;
}
/**
* Dev. Note: Internal evaluate must be passed only a formula cell
* else a runtime exception will be thrown somewhere inside the method.
* (Hence this is a private method.)
*/
private static ValueEval internalEvaluate(Cell srcCell, Sheet sheet, Workbook workbook) {
int srcRowNum = srcCell.getRowIndex();
short srcColNum = srcCell.getCellNum();
EvaluationCycleDetector tracker = EvaluationCycleDetectorManager.getTracker();
if(!tracker.startEvaluate(workbook, sheet, srcRowNum, srcColNum)) {
return ErrorEval.CIRCULAR_REF_ERROR;
}
try {
return evaluateCell(workbook, sheet, srcRowNum, srcColNum, srcCell.getCellFormula());
} finally {
tracker.endEvaluate(workbook, sheet, srcRowNum, srcColNum);
}
}
private static ValueEval evaluateCell(Workbook workbook, Sheet sheet,
int srcRowNum, short srcColNum, String cellFormulaText) {
Ptg[] ptgs = FormulaParser.parse(cellFormulaText, workbook);
Stack 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 (ptg instanceof ControlPtg) {
// skip Parentheses, Attr, etc
continue;
}
if (ptg instanceof MemErrPtg) { continue; }
if (ptg instanceof MissingArgPtg) { continue; }
if (ptg instanceof NamePtg) {
// named ranges, macro functions
NamePtg namePtg = (NamePtg) ptg;
stack.push(new NameEval(namePtg.getIndex()));
continue;
}
if (ptg instanceof NameXPtg) {
NameXPtg nameXPtg = (NameXPtg) ptg;
stack.push(new NameXEval(nameXPtg.getSheetRefIndex(), nameXPtg.getNameIndex()));
continue;
}
if (ptg instanceof UnknownPtg) { continue; }
Eval opResult;
if (ptg instanceof OperationPtg) {
OperationPtg optg = (OperationPtg) ptg;
if (optg instanceof UnionPtg) { continue; }
OperationEval operation = OperationEvaluatorFactory.create(optg);
int numops = operation.getNumberOfOperands();
Eval[] ops = new Eval[numops];
// storing the ops in reverse order since they are popping
for (int j = numops - 1; j >= 0; j--) {
Eval p = (Eval) stack.pop();
ops[j] = p;
}
opResult = invokeOperation(operation, ops, srcRowNum, srcColNum, workbook, sheet);
} else {
opResult = getEvalForPtg(ptg, sheet, workbook);
}
stack.push(opResult);
}
ValueEval value = ((ValueEval) stack.pop());
if (!stack.isEmpty()) {
throw new IllegalStateException("evaluation stack not empty");
}
value = dereferenceValue(value, srcRowNum, srcColNum);
if (value instanceof BlankEval) {
// 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 <tt>NumberEval</tt>, <tt>StringEval</tt>, <tt>BoolEval</tt>,
* <tt>BlankEval</tt> or <tt>ErrorEval</tt>. Never <code>null</code>.
*/
private static ValueEval dereferenceValue(ValueEval evaluationResult, int srcRowNum, short srcColNum) {
if (evaluationResult instanceof RefEval) {
RefEval rv = (RefEval) evaluationResult;
return rv.getInnerValueEval();
}
if (evaluationResult instanceof AreaEval) {
AreaEval ae = (AreaEval) evaluationResult;
if (ae.isRow()) {
if(ae.isColumn()) {
return ae.getRelativeValue(0, 0);
}
return ae.getValueAt(ae.getFirstRow(), srcColNum);
}
if (ae.isColumn()) {
return ae.getValueAt(srcRowNum, ae.getFirstColumn());
}
return ErrorEval.VALUE_INVALID;
}
return evaluationResult;
}
private static Eval invokeOperation(OperationEval operation, Eval[] ops, int srcRowNum, short srcColNum,
Workbook workbook, Sheet sheet) {
if(operation instanceof FunctionEval) {
FunctionEval fe = (FunctionEval) operation;
if(fe.isFreeRefFunction()) {
return fe.getFreeRefFunction().evaluate(ops, srcRowNum, srcColNum, workbook, sheet);
}
}
return operation.evaluate(ops, srcRowNum, srcColNum);
}
/**
* 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 static Eval getEvalForPtg(Ptg ptg, Sheet sheet, Workbook workbook) {
if (ptg instanceof RefPtg) {
return new LazyRefEval(((RefPtg) ptg), sheet, workbook);
}
if (ptg instanceof Ref3DPtg) {
Ref3DPtg refPtg = (Ref3DPtg) ptg;
Sheet xsheet = workbook.getSheetAt(workbook.getSheetIndexFromExternSheetIndex(refPtg.getExternSheetIndex()));
return new LazyRefEval(refPtg, xsheet, workbook);
}
if (ptg instanceof AreaPtg) {
return new LazyAreaEval(((AreaPtg) ptg), sheet, workbook);
}
if (ptg instanceof Area3DPtg) {
Area3DPtg a3dp = (Area3DPtg) ptg;
Sheet xsheet = workbook.getSheetAt(workbook.getSheetIndexFromExternSheetIndex(a3dp.getExternSheetIndex()));
return new LazyAreaEval(a3dp, xsheet, workbook);
}
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());
}
throw new RuntimeException("Unexpected ptg class (" + ptg.getClass().getName() + ")");
}
/**
* Given a cell, find its type and from that create an appropriate ValueEval
* impl instance and return that. Since the cell could be an external
* reference, we need the sheet that this belongs to.
* Non existent cells are treated as empty.
* @param cell
* @param sheet
* @param workbook
*/
public static ValueEval getEvalForCell(Cell cell, Sheet sheet, Workbook workbook) {
if (cell == null) {
return BlankEval.INSTANCE;
}
switch (cell.getCellType()) {
case Cell.CELL_TYPE_NUMERIC:
return new NumberEval(cell.getNumericCellValue());
case Cell.CELL_TYPE_STRING:
return new StringEval(cell.getRichStringCellValue().getString());
case Cell.CELL_TYPE_FORMULA:
return internalEvaluate(cell, sheet, workbook);
case Cell.CELL_TYPE_BOOLEAN:
return BoolEval.valueOf(cell.getBooleanCellValue());
case Cell.CELL_TYPE_BLANK:
return BlankEval.INSTANCE;
case Cell.CELL_TYPE_ERROR:
return ErrorEval.valueOf(cell.getErrorCellValue());
}
throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")");
}
/**
* Mimics the 'data view' of a cell. This allows formula evaluator
* to return a CellValue instead of precasting the value to String
* or Number or boolean type.
* @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
*/
public static class CellValue {
private CreationHelper creationHelper;
private int cellType;
private RichTextString richTextStringValue;
private double numberValue;
private boolean booleanValue;
private byte errorValue;
/**
* CellType should be one of the types defined in Cell
* @param cellType
*/
public CellValue(int cellType, CreationHelper creationHelper) {
super();
this.creationHelper = creationHelper;
this.cellType = cellType;
}
/**
* @return Returns the booleanValue.
*/
public boolean getBooleanValue() {
return booleanValue;
}
/**
* @param booleanValue The booleanValue to set.
*/
public void setBooleanValue(boolean booleanValue) {
this.booleanValue = booleanValue;
}
/**
* @return Returns the numberValue.
*/
public double getNumberValue() {
return numberValue;
}
/**
* @param numberValue The numberValue to set.
*/
public void setNumberValue(double numberValue) {
this.numberValue = numberValue;
}
/**
* @return Returns the stringValue. This method is deprecated, use
* getRichTextStringValue instead
* @deprecated
*/
public String getStringValue() {
return richTextStringValue.getString();
}
/**
* @param stringValue The stringValue to set. This method is deprecated, use
* getRichTextStringValue instead.
* @deprecated
*/
public void setStringValue(String stringValue) {
this.richTextStringValue =
creationHelper.createRichTextString(stringValue);
}
/**
* @return Returns the cellType.
*/
public int getCellType() {
return cellType;
}
/**
* @return Returns the errorValue.
*/
public byte getErrorValue() {
return errorValue;
}
/**
* @param errorValue The errorValue to set.
*/
public void setErrorValue(byte errorValue) {
this.errorValue = errorValue;
}
/**
* @return Returns the richTextStringValue.
*/
public RichTextString getRichTextStringValue() {
return richTextStringValue;
}
/**
* @param richTextStringValue The richTextStringValue to set.
*/
public void setRichTextStringValue(RichTextString richTextStringValue) {
this.richTextStringValue = richTextStringValue;
}
}
}