From: Nick Burch Date: Thu, 14 Feb 2008 16:01:10 +0000 (+0000) Subject: Big formula update from Josh from bug #44364 - support for Match, NA and SumProduct... X-Git-Tag: REL_3_0_3_BETA1~136 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2598100690fe56d623b46352d1da6a267c26f8ed;p=poi.git Big formula update from Josh from bug #44364 - support for Match, NA and SumProduct functions, and initial error support in functions git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@627788 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 34345df5d0..26de01f977 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -36,6 +36,7 @@ + 44364 - Support for Match, NA and SumProduct functions, as well as initial function error support 44375 - Cope with a broken dictionary in Document Summary Information stream. RuntimeExceptions that occured when trying to read bogus data are now caught. Dictionary entries up to but not including the bogus one are preserved, the rest is ignored. 38641 - Handle timezones better with cell.setCellValue(Calendar), so now 20:00-03:00, 20:00+00:00 and 20:00+03:00 will all be recorded as 20:00, and not 17:00 / 20:00 / 23:00 (pass a Date not a Calendar for old behaviour) 44373 - Have HSSFDateUtil.isADateFormat recognize more formats as being dates diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 91641b13ba..2407b58fef 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -33,6 +33,7 @@ + 44364 - Support for Match, NA and SumProduct functions, as well as initial function error support 44375 - Cope with a broken dictionary in Document Summary Information stream. RuntimeExceptions that occured when trying to read bogus data are now caught. Dictionary entries up to but not including the bogus one are preserved, the rest is ignored. 38641 - Handle timezones better with cell.setCellValue(Calendar), so now 20:00-03:00, 20:00+00:00 and 20:00+03:00 will all be recorded as 20:00, and not 17:00 / 20:00 / 23:00 (pass a Date not a Calendar for old behaviour) 44373 - Have HSSFDateUtil.isADateFormat recognize more formats as being dates diff --git a/src/java/org/apache/poi/hssf/model/FormulaParser.java b/src/java/org/apache/poi/hssf/model/FormulaParser.java index 832dbdef7a..1d78b1b239 100644 --- a/src/java/org/apache/poi/hssf/model/FormulaParser.java +++ b/src/java/org/apache/poi/hssf/model/FormulaParser.java @@ -947,20 +947,28 @@ end; // 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--) { diff --git a/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java b/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java index 410364971c..1123fc803a 100644 --- a/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java @@ -62,6 +62,10 @@ public class FuncPtg extends AbstractFunctionPtg{ numParams=0; } + } + public FuncPtg(int functionIndex, int numberOfParameters) { + field_2_fnc_index = (short) functionIndex; + numParams = numberOfParameters; } public void writeBytes(byte[] array, int offset) { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java index 6a04068b76..7b625aaa98 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java @@ -34,6 +34,16 @@ public class BoolEval implements NumericValueEval, StringValueEval { 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 class BoolEval implements NumericValueEval, StringValueEval { } 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(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java index 43ef6c5127..f57976d3ed 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java @@ -24,31 +24,91 @@ package org.apache.poi.hssf.record.formula.eval; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class ErrorEval implements ValueEval { - - private int errorCode; - - - public static final ErrorEval NAME_INVALID = new ErrorEval(525); +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 "???"; + } + } - public static final ErrorEval VALUE_INVALID = new ErrorEval(519); + /** #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); - // 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 class ErrorEval implements ValueEval { 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(); } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java index 847aa56fa6..7816b228f7 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java @@ -58,6 +58,9 @@ public class UnaryPlusEval implements OperationEval /*extends NumericOperationEv // )); + /** + * called by reflection + */ public UnaryPlusEval(Ptg ptg) { this.delegate = (UnaryPlusPtg) ptg; } @@ -108,7 +111,7 @@ public class UnaryPlusEval implements OperationEval /*extends NumericOperationEv } 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(); } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Match.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Match.java index 74afaf695c..b2b252c4f0 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Match.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Match.java @@ -14,12 +14,300 @@ * 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 Match extends NotImplementedFunction { +import org.apache.poi.hssf.record.formula.eval.AreaEval; +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.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; +import org.apache.poi.hssf.util.AreaReference; + +/** + * Implementation for the MATCH() Excel function.

+ * + * 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() + ")"); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java index 2840c89883..f7dc60fea7 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java @@ -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,6 +33,49 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; * 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) ( @@ -76,40 +116,26 @@ public abstract class MultiOperandNumericFunction extends NumericFunction { * 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]; - } - if (tarr.length != arr.length) { - System.arraycopy(tarr, 0, arr, 0, tarr.length); - } - 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]; + 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; } - for (int i=0, iSize=d.length; i 0) { - if (values[0] == null) - break outer; - int len = values[0].length; - for (int i=1, iSize=values.length; i + * + * 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() + ")"); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java index 686c40b627..b322cd985c 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java @@ -22,28 +22,34 @@ package org.apache.poi.hssf.record.formula.functions; 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; } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index f60a6adaac..70b78dfae4 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -173,7 +173,7 @@ public class HSSFFormulaEvaluator { * 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 @@ public class HSSFFormulaEvaluator { 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 @@ public class HSSFFormulaEvaluator { 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 @@ public class HSSFFormulaEvaluator { 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 @@ public class HSSFFormulaEvaluator { 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 @@ public class HSSFFormulaEvaluator { * @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 @@ public class HSSFFormulaEvaluator { /** * @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; + } } /** diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-bad.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-bad.xls deleted file mode 100644 index 54a7edb404..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-bad.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-ok.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-ok.xls deleted file mode 100644 index 5ae84bc90e..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-ok.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/GenericFormulaTestCase.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/GenericFormulaTestCase.java deleted file mode 100644 index b53e8ebc3a..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/GenericFormulaTestCase.java +++ /dev/null @@ -1,147 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -/* - * Created on May 11, 2005 - * - */ -package org.apache.poi.hssf.record.formula.eval; - -import java.io.FileInputStream; - -import junit.framework.AssertionFailedError; -import junit.framework.TestCase; - -import org.apache.poi.hssf.record.formula.functions.TestMathX; -import org.apache.poi.hssf.usermodel.HSSFCell; -import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; -import org.apache.poi.hssf.usermodel.HSSFRow; -import org.apache.poi.hssf.usermodel.HSSFSheet; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.hssf.util.CellReference; - -/** - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * - */ -public class GenericFormulaTestCase extends TestCase { - - protected final static String FILENAME = System.getProperty("HSSF.testdata.path")+ "/FormulaEvalTestData.xls"; - - protected static HSSFWorkbook workbook = null; - - protected CellReference beginCell; - protected int getBeginRow() { - return beginCell.getRow(); - } - - protected short getBeginCol() { - return beginCell.getCol(); - } - - protected final HSSFCell getExpectedValueCell(HSSFSheet sheet, HSSFRow row, HSSFCell cell) { - HSSFCell retval = null; - if (sheet != null) { - row = sheet.getRow(row.getRowNum()+1); - if (row != null) { - retval = row.getCell(cell.getCellNum()); - } - } - - return retval; - } - - protected void assertEquals(String msg, HSSFCell expected, HSSFFormulaEvaluator.CellValue actual) { - if (expected != null && actual!=null) { - if (expected!=null && expected.getCellType() == HSSFCell.CELL_TYPE_STRING) { - String value = expected.getRichStringCellValue().getString(); - if (value.startsWith("#")) { - expected.setCellType(HSSFCell.CELL_TYPE_ERROR); - } - } - if (!(expected == null || actual == null)) { - switch (expected.getCellType()) { - case HSSFCell.CELL_TYPE_BLANK: - assertEquals(msg, HSSFCell.CELL_TYPE_BLANK, actual.getCellType()); - break; - case HSSFCell.CELL_TYPE_BOOLEAN: - assertEquals(msg, HSSFCell.CELL_TYPE_BOOLEAN, actual.getCellType()); - assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); - break; - case HSSFCell.CELL_TYPE_ERROR: - assertEquals(msg, HSSFCell.CELL_TYPE_ERROR, actual.getCellType()); // TODO: check if exact error matches - break; - case HSSFCell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation - throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); - case HSSFCell.CELL_TYPE_NUMERIC: - assertEquals(msg, HSSFCell.CELL_TYPE_NUMERIC, actual.getCellType()); - TestMathX.assertEquals(msg, expected.getNumericCellValue(), actual.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); -// double delta = Math.abs(expected.getNumericCellValue()-actual.getNumberValue()); -// double pctExpected = Math.abs(0.00001*expected.getNumericCellValue()); -// assertTrue(msg, delta <= pctExpected); - break; - case HSSFCell.CELL_TYPE_STRING: - assertEquals(msg, HSSFCell.CELL_TYPE_STRING, actual.getCellType()); - assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getRichTextStringValue().getString()); - break; - } - } - else { - throw new AssertionFailedError("expected: " + expected + " got:" + actual); - } - } - } - - public GenericFormulaTestCase(String beginCell) throws Exception { - super("genericTest"); - if (workbook == null) { - FileInputStream fin = new FileInputStream( FILENAME ); - workbook = new HSSFWorkbook( fin ); - fin.close(); - } - this.beginCell = new CellReference(beginCell); - } - - public void setUp() { - } - - public void genericTest() throws Exception { - HSSFSheet s = workbook.getSheetAt( 0 ); - HSSFRow r = s.getRow(getBeginRow()); - short endcolnum = r.getLastCellNum(); - HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(s, workbook); - evaluator.setCurrentRow(r); - - HSSFCell c = null; - for (short colnum=getBeginCol(); colnum < endcolnum; colnum++) { - try { - c = r.getCell(colnum); - if (c==null || c.getCellType() != HSSFCell.CELL_TYPE_FORMULA) - continue; - - HSSFFormulaEvaluator.CellValue actualValue = evaluator.evaluate(c); - - HSSFCell expectedValueCell = getExpectedValueCell(s, r, c); - assertEquals("Formula: " + c.getCellFormula() - + " @ " + getBeginRow() + ":" + colnum, - expectedValueCell, actualValue); - } catch (RuntimeException re) { - throw new RuntimeException("CELL["+getBeginRow()+","+colnum+"]: "+re.getMessage(), re); - } - } - } - -} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestEverything.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestEverything.java deleted file mode 100644 index ac3ca2eb29..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestEverything.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -/* - * Created on May 11, 2005 - * - */ -package org.apache.poi.hssf.record.formula.eval; - -import junit.framework.TestSuite; - -/** - * This is a test of all the Eval functions we have implemented. - * Add newly implemented Eval functions in here to have them - * tested. - * For newly implemented functions, - * @see org.apache.poi.hssf.record.formula.functions.TestEverything - * - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - */ -public class TestEverything extends TestSuite { - - public static TestSuite suite() throws Exception { - TestSuite suite = new TestSuite("Tests for OperationEval concrete implementation classes."); - suite.addTest(new GenericFormulaTestCase("D23")); // Add - suite.addTest(new GenericFormulaTestCase("D27")); // ConcatEval - suite.addTest(new GenericFormulaTestCase("D31")); // DivideEval - suite.addTest(new GenericFormulaTestCase("D35")); // EqualEval - suite.addTest(new GenericFormulaTestCase("D39")); // GreaterEqualEval - suite.addTest(new GenericFormulaTestCase("D43")); // GreaterThanEval - suite.addTest(new GenericFormulaTestCase("D47")); // LessEqualEval - suite.addTest(new GenericFormulaTestCase("D51")); // LessThanEval - suite.addTest(new GenericFormulaTestCase("D55")); // MultiplyEval - suite.addTest(new GenericFormulaTestCase("D59")); // NotEqualEval - suite.addTest(new GenericFormulaTestCase("D63")); // PowerEval - suite.addTest(new GenericFormulaTestCase("D67")); // SubtractEval - suite.addTest(new GenericFormulaTestCase("D71")); // UnaryMinusEval - suite.addTest(new GenericFormulaTestCase("D75")); // UnaryPlusEval - - // Add newly implemented Eval functions here - // (Formula functions go in - // @see org.apache.poi.hssf.record.formula.functions.TestEverything ) - - return suite; - } -} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulasFromSpreadsheet.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulasFromSpreadsheet.java new file mode 100644 index 0000000000..f57221c9b0 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulasFromSpreadsheet.java @@ -0,0 +1,329 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +package org.apache.poi.hssf.record.formula.eval; + +import java.io.FileInputStream; +import java.io.PrintStream; + +import junit.framework.Assert; +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.functions.TestMathX; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + +/** + * Tests formulas and operators as loaded from a test data spreadsheet.

+ * This class does not test implementors of Function and OperationEval in + * isolation. Much of the evaluation engine (i.e. HSSFFormulaEvaluator, ...) gets + * exercised as well. Tests for bug fixes and specific/tricky behaviour can be found in the + * corresponding test class (TestXxxx) of the target (Xxxx) implementor, + * where execution can be observed more easily. + * + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class TestFormulasFromSpreadsheet extends TestCase { + + private static final class Result { + public static final int SOME_EVALUATIONS_FAILED = -1; + public static final int ALL_EVALUATIONS_SUCCEEDED = +1; + public static final int NO_EVALUATIONS_FOUND = 0; + } + + /** + * This class defines constants for navigating around the test data spreadsheet used for these tests. + */ + private static final class SS { + + /** + * Name of the test spreadsheet (found in the standard test data folder) + */ + public final static String FILENAME = "FormulaEvalTestData.xls"; + /** + * Row (zero-based) in the test spreadsheet where the operator examples start. + */ + public static final int START_OPERATORS_ROW_INDEX = 22; // Row '23' + /** + * Row (zero-based) in the test spreadsheet where the function examples start. + */ + public static final int START_FUNCTIONS_ROW_INDEX = 83; // Row '84' + /** + * Index of the column that contains the function names + */ + public static final short COLUMN_INDEX_FUNCTION_NAME = 1; // Column 'B' + + /** + * Used to indicate when there are no more functions left + */ + public static final String FUNCTION_NAMES_END_SENTINEL = ""; + + /** + * Index of the column where the test values start (for each function) + */ + public static final short COLUMN_INDEX_FIRST_TEST_VALUE = 3; // Column 'D' + + /** + * Each function takes 4 rows in the test spreadsheet + */ + public static final int NUMBER_OF_ROWS_PER_FUNCTION = 4; + } + + private HSSFWorkbook workbook; + private HSSFSheet sheet; + // Note - multiple failures are aggregated before ending. + // If one or more functions fail, a single AssertionFailedError is thrown at the end + private int _functionFailureCount; + private int _functionSuccessCount; + private int _evaluationFailureCount; + private int _evaluationSuccessCount; + + private static final HSSFCell getExpectedValueCell(HSSFRow row, short columnIndex) { + if (row == null) { + return null; + } + return row.getCell(columnIndex); + } + + + private static void confirmExpectedResult(String msg, HSSFCell expected, HSSFFormulaEvaluator.CellValue actual) { + if (expected == null) { + throw new AssertionFailedError(msg + " - Bad setup data expected value is null"); + } + if(actual == null) { + throw new AssertionFailedError(msg + " - actual value was null"); + } + + if (expected.getCellType() == HSSFCell.CELL_TYPE_STRING) { + String value = expected.getRichStringCellValue().getString(); + if (value.startsWith("#")) { + // TODO - this code never called + expected.setCellType(HSSFCell.CELL_TYPE_ERROR); + // expected.setCellErrorValue(...?); + } + } + + switch (expected.getCellType()) { + case HSSFCell.CELL_TYPE_BLANK: + assertEquals(msg, HSSFCell.CELL_TYPE_BLANK, actual.getCellType()); + break; + case HSSFCell.CELL_TYPE_BOOLEAN: + assertEquals(msg, HSSFCell.CELL_TYPE_BOOLEAN, actual.getCellType()); + assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); + break; + case HSSFCell.CELL_TYPE_ERROR: + assertEquals(msg, HSSFCell.CELL_TYPE_ERROR, actual.getCellType()); + if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values + assertEquals(msg, expected.getErrorCellValue(), actual.getErrorValue()); + } + break; + case HSSFCell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation + throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); + case HSSFCell.CELL_TYPE_NUMERIC: + assertEquals(msg, HSSFCell.CELL_TYPE_NUMERIC, actual.getCellType()); + TestMathX.assertEquals(msg, expected.getNumericCellValue(), actual.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); +// double delta = Math.abs(expected.getNumericCellValue()-actual.getNumberValue()); +// double pctExpected = Math.abs(0.00001*expected.getNumericCellValue()); +// assertTrue(msg, delta <= pctExpected); + break; + case HSSFCell.CELL_TYPE_STRING: + assertEquals(msg, HSSFCell.CELL_TYPE_STRING, actual.getCellType()); + assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getRichTextStringValue().getString()); + break; + } + } + + + protected void setUp() throws Exception { + if (workbook == null) { + String filePath = System.getProperty("HSSF.testdata.path")+ "/" + SS.FILENAME; + FileInputStream fin = new FileInputStream( filePath ); + workbook = new HSSFWorkbook( fin ); + sheet = workbook.getSheetAt( 0 ); + } + _functionFailureCount = 0; + _functionSuccessCount = 0; + _evaluationFailureCount = 0; + _evaluationSuccessCount = 0; + } + + public void testFunctionsFromTestSpreadsheet() { + + processFunctionGroup(SS.START_OPERATORS_ROW_INDEX, null); + processFunctionGroup(SS.START_FUNCTIONS_ROW_INDEX, null); + // example for debugging individual functions/operators: +// processFunctionGroup(SS.START_OPERATORS_ROW_INDEX, "ConcatEval"); +// processFunctionGroup(SS.START_FUNCTIONS_ROW_INDEX, "AVERAGE"); + + // confirm results + String successMsg = "There were " + + _evaluationSuccessCount + " successful evaluation(s) and " + + _functionSuccessCount + " function(s) without error"; + if(_functionFailureCount > 0) { + String msg = _functionFailureCount + " function(s) failed in " + + _evaluationFailureCount + " evaluation(s). " + successMsg; + throw new AssertionFailedError(msg); + } + if(false) { // normally no output for successful tests + System.out.println(getClass().getName() + ": " + successMsg); + } + } + + /** + * @param startRowIndex row index in the spreadsheet where the first function/operator is found + * @param testFocusFunctionName name of a single function/operator to test alone. + * Typically pass null to test all functions + */ + private void processFunctionGroup(int startRowIndex, String testFocusFunctionName) { + + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, workbook); + + int rowIndex = startRowIndex; + while (true) { + HSSFRow r = sheet.getRow(rowIndex); + String targetFunctionName = getTargetFunctionName(r); + if(targetFunctionName == null) { + throw new AssertionFailedError("Test spreadsheet cell empty on row (" + + (rowIndex+1) + "). Expected function name or '" + + SS.FUNCTION_NAMES_END_SENTINEL + "'"); + } + if(targetFunctionName.equals(SS.FUNCTION_NAMES_END_SENTINEL)) { + // found end of functions list + break; + } + if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) { + + // expected results are on the row below + HSSFRow expectedValuesRow = sheet.getRow(rowIndex + 1); + if(expectedValuesRow == null) { + int missingRowNum = rowIndex + 2; //+1 for 1-based, +1 for next row + throw new AssertionFailedError("Missing expected values row for function '" + + targetFunctionName + " (row " + missingRowNum + ")"); + } + switch(processFunctionRow(evaluator, targetFunctionName, r, expectedValuesRow)) { + case Result.ALL_EVALUATIONS_SUCCEEDED: _functionSuccessCount++; break; + case Result.SOME_EVALUATIONS_FAILED: _functionFailureCount++; break; + default: + throw new RuntimeException("unexpected result"); + case Result.NO_EVALUATIONS_FOUND: // do nothing + } + } + rowIndex += SS.NUMBER_OF_ROWS_PER_FUNCTION; + } + } + + /** + * + * @return a constant from the local Result class denoting whether there were any evaluation + * cases, and whether they all succeeded. + */ + private int processFunctionRow(HSSFFormulaEvaluator evaluator, String targetFunctionName, + HSSFRow formulasRow, HSSFRow expectedValuesRow) { + + int result = Result.NO_EVALUATIONS_FOUND; // so far + short endcolnum = formulasRow.getLastCellNum(); + evaluator.setCurrentRow(formulasRow); + + // iterate across the row for all the evaluation cases + for (short colnum=SS.COLUMN_INDEX_FIRST_TEST_VALUE; colnum < endcolnum; colnum++) { + HSSFCell c = formulasRow.getCell(colnum); + if (c == null || c.getCellType() != HSSFCell.CELL_TYPE_FORMULA) { + continue; + } + + HSSFFormulaEvaluator.CellValue actualValue = evaluator.evaluate(c); + + HSSFCell expectedValueCell = getExpectedValueCell(expectedValuesRow, colnum); + try { + confirmExpectedResult("Function '" + targetFunctionName + "': Formula: " + c.getCellFormula() + " @ " + formulasRow.getRowNum() + ":" + colnum, + expectedValueCell, actualValue); + _evaluationSuccessCount ++; + if(result != Result.SOME_EVALUATIONS_FAILED) { + result = Result.ALL_EVALUATIONS_SUCCEEDED; + } + } catch (AssertionFailedError e) { + _evaluationFailureCount ++; + printShortStackTrace(System.err, e); + result = Result.SOME_EVALUATIONS_FAILED; + } + } + return result; + } + + /** + * Useful to keep output concise when expecting many failures to be reported by this test case + */ + private static void printShortStackTrace(PrintStream ps, AssertionFailedError e) { + StackTraceElement[] stes = e.getStackTrace(); + + int startIx = 0; + // skip any top frames inside junit.framework.Assert + while(startIx= endIx) { + // something went wrong. just print the whole stack trace + e.printStackTrace(ps); + } + endIx -= 4; // skip 4 frames of reflection invocation + ps.println(e.toString()); + for(int i=startIx; inull if cell is missing, empty or blank + */ + private static String getTargetFunctionName(HSSFRow r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out function name"); + return null; + } + HSSFCell cell = r.getCell(SS.COLUMN_INDEX_FUNCTION_NAME); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + SS.COLUMN_INDEX_FUNCTION_NAME + ", can't figure out function name"); + return null; + } + if(cell.getCellType() == HSSFCell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == HSSFCell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'function name' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestUnaryPlusEval.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestUnaryPlusEval.java new file mode 100755 index 0000000000..724c54cd90 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestUnaryPlusEval.java @@ -0,0 +1,61 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +package org.apache.poi.hssf.record.formula.eval; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.UnaryPlusPtg; +import org.apache.poi.hssf.record.formula.functions.NumericFunctionInvoker; + +import junit.framework.TestCase; + +/** + * Test for unary plus operator evaluator. + * + * @author Josh Micich + */ +public final class TestUnaryPlusEval extends TestCase { + + /** + * Test for bug observable at svn revision 618865 (5-Feb-2008)
+ * The code for handling column operands had been copy-pasted from the row handling code. + */ + public void testColumnOperand() { + + short firstRow = (short)8; + short lastRow = (short)12; + short colNum = (short)5; + AreaPtg areaPtg = new AreaPtg(firstRow, lastRow, colNum, colNum, false, false, false, false); + ValueEval[] values = { + new NumberEval(27), + new NumberEval(29), + new NumberEval(35), // value in row 10 + new NumberEval(37), + new NumberEval(38), + }; + Eval areaEval = new Area2DEval(areaPtg, values); + Eval[] args = { + areaEval, + }; + + double result = NumericFunctionInvoker.invoke(new UnaryPlusEval(new UnaryPlusPtg()), args, 10, (short)20); + + assertEquals(35, result, 0); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java index b5e0843671..c504aaa0a6 100755 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java @@ -36,8 +36,11 @@ public final class AllIndividualFunctionEvaluationTests { 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; } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java index 87405a4918..2093d35b1e 100755 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java @@ -23,13 +23,14 @@ import junit.framework.AssertionFailedError; 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 @@ -59,13 +60,37 @@ final class NumericFunctionInvoker { + ") failed: " + e.getMessage()); } + } + /** + * 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 @@ final class NumericFunctionInvoker { 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) { diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestEverything.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestEverything.java deleted file mode 100644 index 8337810216..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestEverything.java +++ /dev/null @@ -1,48 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -/* - * Created on May 11, 2005 - * - */ -package org.apache.poi.hssf.record.formula.functions; - -import org.apache.poi.hssf.record.formula.eval.GenericFormulaTestCase; - -import junit.framework.TestSuite; - -/** - * This is a test of all the normal formula functions we have implemented. - * It should pick up newly implemented functions which are correctly added - * to the test formula excel file, but tweak the rows below if you - * add any past the end of what's currently checked. - * For newly implemented eval functions, - * @see org.apache.poi.hssf.record.formula.eval.TestEverything - * - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - */ -public class TestEverything extends TestSuite { - public static TestSuite suite() throws Exception { - TestSuite suite = new TestSuite("Tests for individual function classes"); - String s; - for(int i=80; i<1485;i=i+4) { - s = "D"+Integer.toString(i).trim(); - suite.addTest(new GenericFormulaTestCase(s)); - } -// suite.addTest(new GenericFormulaTestCase("D1164")); - return suite; - } -} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMatch.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMatch.java new file mode 100755 index 0000000000..7ad528d61f --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMatch.java @@ -0,0 +1,184 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.eval.Area2DEval; +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.NumberEval; +import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +/** + * Test cases for MATCH() + * + * @author Josh Micich + */ +public final class TestMatch extends TestCase { + /** less than or equal to */ + private static final NumberEval MATCH_LARGEST_LTE = new NumberEval(1); + private static final NumberEval MATCH_EXACT = new NumberEval(0); + /** greater than or equal to */ + private static final NumberEval MATCH_SMALLEST_GTE = new NumberEval(-1); + + + private static Eval invokeMatch(Eval lookup_value, Eval lookup_array, Eval match_type) { + Eval[] args = { lookup_value, lookup_array, match_type, }; + return new Match().evaluate(args, -1, (short)-1); + } + private static void confirmInt(int expected, Eval actualEval) { + if(!(actualEval instanceof NumericValueEval)) { + fail("Expected numeric result"); + } + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + + public void testSimpleNumber() { + + ValueEval[] values = { + new NumberEval(4), + new NumberEval(5), + new NumberEval(10), + new NumberEval(10), + new NumberEval(25), + }; + + AreaPtg areaPtg = new AreaPtg("A1:A5"); + Area2DEval ae = new Area2DEval(areaPtg, values); + + + confirmInt(2, invokeMatch(new NumberEval(5), ae, MATCH_LARGEST_LTE)); + confirmInt(2, invokeMatch(new NumberEval(5), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new NumberEval(10), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new NumberEval(10), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new NumberEval(20), ae, MATCH_LARGEST_LTE)); + assertEquals(ErrorEval.NA, invokeMatch(new NumberEval(20), ae, MATCH_EXACT)); + } + + public void testReversedNumber() { + + ValueEval[] values = { + new NumberEval(25), + new NumberEval(10), + new NumberEval(10), + new NumberEval(10), + new NumberEval(4), + }; + + AreaPtg areaPtg = new AreaPtg("A1:A5"); + Area2DEval ae = new Area2DEval(areaPtg, values); + + + confirmInt(2, invokeMatch(new NumberEval(10), ae, MATCH_SMALLEST_GTE)); + confirmInt(2, invokeMatch(new NumberEval(10), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new NumberEval(9), ae, MATCH_SMALLEST_GTE)); + confirmInt(1, invokeMatch(new NumberEval(20), ae, MATCH_SMALLEST_GTE)); + assertEquals(ErrorEval.NA, invokeMatch(new NumberEval(20), ae, MATCH_EXACT)); + assertEquals(ErrorEval.NA, invokeMatch(new NumberEval(26), ae, MATCH_SMALLEST_GTE)); + } + + public void testSimpleString() { + + ValueEval[] values = { + new StringEval("Albert"), + new StringEval("Charles"), + new StringEval("Ed"), + new StringEval("Greg"), + new StringEval("Ian"), + }; + + AreaPtg areaPtg = new AreaPtg("A1:A5"); + Area2DEval ae = new Area2DEval(areaPtg, values); + + // Note String comparisons are case insensitive + confirmInt(3, invokeMatch(new StringEval("Ed"), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new StringEval("eD"), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new StringEval("Ed"), ae, MATCH_EXACT)); + confirmInt(3, invokeMatch(new StringEval("ed"), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new StringEval("Hugh"), ae, MATCH_LARGEST_LTE)); + assertEquals(ErrorEval.NA, invokeMatch(new StringEval("Hugh"), ae, MATCH_EXACT)); + } + + public void testSimpleBoolean() { + + ValueEval[] values = { + BoolEval.FALSE, + BoolEval.FALSE, + BoolEval.TRUE, + BoolEval.TRUE, + }; + + AreaPtg areaPtg = new AreaPtg("A1:A4"); + Area2DEval ae = new Area2DEval(areaPtg, values); + + // Note String comparisons are case insensitive + confirmInt(2, invokeMatch(BoolEval.FALSE, ae, MATCH_LARGEST_LTE)); + confirmInt(1, invokeMatch(BoolEval.FALSE, ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(BoolEval.TRUE, ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(BoolEval.TRUE, ae, MATCH_EXACT)); + } + + public void testHeterogeneous() { + + ValueEval[] values = { + new NumberEval(4), + BoolEval.FALSE, + new NumberEval(5), + new StringEval("Albert"), + BoolEval.FALSE, + BoolEval.TRUE, + new NumberEval(10), + new StringEval("Charles"), + new StringEval("Ed"), + new NumberEval(10), + new NumberEval(25), + BoolEval.TRUE, + new StringEval("Ed"), + }; + + AreaPtg areaPtg = new AreaPtg("A1:A13"); + Area2DEval ae = new Area2DEval(areaPtg, values); + + assertEquals(ErrorEval.NA, invokeMatch(new StringEval("Aaron"), ae, MATCH_LARGEST_LTE)); + + confirmInt(5, invokeMatch(BoolEval.FALSE, ae, MATCH_LARGEST_LTE)); + confirmInt(2, invokeMatch(BoolEval.FALSE, ae, MATCH_EXACT)); + confirmInt(3, invokeMatch(new NumberEval(5), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new NumberEval(5), ae, MATCH_EXACT)); + + confirmInt(8, invokeMatch(new StringEval("CHARLES"), ae, MATCH_EXACT)); + + confirmInt(4, invokeMatch(new StringEval("Ben"), ae, MATCH_LARGEST_LTE)); + + confirmInt(13, invokeMatch(new StringEval("ED"), ae, MATCH_LARGEST_LTE)); + confirmInt(9, invokeMatch(new StringEval("ED"), ae, MATCH_EXACT)); + + confirmInt(13, invokeMatch(new StringEval("Hugh"), ae, MATCH_LARGEST_LTE)); + assertEquals(ErrorEval.NA, invokeMatch(new StringEval("Hugh"), ae, MATCH_EXACT)); + + confirmInt(11, invokeMatch(new NumberEval(30), ae, MATCH_LARGEST_LTE)); + confirmInt(12, invokeMatch(BoolEval.TRUE, ae, MATCH_LARGEST_LTE)); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRoundFuncs.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRoundFuncs.java new file mode 100755 index 0000000000..a6ce345aeb --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRoundFuncs.java @@ -0,0 +1,49 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +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.StringEval; + +import junit.framework.TestCase; + +/** + * Test cases for ROUND(), ROUNDUP(), ROUNDDOWN() + * + * @author Josh Micich + */ +public final class TestRoundFuncs extends TestCase { + public void testRounddownWithStringArg() { + + Eval strArg = new StringEval("abc"); + Eval[] args = { strArg, new NumberEval(2), }; + Eval result = new Rounddown().evaluate(args, -1, (short)-1); + assertEquals(ErrorEval.VALUE_INVALID, result); + } + + public void testRoundupWithStringArg() { + + Eval strArg = new StringEval("abc"); + Eval[] args = { strArg, new NumberEval(2), }; + Eval result = new Roundup().evaluate(args, -1, (short)-1); + assertEquals(ErrorEval.VALUE_INVALID, result); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestSumproduct.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestSumproduct.java new file mode 100755 index 0000000000..9fbc9e42fe --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestSumproduct.java @@ -0,0 +1,120 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.ReferencePtg; +import org.apache.poi.hssf.record.formula.eval.AreaEval; +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.Ref2DEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +import junit.framework.TestCase; + +/** + * Test cases for SUMPRODUCT() + * + * @author Josh Micich + */ +public final class TestSumproduct extends TestCase { + + private static Eval invokeSumproduct(Eval[] args) { + // srcCellRow and srcCellColumn are ignored by SUMPRODUCT + return new Sumproduct().evaluate(args, -1, (short)-1); + } + private static void confirmDouble(double expected, Eval actualEval) { + if(!(actualEval instanceof NumericValueEval)) { + fail("Expected numeric result"); + } + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + + public void testScalarSimple() { + + RefEval refEval = new Ref2DEval(new ReferencePtg("A1"), new NumberEval(3), true); + Eval[] args = { + refEval, + new NumberEval(2), + }; + Eval result = invokeSumproduct(args); + confirmDouble(6D, result); + } + + + public void testAreaSimple() { + + AreaEval aeA = EvalFactory.createAreaEval("A1:A3", 1, 3); + AreaEval aeB = EvalFactory.createAreaEval("B1:B3", 1, 3); + ValueEval[] aValues = aeA.getValues(); + ValueEval[] bValues = aeB.getValues(); + aValues[0] = new NumberEval(2); + aValues[1] = new NumberEval(4); + aValues[2] = new NumberEval(5); + bValues[0] = new NumberEval(3); + bValues[1] = new NumberEval(6); + bValues[2] = new NumberEval(7); + + Eval[] args = { aeA, aeB, }; + Eval result = invokeSumproduct(args); + confirmDouble(65D, result); + } + + /** + * For scalar products, the terms may be 1x1 area refs + */ + public void testOneByOneArea() { + + AreaEval ae = EvalFactory.createAreaEval("A1:A1", 1, 1); + ae.getValues()[0] = new NumberEval(7); + + Eval[] args = { + ae, + new NumberEval(2), + }; + Eval result = invokeSumproduct(args); + confirmDouble(14D, result); + } + + + public void testMismatchAreaDimensions() { + + AreaEval aeA = EvalFactory.createAreaEval("A1:A3", 1, 3); + AreaEval aeB = EvalFactory.createAreaEval("B1:D1", 3, 1); + + Eval[] args; + args = new Eval[] { aeA, aeB, }; + assertEquals(ErrorEval.VALUE_INVALID, invokeSumproduct(args)); + + args = new Eval[] { aeA, new NumberEval(5), }; + assertEquals(ErrorEval.VALUE_INVALID, invokeSumproduct(args)); + } + + public void testAreaWithErrorCell() { + AreaEval aeA = EvalFactory.createAreaEval("A1:A2", 1, 2); + AreaEval aeB = EvalFactory.createAreaEval("B1:B2", 1, 2); + aeB.getValues()[1] = ErrorEval.REF_INVALID; + + Eval[] args = { aeA, aeB, }; + assertEquals(ErrorEval.REF_INVALID, invokeSumproduct(args)); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTFunc.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTFunc.java new file mode 100755 index 0000000000..6ecc3619c4 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTFunc.java @@ -0,0 +1,118 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.ReferencePtg; +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.NumberEval; +import org.apache.poi.hssf.record.formula.eval.Ref2DEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +import junit.framework.TestCase; + +/** + * Test cases for Excel function T() + * + * @author Josh Micich + */ +public final class TestTFunc extends TestCase { + + /** + * @return the result of calling function T() with the specified argument + */ + private static Eval invokeT(Eval arg) { + Eval[] args = { arg, }; + Eval result = new T().evaluate(args, -1, (short)-1); + assertNotNull("result may never be null", result); + return result; + } + /** + * Simulates call: T(A1) + * where cell A1 has the specified innerValue + */ + private Eval invokeTWithReference(ValueEval innerValue) { + Eval arg = new Ref2DEval(new ReferencePtg((short)1, (short)1, false, false), innerValue, true); + return invokeT(arg); + } + + private static void confirmText(String text) { + Eval arg = new StringEval(text); + Eval eval = invokeT(arg); + StringEval se = (StringEval) eval; + assertEquals(text, se.getStringValue()); + } + + public void testTextValues() { + + confirmText("abc"); + confirmText(""); + confirmText(" "); + confirmText("~"); + confirmText("123"); + confirmText("TRUE"); + } + + private static void confirmError(Eval arg) { + Eval eval = invokeT(arg); + assertTrue(arg == eval); + } + + public void testErrorValues() { + + confirmError(ErrorEval.VALUE_INVALID); + confirmError(ErrorEval.NA); + confirmError(ErrorEval.REF_INVALID); + } + + private static void confirmString(Eval eval, String expected) { + assertTrue(eval instanceof StringEval); + assertEquals(expected, ((StringEval)eval).getStringValue()); + } + + private static void confirmOther(Eval arg) { + Eval eval = invokeT(arg); + confirmString(eval, ""); + } + + public void testOtherValues() { + confirmOther(new NumberEval(2)); + confirmOther(BoolEval.FALSE); + confirmOther(BlankEval.INSTANCE); // can this particular case be verified? + } + + public void testRefValues() { + Eval eval; + + eval = invokeTWithReference(new StringEval("def")); + confirmString(eval, "def"); + eval = invokeTWithReference(new StringEval(" ")); + confirmString(eval, " "); + + eval = invokeTWithReference(new NumberEval(2)); + confirmString(eval, ""); + eval = invokeTWithReference(BoolEval.TRUE); + confirmString(eval, ""); + + eval = invokeTWithReference(ErrorEval.NAME_INVALID); + assertTrue(eval == ErrorEval.NAME_INVALID); + } +} diff --git a/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls b/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls index cf4b6fa501..001432f0aa 100644 Binary files a/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls and b/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls b/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls index 001432f0aa..5241369212 100644 Binary files a/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls and b/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls differ diff --git a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java index 1b41d87d4d..e7400bc351 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java +++ b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java @@ -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.AttrPtg; 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 class TestFormulaParser extends TestCase { 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 class TestFormulaParser extends TestCase { 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 class TestFormulaParser extends TestCase { 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 class TestFormulaParser extends TestCase { 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 @@ public class TestFormulaParser extends TestCase { 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)); + } }