Index: src/java/org/apache/poi/hssf/model/FormulaParser.java =================================================================== --- src/java/org/apache/poi/hssf/model/FormulaParser.java (revision 618865) +++ src/java/org/apache/poi/hssf/model/FormulaParser.java (working copy) @@ -947,20 +947,28 @@ // Excel allows to have AttrPtg at position 0 (such as Blanks) which // do not have any operands. Skip them. - stack.push(ptgs[0].toFormulaString(book)); + int i; + if(ptgs[0] instanceof AttrPtg) { + // TODO -this requirement is unclear and is not addressed by any junits + stack.push(ptgs[0].toFormulaString(book)); + i=1; + } else { + i=0; + } - for (int i = 1; i < ptgs.length; i++) { - if (! (ptgs[i] instanceof OperationPtg)) { - stack.push(ptgs[i].toFormulaString(book)); + for ( ; i < ptgs.length; i++) { + Ptg ptg = ptgs[i]; + if (! (ptg instanceof OperationPtg)) { + stack.push(ptg.toFormulaString(book)); continue; } - if (ptgs[i] instanceof AttrPtg && ((AttrPtg) ptgs[i]).isOptimizedIf()) { - ifptg = (AttrPtg) ptgs[i]; + if (ptg instanceof AttrPtg && ((AttrPtg) ptg).isOptimizedIf()) { + ifptg = (AttrPtg) ptg; continue; } - final OperationPtg o = (OperationPtg) ptgs[i]; + final OperationPtg o = (OperationPtg) ptg; final String[] operands = new String[o.getNumberOfOperands()]; for (int j = operands.length; j > 0; j--) { Index: src/java/org/apache/poi/hssf/record/formula/FuncPtg.java =================================================================== --- src/java/org/apache/poi/hssf/record/formula/FuncPtg.java (revision 618865) +++ src/java/org/apache/poi/hssf/record/formula/FuncPtg.java (working copy) @@ -63,6 +63,10 @@ } } + public FuncPtg(int functionIndex, int numberOfParameters) { + field_2_fnc_index = (short) functionIndex; + numParams = numberOfParameters; + } public void writeBytes(byte[] array, int offset) { array[offset+0]= (byte) (sid + ptgClass); Index: src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java (working copy) @@ -173,7 +173,7 @@ * formula evaluated. */ public static FormulaParser getUnderlyingParser(HSSFWorkbook workbook, String formula) { - return new FormulaParser(formula, workbook.getWorkbook()); + return new FormulaParser(formula, workbook.getWorkbook()); } /** @@ -286,19 +286,19 @@ CellValue cv = getCellValueForEval(internalEvaluate(cell, row, sheet, workbook)); switch (cv.getCellType()) { case HSSFCell.CELL_TYPE_BOOLEAN: - cell.setCellType(HSSFCell.CELL_TYPE_BOOLEAN); + cell.setCellType(HSSFCell.CELL_TYPE_BOOLEAN); cell.setCellValue(cv.getBooleanValue()); break; case HSSFCell.CELL_TYPE_ERROR: - cell.setCellType(HSSFCell.CELL_TYPE_ERROR); + cell.setCellType(HSSFCell.CELL_TYPE_ERROR); cell.setCellValue(cv.getErrorValue()); break; case HSSFCell.CELL_TYPE_NUMERIC: - cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC); + cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC); cell.setCellValue(cv.getNumberValue()); break; case HSSFCell.CELL_TYPE_STRING: - cell.setCellType(HSSFCell.CELL_TYPE_STRING); + cell.setCellType(HSSFCell.CELL_TYPE_STRING); cell.setCellValue(cv.getRichTextStringValue()); break; case HSSFCell.CELL_TYPE_BLANK: @@ -337,6 +337,11 @@ else if (eval instanceof BlankEval) { retval = new CellValue(HSSFCell.CELL_TYPE_BLANK); } + else if (eval instanceof ErrorEval) { + retval = new CellValue(HSSFCell.CELL_TYPE_ERROR); + retval.setErrorValue((byte)((ErrorEval)eval).getErrorCode()); +// retval.setRichTextStringValue(new HSSFRichTextString("#An error occurred. check cell.getErrorCode()")); + } else { retval = new CellValue(HSSFCell.CELL_TYPE_ERROR); } @@ -401,7 +406,7 @@ short rownum = ptg.getRow(); HSSFRow row = sheet.getRow(rownum); HSSFCell cell = (row != null) ? row.getCell(colnum) : null; - pushRef2DEval(ptg, stack, cell, row, sheet, workbook); + stack.push(createRef2DEval(ptg, cell, row, sheet, workbook)); } else if (ptgs[i] instanceof Ref3DPtg) { Ref3DPtg ptg = (Ref3DPtg) ptgs[i]; @@ -411,7 +416,7 @@ HSSFSheet xsheet = workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(ptg.getExternSheetIndex())); HSSFRow row = xsheet.getRow(rownum); HSSFCell cell = (row != null) ? row.getCell(colnum) : null; - pushRef3DEval(ptg, stack, cell, row, xsheet, workbook); + stack.push(createRef3DEval(ptg, cell, row, xsheet, workbook)); } else if (ptgs[i] instanceof AreaPtg) { AreaPtg ap = (AreaPtg) ptgs[i]; @@ -544,104 +549,77 @@ * @param workbook */ protected static ValueEval getEvalForCell(HSSFCell cell, HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { - ValueEval retval = BlankEval.INSTANCE; - if (cell != null) { - switch (cell.getCellType()) { + + if (cell == null) { + return BlankEval.INSTANCE; + } + switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_NUMERIC: - retval = new NumberEval(cell.getNumericCellValue()); - break; + return new NumberEval(cell.getNumericCellValue()); case HSSFCell.CELL_TYPE_STRING: - retval = new StringEval(cell.getRichStringCellValue().getString()); - break; + return new StringEval(cell.getRichStringCellValue().getString()); case HSSFCell.CELL_TYPE_FORMULA: - retval = internalEvaluate(cell, row, sheet, workbook); - break; + return internalEvaluate(cell, row, sheet, workbook); case HSSFCell.CELL_TYPE_BOOLEAN: - retval = cell.getBooleanCellValue() ? BoolEval.TRUE : BoolEval.FALSE; - break; + return BoolEval.valueOf(cell.getBooleanCellValue()); case HSSFCell.CELL_TYPE_BLANK: - retval = BlankEval.INSTANCE; - break; + return BlankEval.INSTANCE; case HSSFCell.CELL_TYPE_ERROR: - retval = ErrorEval.UNKNOWN_ERROR; // TODO: think about this... - break; - } + return ErrorEval.valueOf(cell.getErrorCellValue()); } - return retval; + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } /** - * create a Ref2DEval for ReferencePtg and push it on the stack. + * Creates a Ref2DEval for ReferencePtg. * Non existent cells are treated as RefEvals containing BlankEval. - * @param ptg - * @param stack - * @param cell - * @param sheet - * @param workbook */ - protected static void pushRef2DEval(ReferencePtg ptg, Stack stack, - HSSFCell cell, HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { - if (cell != null) - switch (cell.getCellType()) { + private static Ref2DEval createRef2DEval(ReferencePtg ptg, HSSFCell cell, + HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { + if (cell == null) { + return new Ref2DEval(ptg, BlankEval.INSTANCE, false); + } + + switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_NUMERIC: - stack.push(new Ref2DEval(ptg, new NumberEval(cell.getNumericCellValue()), false)); - break; + return new Ref2DEval(ptg, new NumberEval(cell.getNumericCellValue()), false); case HSSFCell.CELL_TYPE_STRING: - stack.push(new Ref2DEval(ptg, new StringEval(cell.getRichStringCellValue().getString()), false)); - break; + return new Ref2DEval(ptg, new StringEval(cell.getRichStringCellValue().getString()), false); case HSSFCell.CELL_TYPE_FORMULA: - stack.push(new Ref2DEval(ptg, internalEvaluate(cell, row, sheet, workbook), true)); - break; + return new Ref2DEval(ptg, internalEvaluate(cell, row, sheet, workbook), true); case HSSFCell.CELL_TYPE_BOOLEAN: - stack.push(new Ref2DEval(ptg, cell.getBooleanCellValue() ? BoolEval.TRUE : BoolEval.FALSE, false)); - break; + return new Ref2DEval(ptg, BoolEval.valueOf(cell.getBooleanCellValue()), false); case HSSFCell.CELL_TYPE_BLANK: - stack.push(new Ref2DEval(ptg, BlankEval.INSTANCE, false)); - break; + return new Ref2DEval(ptg, BlankEval.INSTANCE, false); case HSSFCell.CELL_TYPE_ERROR: - stack.push(new Ref2DEval(ptg, ErrorEval.UNKNOWN_ERROR, false)); // TODO: think abt this - break; - } - else { - stack.push(new Ref2DEval(ptg, BlankEval.INSTANCE, false)); + return new Ref2DEval(ptg, ErrorEval.valueOf(cell.getErrorCellValue()), false); } + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } /** - * create a Ref3DEval for Ref3DPtg and push it on the stack. - * - * @param ptg - * @param stack - * @param cell - * @param sheet - * @param workbook + * create a Ref3DEval for Ref3DPtg. */ - protected static void pushRef3DEval(Ref3DPtg ptg, Stack stack, HSSFCell cell, + private static Ref3DEval createRef3DEval(Ref3DPtg ptg, HSSFCell cell, HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { - if (cell != null) - switch (cell.getCellType()) { + if (cell == null) { + return new Ref3DEval(ptg, BlankEval.INSTANCE, false); + } + switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_NUMERIC: - stack.push(new Ref3DEval(ptg, new NumberEval(cell.getNumericCellValue()), false)); - break; + return new Ref3DEval(ptg, new NumberEval(cell.getNumericCellValue()), false); case HSSFCell.CELL_TYPE_STRING: - stack.push(new Ref3DEval(ptg, new StringEval(cell.getRichStringCellValue().getString()), false)); - break; + return new Ref3DEval(ptg, new StringEval(cell.getRichStringCellValue().getString()), false); case HSSFCell.CELL_TYPE_FORMULA: - stack.push(new Ref3DEval(ptg, internalEvaluate(cell, row, sheet, workbook), true)); - break; + return new Ref3DEval(ptg, internalEvaluate(cell, row, sheet, workbook), true); case HSSFCell.CELL_TYPE_BOOLEAN: - stack.push(new Ref3DEval(ptg, cell.getBooleanCellValue() ? BoolEval.TRUE : BoolEval.FALSE, false)); - break; + return new Ref3DEval(ptg, BoolEval.valueOf(cell.getBooleanCellValue()), false); case HSSFCell.CELL_TYPE_BLANK: - stack.push(new Ref3DEval(ptg, BlankEval.INSTANCE, false)); - break; + return new Ref3DEval(ptg, BlankEval.INSTANCE, false); case HSSFCell.CELL_TYPE_ERROR: - stack.push(new Ref3DEval(ptg, ErrorEval.UNKNOWN_ERROR, false)); // TODO: think abt this - break; - } - else { - stack.push(new Ref3DEval(ptg, BlankEval.INSTANCE, false)); + return new Ref3DEval(ptg, ErrorEval.valueOf(cell.getErrorCellValue()), false); } + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } /** @@ -726,15 +704,15 @@ /** * @return Returns the richTextStringValue. */ - public HSSFRichTextString getRichTextStringValue() { - return richTextStringValue; - } + public HSSFRichTextString getRichTextStringValue() { + return richTextStringValue; + } /** * @param richTextStringValue The richTextStringValue to set. */ - public void setRichTextStringValue(HSSFRichTextString richTextStringValue) { - this.richTextStringValue = richTextStringValue; - } + public void setRichTextStringValue(HSSFRichTextString richTextStringValue) { + this.richTextStringValue = richTextStringValue; + } } /** Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java (working copy) @@ -24,31 +24,91 @@ * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class ErrorEval implements ValueEval { +public final class ErrorEval implements ValueEval { + /** + * Contains raw Excel error codes (as defined in OOO's excelfileformat.pdf (2.5.6) + */ + private static final class ErrorCode { + /** #NULL! - Intersection of two cell ranges is empty */ + public static final int NULL = 0x00; + /** #DIV/0! - Division by zero */ + public static final int DIV_0 = 0x07; + /** #VALUE! - Wrong type of operand */ + public static final int VALUE = 0x0F; + /** #REF! - Illegal or deleted cell reference */ + public static final int REF = 0x17; + /** #NAME? - Wrong function or range name */ + public static final int NAME = 0x1D; + /** #NUM! - Value range overflow */ + public static final int NUM = 0x24; + /** #N/A - Argument or function not available */ + public static final int N_A = 0x2A; + + public static final String getText(int errorCode) { + switch(errorCode) { + case NULL: return "#NULL!"; + case DIV_0: return "#DIV/0!"; + case VALUE: return "#VALUE!"; + case REF: return "#REF!"; + case NAME: return "#NAME?"; + case NUM: return "#NUM!"; + case N_A: return "#N/A"; + } + return "???"; + } + } - private int errorCode; + /** #NULL! - Intersection of two cell ranges is empty */ + public static final ErrorEval NULL_INTERSECTION = new ErrorEval(ErrorCode.NULL); + /** #DIV/0! - Division by zero */ + public static final ErrorEval DIV_ZERO = new ErrorEval(ErrorCode.DIV_0); + /** #VALUE! - Wrong type of operand */ + public static final ErrorEval VALUE_INVALID = new ErrorEval(ErrorCode.VALUE); + /** #REF! - Illegal or deleted cell reference */ + public static final ErrorEval REF_INVALID = new ErrorEval(ErrorCode.REF); + /** #NAME? - Wrong function or range name */ + public static final ErrorEval NAME_INVALID = new ErrorEval(ErrorCode.NAME); + /** #NUM! - Value range overflow */ + public static final ErrorEval NUM_ERROR = new ErrorEval(ErrorCode.NUM); + /** #N/A - Argument or function not available */ + public static final ErrorEval NA = new ErrorEval(ErrorCode.N_A); - - public static final ErrorEval NAME_INVALID = new ErrorEval(525); - - public static final ErrorEval VALUE_INVALID = new ErrorEval(519); - - // Non std error codes + /** + * Translates an Excel internal error code into the corresponding POI ErrorEval instance + * @param errorCode + */ + public static ErrorEval valueOf(int errorCode) { + switch(errorCode) { + case ErrorCode.NULL: return NULL_INTERSECTION; + case ErrorCode.DIV_0: return DIV_ZERO; + case ErrorCode.VALUE: return VALUE_INVALID; +// case ErrorCode.REF: return REF_INVALID; + case ErrorCode.REF: return UNKNOWN_ERROR; + case ErrorCode.NAME: return NAME_INVALID; + case ErrorCode.NUM: return NUM_ERROR; + case ErrorCode.N_A: return NA; + + // these cases probably shouldn't be coming through here + // but (as of Jan-2008) a lot of code depends on it. +// case -20: return UNKNOWN_ERROR; +// case -30: return FUNCTION_NOT_IMPLEMENTED; +// case -60: return CIRCULAR_REF_ERROR; + } + throw new RuntimeException("Unexpected error code (" + errorCode + ")"); + } + + // POI internal error codes public static final ErrorEval UNKNOWN_ERROR = new ErrorEval(-20); - public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(-30); + // Note - Excel does not seem to represent this condition with an error code + public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(-60); - public static final ErrorEval REF_INVALID = new ErrorEval(-40); - public static final ErrorEval NA = new ErrorEval(-50); - - public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(-60); - - public static final ErrorEval DIV_ZERO = new ErrorEval(-70); - - public static final ErrorEval NUM_ERROR = new ErrorEval(-80); - + private int errorCode; + /** + * @param errorCode an 8-bit value + */ private ErrorEval(int errorCode) { this.errorCode = errorCode; } @@ -56,9 +116,11 @@ public int getErrorCode() { return errorCode; } - - public String getStringValue() { - return "Err:" + Integer.toString(errorCode); + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(ErrorCode.getText(errorCode)); + sb.append("]"); + return sb.toString(); } - } Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java (working copy) @@ -34,6 +34,16 @@ public static final BoolEval FALSE = new BoolEval(false); public static final BoolEval TRUE = new BoolEval(true); + + /** + * Convenience method for the following:
+ * (b ? BoolEval.TRUE : BoolEval.FALSE) + * @return a BoolEval instance representing b. + */ + public static final BoolEval valueOf(boolean b) { + // TODO - find / replace all occurrences + return b ? TRUE : FALSE; + } public BoolEval(Ptg ptg) { this.value = ((BoolPtg) ptg).getValue(); @@ -48,10 +58,17 @@ } public double getNumberValue() { - return value ? (short) 1 : (short) 0; + return value ? 1 : 0; } public String getStringValue() { return value ? "TRUE" : "FALSE"; } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getStringValue()); + sb.append("]"); + return sb.toString(); + } } Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java (working copy) @@ -58,6 +58,9 @@ // )); + /** + * called by reflection + */ public UnaryPlusEval(Ptg ptg) { this.delegate = (UnaryPlusPtg) ptg; } @@ -108,7 +111,7 @@ } else if (ae.isColumn()) { if (ae.containsRow(srcRow)) { - ValueEval ve = ae.getValueAt(ae.getFirstRow(), srcCol); + ValueEval ve = ae.getValueAt(srcRow, ae.getFirstColumn()); if (ve instanceof RefEval) { ve = ((RefEval) ve).getInnerValueEval(); } Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java (working copy) @@ -22,28 +22,34 @@ 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.RefEval; import org.apache.poi.hssf.record.formula.eval.StringEval; -import org.apache.poi.hssf.record.formula.eval.ValueEval; -public class T implements Function { - - +public final class T implements Function { - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 1: - if (operands[0] instanceof StringEval - || operands[0] instanceof ErrorEval) { - retval = (ValueEval) operands[0]; - } - else if (operands[0] instanceof ErrorEval) { - retval = StringEval.EMPTY_INSTANCE; - } + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + switch (args.length) { + default: + return ErrorEval.VALUE_INVALID; + case 1: + break; } - return retval; + Eval arg = args[0]; + if (arg instanceof RefEval) { + RefEval re = (RefEval) arg; + arg = re.getInnerValueEval(); + } + + if (arg instanceof StringEval) { + // Text values are returned unmodified + return arg; + } + + if (arg instanceof ErrorEval) { + // Error values also returned unmodified + return arg; + } + // for all other argument types the result is empty string + return StringEval.EMPTY_INSTANCE; } } Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java (working copy) @@ -40,6 +40,9 @@ break; case 2: ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); + if(ve instanceof ErrorEval) { + return ve; + } if (ve instanceof NumericValueEval) { NumericValueEval ne = (NumericValueEval) ve; d0 = ne.getNumberValue(); Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java (working copy) @@ -40,6 +40,9 @@ break; case 2: ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); + if(ve instanceof ErrorEval) { + return ve; + } if (ve instanceof NumericValueEval) { NumericValueEval ne = (NumericValueEval) ve; d0 = ne.getNumberValue(); Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java (working copy) @@ -14,16 +14,228 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + + package org.apache.poi.hssf.record.formula.functions; +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.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +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 > - * + * Implementation for the Excel function SUMPRODUCT

+ * + * Syntax :
+ * SUMPRODUCT ( array1[, array2[, array3[, ...]]]) + * + * + *
array1, ... arrayN  typically area references, + * possibly cell references or scalar values

+ * + * Let An(i,j) represent the element in the ith row jth column + * of the nth array
+ * Assuming each array has the same dimensions (W, H), the result is defined as:
+ * SUMPRODUCT = Σi: 1..H   + * (  Σj: 1..W   + * (  Πn: 1..N + * An(i,j)  + * )  + * ) + * + * @author Josh Micich */ -public class Sumproduct extends NotImplementedFunction { +public final class Sumproduct implements Function { + + private static final class EvalEx extends Exception { + private final ErrorEval _error; + + public EvalEx(ErrorEval error) { + _error = error; + } + public ErrorEval getError() { + return _error; + } + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + + int maxN = args.length; + + if(maxN < 1) { + return ErrorEval.VALUE_INVALID; + } + Eval firstArg = args[0]; + try { + if(firstArg instanceof NumericValueEval) { + return evaluateSingleProduct(args); + } + if(firstArg instanceof RefEval) { + return evaluateSingleProduct(args); + } + if(firstArg instanceof AreaEval) { + AreaEval ae = (AreaEval) firstArg; + if(ae.isRow() && ae.isColumn()) { + return evaluateSingleProduct(args); + } + return evaluateAreaSumProduct(args); + } + } catch (EvalEx e) { + return e.getError(); + } + throw new RuntimeException("Invalid arg type for SUMPRODUCT: (" + + firstArg.getClass().getName() + ")"); + } + + private Eval evaluateSingleProduct(Eval[] evalArgs) throws EvalEx { + int maxN = evalArgs.length; + + double term = 1D; + for(int n=0; ndouble value for the specified ValueEval. + * @param isScalarProduct false for SUMPRODUCTs over area refs. + * @throws EvalEx if ve represents an error value. + *

+ * Note - string values and empty cells are interpreted differently depending on + * isScalarProduct. For scalar products, if any term is blank or a string, the + * error (#VALUE!) is raised. For area (sum)products, if any term is blank or a string, the + * result is zero. + */ + private static double getProductTerm(ValueEval ve, boolean isScalarProduct) throws EvalEx { + + if(ve instanceof BlankEval || ve == null) { + // TODO - shouldn't BlankEval.INSTANCE be used always instead of null? + // null seems to occur when the blank cell is part of an area ref (but not reliably) + if(isScalarProduct) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return 0; + } + + if(ve instanceof ErrorEval) { + throw new EvalEx((ErrorEval)ve); + } + if(ve instanceof StringEval) { + if(isScalarProduct) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + // Note for area SUMPRODUCTs, string values are interpreted as zero + // even if they would parse as valid numeric values + return 0; + } + if(ve instanceof NumericValueEval) { + NumericValueEval nve = (NumericValueEval) ve; + return nve.getNumberValue(); + } + throw new RuntimeException("Unexpected value eval class (" + + ve.getClass().getName() + ")"); + } } Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Na.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Na.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Na.java (working copy) @@ -14,12 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + + package org.apache.poi.hssf.record.formula.functions; -public class Na extends NotImplementedFunction { +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +/** + * Implementation of Excel function NA() + * + * @author Josh Micich + */ +public final class Na implements Function { + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + return ErrorEval.NA; + } + } Index: src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java =================================================================== --- src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java (revision 618865) +++ src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java (working copy) @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 22, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.AreaEval; @@ -36,7 +33,50 @@ * where the order of operands does not matter */ public abstract class MultiOperandNumericFunction extends NumericFunction { + static final double[] EMPTY_DOUBLE_ARRAY = { }; + + private static class DoubleList { + private double[] _array; + private int _count; + public DoubleList() { + _array = new double[8]; + _count = 0; + } + + public double[] toArray() { + if(_count < 1) { + return EMPTY_DOUBLE_ARRAY; + } + double[] result = new double[_count]; + System.arraycopy(_array, 0, result, 0, _count); + return result; + } + + public void add(double[] values) { + int addLen = values.length; + ensureCapacity(_count + addLen); + System.arraycopy(values, 0, _array, _count, addLen); + _count += addLen; + } + + private void ensureCapacity(int reqSize) { + if(reqSize > _array.length) { + int newSize = reqSize * 3 / 2; // grow with 50% extra + double[] newArr = new double[newSize]; + System.arraycopy(_array, 0, newArr, 0, _count); + _array = newArr; + } + } + + public void add(double value) { + ensureCapacity(_count + 1); + _array[_count] = value; + _count++; + } + } + + private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED @@ -76,40 +116,26 @@ * from among the list of operands. Blanks and Blank equivalent cells * are ignored. Error operands or cells containing operands of type * that are considered invalid and would result in #VALUE! error in - * excel cause this function to return null. + * excel cause this function to return null. * * @param operands * @param srcRow * @param srcCol */ protected double[] getNumberArray(Eval[] operands, int srcRow, short srcCol) { - double[] retval = new double[30]; - int count = 0; + if (operands.length > getMaxNumOperands()) { + return null; + } + DoubleList retval = new DoubleList(); - outer: do { // goto simulator loop - if (operands.length > getMaxNumOperands()) { - break outer; + for (int i=0, iSize=operands.length; i indicate to calling subclass that error occurred - break; + return null; // indicate to calling subclass that error occurred } } + return retval.toArray(); } - else { // for ValueEvals other than AreaEval - retval = new double[1]; - ValueEval ve = singleOperandEvaluate(operand, srcRow, srcCol); - - if (ve instanceof NumericValueEval) { - NumericValueEval nve = (NumericValueEval) ve; - retval = putInArray(retval, count++, nve.getNumberValue()); - } - else if (ve instanceof BlankEval) {} // ignore operand - else { - retval = null; // null => indicate to calling subclass that error occurred - } - } - if (retval != null && retval.length >= 1) { - double[] temp = retval; - retval = new double[count]; - System.arraycopy(temp, 0, retval, 0, count); + // for ValueEvals other than AreaEval + ValueEval ve = singleOperandEvaluate(operand, srcRow, srcCol); + + if (ve instanceof NumericValueEval) { + NumericValueEval nve = (NumericValueEval) ve; + return new double[] { nve.getNumberValue(), }; } - return retval; + if (ve instanceof BlankEval) { + // ignore blanks + return EMPTY_DOUBLE_ARRAY; + } + return null; } /** - * puts d at position pos in array arr. If pos is greater than arr, the - * array is dynamically resized (using a simple doubling rule). - * @param arr - * @param pos - * @param d + * Ensures that a two dimensional array has all sub-arrays present and the same length + * @return false if any sub-array is missing, or is of different length */ - private static double[] putInArray(double[] arr, int pos, double d) { - double[] tarr = arr; - while (pos >= arr.length) { - arr = new double[arr.length << 1]; + protected static final boolean areSubArraysConsistent(double[][] values) { + + if (values == null || values.length < 1) { + // TODO this doesn't seem right. Fix or add comment. + return true; } - if (tarr.length != arr.length) { - System.arraycopy(tarr, 0, arr, 0, tarr.length); + + if (values[0] == null) { + return false; } - arr[pos] = d; - return arr; - } - - private static double[] putInArray(double[] arr, int pos, double[] d) { - double[] tarr = arr; - while (pos+d.length >= arr.length) { - arr = new double[arr.length << 1]; + int outerMax = values.length; + int innerMax = values[0].length; + for (int i=1; i 0) { - if (values[0] == null) - break outer; - int len = values[0].length; - for (int i=1, iSize=values.length; i + * + * Syntax:
+ * MATCH(lookup_value, lookup_array, match_type)

+ * + * Returns a 1-based index specifying at what position in the lookup_array the specified + * lookup_value is found.

+ * + * Specific matching behaviour can be modified with the optional match_type parameter. + * + * + * + * + * + * + *
ValueMatching Behaviour
1(default) find the largest value that is less than or equal to lookup_value. + * The lookup_array must be in ascending order*.
0find the first value that is exactly equal to lookup_value. + * The lookup_array can be in any order.
-1find the smallest value that is greater than or equal to lookup_value. + * The lookup_array must be in descending order*.
+ * + * * Note regarding order - For the match_type cases that require the lookup_array to + * be ordered, MATCH() can produce incorrect results if this requirement is not met. Observed + * behaviour in Excel is to return the lowest index value for which every item after that index + * breaks the match rule.
+ * The (ascending) sort order expected by MATCH() is:
+ * numbers (low to high), strings (A to Z), boolean (FALSE to TRUE)
+ * MATCH() ignores all elements in the lookup_array with a different type to the lookup_value. + * Type conversion of the lookup_array elements is never performed. + * + * + * @author Josh Micich + */ +public final class Match implements Function { + + private static final class EvalEx extends Exception { + private final ErrorEval _error; + + public EvalEx(ErrorEval error) { + _error = error; + } + public ErrorEval getError() { + return _error; + } + } + + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + + double match_type = 1; // default + + switch(args.length) { + case 3: + try { + match_type = evaluateMatchTypeArg(args[2], srcCellRow, srcCellCol); + } catch (EvalEx e) { + // Excel/MATCH() seems to have slightly abnormal handling of errors with + // the last parameter. Errors do not propagate up. Every error gets + // translated into #REF! + return ErrorEval.REF_INVALID; + } + case 2: + break; + default: + return ErrorEval.VALUE_INVALID; + } + + boolean matchExact = match_type == 0; + // Note - Excel does not strictly require -1 and +1 + boolean findLargestLessThanOrEqual = match_type > 0; + + + try { + ValueEval lookupValue = evaluateLookupValue(args[0], srcCellRow, srcCellCol); + ValueEval[] lookupRange = evaluateLookupRange(args[1]); + int index = findIndexOfValue(lookupValue, lookupRange, matchExact, findLargestLessThanOrEqual); + return new NumberEval(index + 1); // +1 to convert to 1-based + } catch (EvalEx e) { + return e.getError(); + } + } + + private static ValueEval chooseSingleElementFromArea(AreaEval ae, + int srcCellRow, short srcCellCol) throws EvalEx { + if (ae.isColumn()) { + if(ae.isRow()) { + return ae.getValues()[0]; + } + if(!ae.containsRow(srcCellRow)) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return ae.getValueAt(srcCellRow, ae.getFirstColumn()); + } + if(!ae.isRow()) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + if(!ae.containsColumn(srcCellCol)) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return ae.getValueAt(ae.getFirstRow(), srcCellCol); + + } + + private static ValueEval evaluateLookupValue(Eval eval, int srcCellRow, short srcCellCol) + throws EvalEx { + if (eval instanceof RefEval) { + RefEval re = (RefEval) eval; + return re.getInnerValueEval(); + } + if (eval instanceof AreaEval) { + return chooseSingleElementFromArea((AreaEval) eval, srcCellRow, srcCellCol); + } + if (eval instanceof ValueEval) { + return (ValueEval) eval; + } + throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); + } + + + private static ValueEval[] evaluateLookupRange(Eval eval) throws EvalEx { + if (eval instanceof RefEval) { + RefEval re = (RefEval) eval; + return new ValueEval[] { re.getInnerValueEval(), }; + } + if (eval instanceof AreaEval) { + AreaEval ae = (AreaEval) eval; + if(!ae.isColumn() && !ae.isRow()) { + throw new EvalEx(ErrorEval.NA); + } + return ae.getValues(); + } + + // Error handling for lookup_range arg is also unusual + if(eval instanceof NumericValueEval) { + throw new EvalEx(ErrorEval.NA); + } + if (eval instanceof StringEval) { + StringEval se = (StringEval) eval; + Double d = parseDouble(se.getStringValue()); + if(d == null) { + // plain string + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + // else looks like a number + throw new EvalEx(ErrorEval.NA); + } + throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); + } + + + private static Double parseDouble(String stringValue) { + // TODO find better home for parseDouble + return Countif.parseDouble(stringValue); + } + + + + private static double evaluateMatchTypeArg(Eval arg, int srcCellRow, short srcCellCol) + throws EvalEx { + Eval match_type = arg; + if(arg instanceof AreaReference) { + AreaEval ae = (AreaEval) arg; + // an area ref can work as a scalar value if it is 1x1 + if(ae.isColumn() && ae.isRow()) { + match_type = ae.getValues()[0]; + } else { + match_type = chooseSingleElementFromArea(ae, srcCellRow, srcCellCol); + } + } + + if(match_type instanceof RefEval) { + RefEval re = (RefEval) match_type; + match_type = re.getInnerValueEval(); + } + if(match_type instanceof ErrorEval) { + throw new EvalEx((ErrorEval)match_type); + } + if(match_type instanceof NumericValueEval) { + NumericValueEval ne = (NumericValueEval) match_type; + return ne.getNumberValue(); + } + if (match_type instanceof StringEval) { + StringEval se = (StringEval) match_type; + Double d = parseDouble(se.getStringValue()); + if(d == null) { + // plain string + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + // if the string parses as a number, it is ok + return d.doubleValue(); + } + throw new RuntimeException("Unexpected match_type type (" + match_type.getClass().getName() + ")"); + } + + /** + * @return zero based index + */ + private static int findIndexOfValue(ValueEval lookupValue, ValueEval[] lookupRange, + boolean matchExact, boolean findLargestLessThanOrEqual) throws EvalEx { + // TODO - wildcard matching when matchExact and lookupValue is text containing * or ? + if(matchExact) { + for (int i = 0; i < lookupRange.length; i++) { + ValueEval lri = lookupRange[i]; + if(lri.getClass() != lookupValue.getClass()) { + continue; + } + if(compareValues(lookupValue, lri) == 0) { + return i; + } + } + } else { + // Note - backward iteration + if(findLargestLessThanOrEqual) { + for (int i = lookupRange.length - 1; i>=0; i--) { + ValueEval lri = lookupRange[i]; + if(lri.getClass() != lookupValue.getClass()) { + continue; + } + int cmp = compareValues(lookupValue, lri); + if(cmp == 0) { + return i; + } + if(cmp > 0) { + return i; + } + } + } else { + // find smallest greater than or equal to + for (int i = 0; i 0) { + if(i<1) { + throw new EvalEx(ErrorEval.NA); + } + return i-1; + } + } + + } + } + + throw new EvalEx(ErrorEval.NA); + } + + + /** + * This method can only compare a pair of NumericValueEvals, StringEvals + * or BoolEvals + * @return negative for a<b, positive for a>b and 0 for a = b + */ + private static int compareValues(ValueEval a, ValueEval b) { + if (a instanceof StringEval) { + StringEval sa = (StringEval) a; + StringEval sb = (StringEval) b; + return sa.getStringValue().compareToIgnoreCase(sb.getStringValue()); + } + if (a instanceof NumericValueEval) { + NumericValueEval na = (NumericValueEval) a; + NumericValueEval nb = (NumericValueEval) b; + return Double.compare(na.getNumberValue(), nb.getNumberValue()); + } + if (a instanceof BoolEval) { + boolean ba = ((BoolEval) a).getBooleanValue(); + boolean bb = ((BoolEval) b).getBooleanValue(); + if(ba == bb) { + return 0; + } + // TRUE > FALSE + if(ba) { + return +1; + } + return -1; + } + throw new RuntimeException("bad eval type (" + a.getClass().getName() + ")"); + } } Index: src/scratchpad/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Index: src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java =================================================================== --- src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java (revision 618865) +++ src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java (working copy) @@ -36,8 +36,11 @@ result.addTestSuite(TestFinanceLib.class); result.addTestSuite(TestIndex.class); result.addTestSuite(TestMathX.class); + result.addTestSuite(TestMatch.class); result.addTestSuite(TestRowCol.class); + result.addTestSuite(TestSumproduct.class); result.addTestSuite(TestStatsLib.class); + result.addTestSuite(TestTFunc.class); return result; } Index: src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java =================================================================== --- src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java (revision 618865) +++ src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java (working copy) @@ -23,13 +23,14 @@ 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.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.OperationEval; /** * Test helper class for invoking functions with numeric results. * * @author Josh Micich */ -final class NumericFunctionInvoker { +public final class NumericFunctionInvoker { private NumericFunctionInvoker() { // no instances of this class @@ -61,11 +62,35 @@ } /** + * Invokes the specified operator with the arguments. + *

+ * This method cannot be used for confirming error return codes. Any non-numeric evaluation + * result causes the current junit test to fail. + */ + public static double invoke(OperationEval f, Eval[] args, int srcCellRow, int srcCellCol) { + try { + return invokeInternal(f, args, srcCellRow, srcCellCol); + } catch (NumericEvalEx e) { + throw new AssertionFailedError("Evaluation of function (" + f.getClass().getName() + + ") failed: " + e.getMessage()); + } + + } + /** * Formats nicer error messages for the junit output */ - private static double invokeInternal(Function f, Eval[] args, int srcCellRow, int srcCellCol) + private static double invokeInternal(Object target, Eval[] args, int srcCellRow, int srcCellCol) throws NumericEvalEx { - Eval evalResult = f.evaluate(args, srcCellRow, (short)srcCellCol); + Eval evalResult; + // TODO - make OperationEval extend Function + if (target instanceof Function) { + Function ff = (Function) target; + evalResult = ff.evaluate(args, srcCellRow, (short)srcCellCol); + } else { + OperationEval ff = (OperationEval) target; + evalResult = ff.evaluate(args, srcCellRow, (short)srcCellCol); + } + if(evalResult == null) { throw new NumericEvalEx("Result object was null"); } @@ -89,6 +114,9 @@ if(errorCodesAreEqual(ee, ErrorEval.UNKNOWN_ERROR)) { return "Unknown error"; } + if(errorCodesAreEqual(ee, ErrorEval.VALUE_INVALID)) { + return "Error code: #VALUE! (invalid value)"; + } return "Error code=" + ee.getErrorCode(); } private static boolean errorCodesAreEqual(ErrorEval a, ErrorEval b) { Index: src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java =================================================================== --- src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java (revision 618865) +++ src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java (working copy) @@ -18,6 +18,8 @@ package org.apache.poi.hssf.model; +import java.util.List; + import junit.framework.TestCase; import org.apache.poi.hssf.record.formula.AbstractFunctionPtg; @@ -26,6 +28,7 @@ import org.apache.poi.hssf.record.formula.BoolPtg; import org.apache.poi.hssf.record.formula.DividePtg; import org.apache.poi.hssf.record.formula.EqualPtg; +import org.apache.poi.hssf.record.formula.FuncPtg; import org.apache.poi.hssf.record.formula.FuncVarPtg; import org.apache.poi.hssf.record.formula.IntPtg; import org.apache.poi.hssf.record.formula.LessEqualPtg; @@ -397,7 +400,7 @@ public void testUnderscore() { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -438,7 +441,7 @@ public void testExponentialInSheet() throws Exception { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -514,7 +517,7 @@ public void testNumbers() { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -553,7 +556,7 @@ public void testRanges() { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -571,5 +574,19 @@ cell.setCellFormula("A1...A2"); formula = cell.getCellFormula(); assertEquals("A1:A2", formula); - } + } + + /** + * Test for bug observable at svn revision 618865 (5-Feb-2008)
+ * a formula consisting of a single no-arg function got rendered without the function braces + */ + public void testToFormulaStringZeroArgFunction() { + + Workbook book = Workbook.createWorkbook(); // not really used in this test + + Ptg[] ptgs = { + new FuncPtg(10, 0), + }; + assertEquals("NA()", FormulaParser.toFormulaString(book, ptgs)); + } } Index: src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream