Patch from Josh from bug #44450 - VLookup and HLookup support, and improvements to Lookup and Offset

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@630160 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Nick Burch 2008-02-22 11:23:50 +00:00
parent 08a1a952b8
commit 7cdfaaa61d
14 changed files with 1495 additions and 354 deletions

View File

@ -36,6 +36,7 @@
<!-- Don't forget to update status.xml too! --> <!-- Don't forget to update status.xml too! -->
<release version="3.1-beta1" date="2008-??-??"> <release version="3.1-beta1" date="2008-??-??">
<action dev="POI-DEVELOPERS" type="add">44450 - Support for Lookup, HLookup and VLookup functions</action>
<action dev="POI-DEVELOPERS" type="fix">44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly</action> <action dev="POI-DEVELOPERS" type="fix">44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly</action>
<action dev="POI-DEVELOPERS" type="fix">44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied</action> <action dev="POI-DEVELOPERS" type="fix">44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied</action>
<action dev="POI-DEVELOPERS" type="add">44371 - Support for the Offset function</action> <action dev="POI-DEVELOPERS" type="add">44371 - Support for the Offset function</action>

View File

@ -33,6 +33,7 @@
<!-- Don't forget to update changes.xml too! --> <!-- Don't forget to update changes.xml too! -->
<changes> <changes>
<release version="3.1-beta1" date="2008-??-??"> <release version="3.1-beta1" date="2008-??-??">
<action dev="POI-DEVELOPERS" type="add">44450 - Support for Lookup, HLookup and VLookup functions</action>
<action dev="POI-DEVELOPERS" type="fix">44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly</action> <action dev="POI-DEVELOPERS" type="fix">44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly</action>
<action dev="POI-DEVELOPERS" type="fix">44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied</action> <action dev="POI-DEVELOPERS" type="fix">44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied</action>
<action dev="POI-DEVELOPERS" type="add">44371 - Support for the Offset function</action> <action dev="POI-DEVELOPERS" type="add">44371 - Support for the Offset function</action>

View File

@ -26,66 +26,67 @@ import org.apache.poi.hssf.usermodel.HSSFErrorConstants;
/** /**
* @author Daniel Noll (daniel at nuix dot com dot au) * @author Daniel Noll (daniel at nuix dot com dot au)
*/ */
public class ErrPtg extends Ptg public final class ErrPtg extends Ptg {
{
// convenient access to namespace
private static final HSSFErrorConstants EC = null;
/** <b>#NULL!</b> - Intersection of two cell ranges is empty */
public static final ErrPtg NULL_INTERSECTION = new ErrPtg(EC.ERROR_NULL);
/** <b>#DIV/0!</b> - Division by zero */
public static final ErrPtg DIV_ZERO = new ErrPtg(EC.ERROR_DIV_0);
/** <b>#VALUE!</b> - Wrong type of operand */
public static final ErrPtg VALUE_INVALID = new ErrPtg(EC.ERROR_VALUE);
/** <b>#REF!</b> - Illegal or deleted cell reference */
public static final ErrPtg REF_INVALID = new ErrPtg(EC.ERROR_REF);
/** <b>#NAME?</b> - Wrong function or range name */
public static final ErrPtg NAME_INVALID = new ErrPtg(EC.ERROR_NAME);
/** <b>#NUM!</b> - Value range overflow */
public static final ErrPtg NUM_ERROR = new ErrPtg(EC.ERROR_NUM);
/** <b>#N/A</b> - Argument or function not available */
public static final ErrPtg N_A = new ErrPtg(EC.ERROR_NA);
public static final short sid = 0x1c; public static final short sid = 0x1c;
private static final int SIZE = 2; private static final int SIZE = 2;
private byte field_1_error_code; private int field_1_error_code;
/** Creates new ErrPtg */ /** Creates new ErrPtg */
public ErrPtg() public ErrPtg(int errorCode) {
{ if(!HSSFErrorConstants.isValidCode(errorCode)) {
throw new IllegalArgumentException("Invalid error code (" + errorCode + ")");
}
field_1_error_code = errorCode;
} }
public ErrPtg(RecordInputStream in) public ErrPtg(RecordInputStream in) {
{ this(in.readByte());
field_1_error_code = in.readByte();
} }
public void writeBytes(byte [] array, int offset) public void writeBytes(byte [] array, int offset)
{ {
array[offset] = (byte) (sid + ptgClass); array[offset] = (byte) (sid + ptgClass);
array[offset + 1] = field_1_error_code; array[offset + 1] = (byte)field_1_error_code;
} }
public String toFormulaString(Workbook book) public String toFormulaString(Workbook book) {
{ return HSSFErrorConstants.getText(field_1_error_code);
switch(field_1_error_code)
{
case HSSFErrorConstants.ERROR_NULL:
return "#NULL!";
case HSSFErrorConstants.ERROR_DIV_0:
return "#DIV/0!";
case HSSFErrorConstants.ERROR_VALUE:
return "#VALUE!";
case HSSFErrorConstants.ERROR_REF:
return "#REF!";
case HSSFErrorConstants.ERROR_NAME:
return "#NAME?";
case HSSFErrorConstants.ERROR_NUM:
return "#NUM!";
case HSSFErrorConstants.ERROR_NA:
return "#N/A";
} }
// Shouldn't happen anyway. Excel docs say that this is returned for all other codes. public int getSize() {
return "#N/A";
}
public int getSize()
{
return SIZE; return SIZE;
} }
public byte getDefaultOperandClass() public byte getDefaultOperandClass() {
{
return Ptg.CLASS_VALUE; return Ptg.CLASS_VALUE;
} }
public Object clone() { public Object clone() {
ErrPtg ptg = new ErrPtg(); return new ErrPtg(field_1_error_code);
ptg.field_1_error_code = field_1_error_code; }
return ptg;
public int getErrorCode() {
return field_1_error_code;
} }
} }

View File

@ -15,26 +15,68 @@
limitations under the License. limitations under the License.
==================================================================== */ ==================================================================== */
/*
* HSSFErrorConstants.java
*
* Created on January 19, 2002, 9:30 AM
*/
package org.apache.poi.hssf.usermodel; package org.apache.poi.hssf.usermodel;
/** /**
* contains constants representing Excel error codes. * Contains raw Excel error codes (as defined in OOO's excelfileformat.pdf (2.5.6)
*
* @author Michael Harhen * @author Michael Harhen
*/ */
public final class HSSFErrorConstants {
private HSSFErrorConstants() {
// no instances of this class
}
public interface HSSFErrorConstants /** <b>#NULL!</b> - Intersection of two cell ranges is empty */
{ public static final int ERROR_NULL = 0x00;
public static final byte ERROR_NULL = 0x00; // #NULL! /** <b>#DIV/0!</b> - Division by zero */
public static final byte ERROR_DIV_0 = 0x07; // #DIV/0! public static final int ERROR_DIV_0 = 0x07;
public static final byte ERROR_VALUE = 0x0f; // #VALUE! /** <b>#VALUE!</b> - Wrong type of operand */
public static final byte ERROR_REF = 0x17; // #REF! public static final int ERROR_VALUE = 0x0F;
public static final byte ERROR_NAME = 0x1d; // #NAME? /** <b>#REF!</b> - Illegal or deleted cell reference */
public static final byte ERROR_NUM = 0x24; // #NUM! public static final int ERROR_REF = 0x17;
public static final byte ERROR_NA = 0x2a; // #N/A /** <b>#NAME?</b> - Wrong function or range name */
public static final int ERROR_NAME = 0x1D;
/** <b>#NUM!</b> - Value range overflow */
public static final int ERROR_NUM = 0x24;
/** <b>#N/A</b> - Argument or function not available */
public static final int ERROR_NA = 0x2A;
/**
* @return Standard Excel error literal for the specified error code.
* @throws IllegalArgumentException if the specified error code is not one of the 7
* standard error codes
*/
public static final String getText(int errorCode) {
switch(errorCode) {
case ERROR_NULL: return "#NULL!";
case ERROR_DIV_0: return "#DIV/0!";
case ERROR_VALUE: return "#VALUE!";
case ERROR_REF: return "#REF!";
case ERROR_NAME: return "#NAME?";
case ERROR_NUM: return "#NUM!";
case ERROR_NA: return "#N/A";
}
throw new IllegalArgumentException("Bad error code (" + errorCode + ")");
}
/**
* @return <code>true</code> if the specified error code is a standard Excel error code.
*/
public static final boolean isValidCode(int errorCode) {
// This method exists because it would be bad to force clients to catch
// IllegalArgumentException if there were potential for passing an invalid error code.
switch(errorCode) {
case ERROR_NULL:
case ERROR_DIV_0:
case ERROR_VALUE:
case ERROR_REF:
case ERROR_NAME:
case ERROR_NUM:
case ERROR_NA:
return true;
}
return false;
}
} }

View File

@ -14,64 +14,49 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/*
* Created on May 8, 2005
*
*/
package org.apache.poi.hssf.record.formula.eval; package org.apache.poi.hssf.record.formula.eval;
import org.apache.poi.hssf.usermodel.HSSFErrorConstants;
/** /**
* @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt; * @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
* *
*/ */
public final class ErrorEval implements ValueEval { public final class ErrorEval implements ValueEval {
// convenient access to namespace
private static final HSSFErrorConstants EC = null;
/** <b>#NULL!</b> - Intersection of two cell ranges is empty */
public static final ErrorEval NULL_INTERSECTION = new ErrorEval(EC.ERROR_NULL);
/** <b>#DIV/0!</b> - Division by zero */
public static final ErrorEval DIV_ZERO = new ErrorEval(EC.ERROR_DIV_0);
/** <b>#VALUE!</b> - Wrong type of operand */
public static final ErrorEval VALUE_INVALID = new ErrorEval(EC.ERROR_VALUE);
/** <b>#REF!</b> - Illegal or deleted cell reference */
public static final ErrorEval REF_INVALID = new ErrorEval(EC.ERROR_REF);
/** <b>#NAME?</b> - Wrong function or range name */
public static final ErrorEval NAME_INVALID = new ErrorEval(EC.ERROR_NAME);
/** <b>#NUM!</b> - Value range overflow */
public static final ErrorEval NUM_ERROR = new ErrorEval(EC.ERROR_NUM);
/** <b>#N/A</b> - Argument or function not available */
public static final ErrorEval NA = new ErrorEval(EC.ERROR_NA);
// POI internal error codes
private static final int CIRCULAR_REF_ERROR_CODE = 0xFFFFFFC4;
private static final int FUNCTION_NOT_IMPLEMENTED_CODE = 0xFFFFFFE2;
/** /**
* Contains raw Excel error codes (as defined in OOO's excelfileformat.pdf (2.5.6) * @deprecated do not use this error code. For conditions that should never occur, throw an
* unchecked exception. For all other situations use the error code that corresponds to the
* error Excel would have raised under the same circumstances.
*/ */
private static final class ErrorCode { public static final ErrorEval UNKNOWN_ERROR = new ErrorEval(-20);
/** <b>#NULL!</b> - Intersection of two cell ranges is empty */ public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(FUNCTION_NOT_IMPLEMENTED_CODE);
public static final int NULL = 0x00; // Note - Excel does not seem to represent this condition with an error code
/** <b>#DIV/0!</b> - Division by zero */ public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(CIRCULAR_REF_ERROR_CODE);
public static final int DIV_0 = 0x07;
/** <b>#VALUE!</b> - Wrong type of operand */
public static final int VALUE = 0x0F;
/** <b>#REF!</b> - Illegal or deleted cell reference */
public static final int REF = 0x17;
/** <b>#NAME?</b> - Wrong function or range name */
public static final int NAME = 0x1D;
/** <b>#NUM!</b> - Value range overflow */
public static final int NUM = 0x24;
/** <b>#N/A</b> - 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 "???";
}
}
/** <b>#NULL!</b> - Intersection of two cell ranges is empty */
public static final ErrorEval NULL_INTERSECTION = new ErrorEval(ErrorCode.NULL);
/** <b>#DIV/0!</b> - Division by zero */
public static final ErrorEval DIV_ZERO = new ErrorEval(ErrorCode.DIV_0);
/** <b>#VALUE!</b> - Wrong type of operand */
public static final ErrorEval VALUE_INVALID = new ErrorEval(ErrorCode.VALUE);
/** <b>#REF!</b> - Illegal or deleted cell reference */
public static final ErrorEval REF_INVALID = new ErrorEval(ErrorCode.REF);
/** <b>#NAME?</b> - Wrong function or range name */
public static final ErrorEval NAME_INVALID = new ErrorEval(ErrorCode.NAME);
/** <b>#NUM!</b> - Value range overflow */
public static final ErrorEval NUM_ERROR = new ErrorEval(ErrorCode.NUM);
/** <b>#N/A</b> - Argument or function not available */
public static final ErrorEval NA = new ErrorEval(ErrorCode.N_A);
/** /**
@ -80,46 +65,54 @@ public final class ErrorEval implements ValueEval {
*/ */
public static ErrorEval valueOf(int errorCode) { public static ErrorEval valueOf(int errorCode) {
switch(errorCode) { switch(errorCode) {
case ErrorCode.NULL: return NULL_INTERSECTION; case HSSFErrorConstants.ERROR_NULL: return NULL_INTERSECTION;
case ErrorCode.DIV_0: return DIV_ZERO; case HSSFErrorConstants.ERROR_DIV_0: return DIV_ZERO;
case ErrorCode.VALUE: return VALUE_INVALID; case HSSFErrorConstants.ERROR_VALUE: return VALUE_INVALID;
// case ErrorCode.REF: return REF_INVALID; case HSSFErrorConstants.ERROR_REF: return REF_INVALID;
case ErrorCode.REF: return UNKNOWN_ERROR; case HSSFErrorConstants.ERROR_NAME: return NAME_INVALID;
case ErrorCode.NAME: return NAME_INVALID; case HSSFErrorConstants.ERROR_NUM: return NUM_ERROR;
case ErrorCode.NUM: return NUM_ERROR; case HSSFErrorConstants.ERROR_NA: return NA;
case ErrorCode.N_A: return NA; // non-std errors (conditions modeled as errors by POI)
case CIRCULAR_REF_ERROR_CODE: return CIRCULAR_REF_ERROR;
// these cases probably shouldn't be coming through here case FUNCTION_NOT_IMPLEMENTED_CODE: return FUNCTION_NOT_IMPLEMENTED;
// 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 + ")"); throw new RuntimeException("Unexpected error code (" + errorCode + ")");
} }
// POI internal error codes /**
public static final ErrorEval UNKNOWN_ERROR = new ErrorEval(-20); * Converts error codes to text. Handles non-standard error codes OK.
public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(-30); * For debug/test purposes (and for formatting error messages).
// Note - Excel does not seem to represent this condition with an error code * @return the String representation of the specified Excel error code.
public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(-60); */
public static String getText(int errorCode) {
if(HSSFErrorConstants.isValidCode(errorCode)) {
return HSSFErrorConstants.getText(errorCode);
}
// It is desirable to make these (arbitrary) strings look clearly different from any other
// value expression that might appear in a formula. In addition these error strings should
// look unlike the standard Excel errors. Hence tilde ('~') was used.
switch(errorCode) {
case CIRCULAR_REF_ERROR_CODE: return "~CIRCULAR~REF~";
case FUNCTION_NOT_IMPLEMENTED_CODE: return "~FUNCTION~NOT~IMPLEMENTED~";
}
return "~non~std~err(" + errorCode + ")~";
}
private int _errorCode;
private int errorCode;
/** /**
* @param errorCode an 8-bit value * @param errorCode an 8-bit value
*/ */
private ErrorEval(int errorCode) { private ErrorEval(int errorCode) {
this.errorCode = errorCode; _errorCode = errorCode;
} }
public int getErrorCode() { public int getErrorCode() {
return errorCode; return _errorCode;
} }
public String toString() { public String toString() {
StringBuffer sb = new StringBuffer(64); StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" ["); sb.append(getClass().getName()).append(" [");
sb.append(ErrorCode.getText(errorCode)); sb.append(getText(_errorCode));
sb.append("]"); sb.append("]");
return sb.toString(); return sb.toString();
} }

View File

@ -14,10 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/*
* Created on May 8, 2005
*
*/
package org.apache.poi.hssf.record.formula.eval; package org.apache.poi.hssf.record.formula.eval;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
@ -27,21 +24,31 @@ import org.apache.poi.hssf.record.formula.StringPtg;
* @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt; * @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
* *
*/ */
public class StringEval implements StringValueEval { public final class StringEval implements StringValueEval {
public static final StringEval EMPTY_INSTANCE = new StringEval(""); public static final StringEval EMPTY_INSTANCE = new StringEval("");
private String value; private final String value;
public StringEval(Ptg ptg) { public StringEval(Ptg ptg) {
this.value = ((StringPtg) ptg).getValue(); this(((StringPtg) ptg).getValue());
} }
public StringEval(String value) { public StringEval(String value) {
if(value == null) {
throw new IllegalArgumentException("value must not be null");
}
this.value = value; this.value = value;
} }
public String getStringValue() { public String getStringValue() {
return value; return value;
} }
public String toString() {
StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" [");
sb.append(value);
sb.append("]");
return sb.toString();
}
} }

View File

@ -14,10 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/*
* Created on May 8, 2005
*
*/
package org.apache.poi.hssf.record.formula.eval; package org.apache.poi.hssf.record.formula.eval;
/** /**
@ -26,5 +23,8 @@ package org.apache.poi.hssf.record.formula.eval;
*/ */
public interface StringValueEval extends ValueEval { public interface StringValueEval extends ValueEval {
public String getStringValue(); /**
* @return never <code>null</code>, possibly empty string.
*/
String getStringValue();
} }

View File

@ -1,25 +1,123 @@
/* /* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one or more Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0 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 not use this file except in compliance with
* the License. You may obtain a copy of the License at the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and See the License for the specific language governing permissions and
* limitations under the License. limitations under the License.
*/ ==================================================================== */
/*
* Created on May 15, 2005
*
*/
package org.apache.poi.hssf.record.formula.functions; package org.apache.poi.hssf.record.formula.functions;
public class Hlookup extends NotImplementedFunction { 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.EvaluationException;
import org.apache.poi.hssf.record.formula.eval.OperandResolver;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.record.formula.functions.LookupUtils.ValueVector;
/**
* Implementation of the VLOOKUP() function.<p/>
*
* HLOOKUP finds a column in a lookup table by the first row value and returns the value from another row.
*
* <b>Syntax</b>:<br/>
* <b>HLOOKUP</b>(<b>lookup_value</b>, <b>table_array</b>, <b>row_index_num</b>, range_lookup)<p/>
*
* <b>lookup_value</b> The value to be found in the first column of the table array.<br/>
* <b>table_array</> An area reference for the lookup data. <br/>
* <b>row_index_num</b> a 1 based index specifying which row value of the lookup data will be returned.<br/>
* <b>range_lookup</b> If TRUE (default), HLOOKUP finds the largest value less than or equal to
* the lookup_value. If FALSE, only exact matches will be considered<br/>
*
* @author Josh Micich
*/
public final class Hlookup implements Function {
private static final class RowVector implements ValueVector {
private final AreaEval _tableArray;
private final int _size;
private final int _rowAbsoluteIndex;
private final int _firstColumnAbsoluteIndex;
public RowVector(AreaEval tableArray, int rowIndex) {
_rowAbsoluteIndex = tableArray.getFirstRow() + rowIndex;
if(!tableArray.containsRow(_rowAbsoluteIndex)) {
int lastRowIx = tableArray.getLastRow() - tableArray.getFirstRow();
throw new IllegalArgumentException("Specified row index (" + rowIndex
+ ") is outside the allowed range (0.." + lastRowIx + ")");
}
_tableArray = tableArray;
_size = tableArray.getLastColumn() - tableArray.getFirstColumn() + 1;
if(_size < 1) {
throw new RuntimeException("bad table array size zero");
}
_firstColumnAbsoluteIndex = tableArray.getFirstColumn();
}
public ValueEval getItem(int index) {
if(index>_size) {
throw new ArrayIndexOutOfBoundsException("Specified index (" + index
+ ") is outside the allowed range (0.." + (_size-1) + ")");
}
return _tableArray.getValueAt(_rowAbsoluteIndex, (short) (_firstColumnAbsoluteIndex + index));
}
public int getSize() {
return _size;
}
}
public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) {
Eval arg3 = null;
switch(args.length) {
case 4:
arg3 = args[3]; // important: assumed array element is never null
case 3:
break;
default:
// wrong number of arguments
return ErrorEval.VALUE_INVALID;
}
try {
// Evaluation order:
// arg0 lookup_value, arg1 table_array, arg3 range_lookup, find lookup value, arg2 row_index, fetch result
ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol);
AreaEval tableArray = LookupUtils.resolveTableArrayArg(args[1]);
boolean isRangeLookup = LookupUtils.resolveRangeLookupArg(arg3, srcCellRow, srcCellCol);
int colIndex = LookupUtils.lookupIndexOfValue(lookupValue, new RowVector(tableArray, 0), isRangeLookup);
ValueEval veColIndex = OperandResolver.getSingleValue(args[2], srcCellRow, srcCellCol);
int rowIndex = LookupUtils.resolveRowOrColIndexArg(veColIndex);
ValueVector resultCol = createResultColumnVector(tableArray, rowIndex);
return resultCol.getItem(colIndex);
} catch (EvaluationException e) {
return e.getErrorEval();
}
}
/**
* Returns one column from an <tt>AreaEval</tt>
*
* @throws EvaluationException (#VALUE!) if colIndex is negative, (#REF!) if colIndex is too high
*/
private ValueVector createResultColumnVector(AreaEval tableArray, int colIndex) throws EvaluationException {
if(colIndex < 0) {
throw EvaluationException.invalidValue();
}
int nCols = tableArray.getLastColumn() - tableArray.getFirstRow() + 1;
if(colIndex >= nCols) {
throw EvaluationException.invalidRef();
}
return new RowVector(tableArray, colIndex);
}
} }

View File

@ -1,25 +1,96 @@
/* /* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one or more Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0 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 not use this file except in compliance with
* the License. You may obtain a copy of the License at the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and See the License for the specific language governing permissions and
* limitations under the License. limitations under the License.
*/ ==================================================================== */
/*
* Created on May 15, 2005
*
*/
package org.apache.poi.hssf.record.formula.functions; package org.apache.poi.hssf.record.formula.functions;
public class Lookup extends NotImplementedFunction { 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.EvaluationException;
import org.apache.poi.hssf.record.formula.eval.OperandResolver;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.record.formula.functions.LookupUtils.ValueVector;
/**
* Implementation of Excel function LOOKUP.<p/>
*
* LOOKUP finds an index row in a lookup table by the first column value and returns the value from another column.
*
* <b>Syntax</b>:<br/>
* <b>VLOOKUP</b>(<b>lookup_value</b>, <b>lookup_vector</b>, result_vector)<p/>
*
* <b>lookup_value</b> The value to be found in the lookup vector.<br/>
* <b>lookup_vector</> An area reference for the lookup data. <br/>
* <b>result_vector</b> Single row or single column area reference from which the result value is chosen.<br/>
*
* @author Josh Micich
*/
public final class Lookup implements Function {
private static final class SimpleValueVector implements ValueVector {
private final ValueEval[] _values;
public SimpleValueVector(ValueEval[] values) {
_values = values;
}
public ValueEval getItem(int index) {
return _values[index];
}
public int getSize() {
return _values.length;
}
}
public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) {
switch(args.length) {
case 3:
break;
case 2:
// complex rules to choose lookupVector and resultVector from the single area ref
throw new RuntimeException("Two arg version of LOOKUP not supported yet");
default:
return ErrorEval.VALUE_INVALID;
}
try {
ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol);
AreaEval aeLookupVector = LookupUtils.resolveTableArrayArg(args[1]);
AreaEval aeResultVector = LookupUtils.resolveTableArrayArg(args[2]);
ValueVector lookupVector = createVector(aeLookupVector);
ValueVector resultVector = createVector(aeResultVector);
if(lookupVector.getSize() > resultVector.getSize()) {
// Excel seems to handle this by accessing past the end of the result vector.
throw new RuntimeException("Lookup vector and result vector of differing sizes not supported yet");
}
int index = LookupUtils.lookupIndexOfValue(lookupValue, lookupVector, true);
return resultVector.getItem(index);
} catch (EvaluationException e) {
return e.getErrorEval();
}
}
private static ValueVector createVector(AreaEval ae) {
if(!ae.isRow() && !ae.isColumn()) {
// extra complexity required to emulate the way LOOKUP can handles these abnormal cases.
throw new RuntimeException("non-vector lookup or result areas not supported yet");
}
return new SimpleValueVector(ae.getValues());
}
} }

View File

@ -0,0 +1,530 @@
/* ====================================================================
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.AreaPtg;
import org.apache.poi.hssf.record.formula.eval.Area2DEval;
import org.apache.poi.hssf.record.formula.eval.AreaEval;
import org.apache.poi.hssf.record.formula.eval.BlankEval;
import org.apache.poi.hssf.record.formula.eval.BoolEval;
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
import org.apache.poi.hssf.record.formula.eval.Eval;
import org.apache.poi.hssf.record.formula.eval.EvaluationException;
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.OperandResolver;
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;
/**
* Common functionality used by VLOOKUP, HLOOKUP, LOOKUP and MATCH
*
* @author Josh Micich
*/
final class LookupUtils {
/**
* Represents a single row or column within an <tt>AreaEval</tt>.
*/
public interface ValueVector {
ValueEval getItem(int index);
int getSize();
}
/**
* Enumeration to support <b>4</b> valued comparison results.<p/>
* Excel lookup functions have complex behaviour in the case where the lookup array has mixed
* types, and/or is unordered. Contrary to suggestions in some Excel documentation, there
* does not appear to be a universal ordering across types. The binary search algorithm used
* changes behaviour when the evaluated 'mid' value has a different type to the lookup value.<p/>
*
* A simple int might have done the same job, but there is risk in confusion with the well
* known <tt>Comparable.compareTo()</tt> and <tt>Comparator.compare()</tt> which both use
* a ubiquitous 3 value result encoding.
*/
public static final class CompareResult {
private final boolean _isTypeMismatch;
private final boolean _isLessThan;
private final boolean _isEqual;
private final boolean _isGreaterThan;
private CompareResult(boolean isTypeMismatch, int simpleCompareResult) {
if(isTypeMismatch) {
_isTypeMismatch = true;
_isLessThan = false;
_isEqual = false;
_isGreaterThan = false;
} else {
_isTypeMismatch = false;
_isLessThan = simpleCompareResult < 0;
_isEqual = simpleCompareResult == 0;
_isGreaterThan = simpleCompareResult > 0;
}
}
public static final CompareResult TYPE_MISMATCH = new CompareResult(true, 0);
public static final CompareResult LESS_THAN = new CompareResult(false, -1);
public static final CompareResult EQUAL = new CompareResult(false, 0);
public static final CompareResult GREATER_THAN = new CompareResult(false, +1);
public static final CompareResult valueOf(int simpleCompareResult) {
if(simpleCompareResult < 0) {
return LESS_THAN;
}
if(simpleCompareResult > 0) {
return GREATER_THAN;
}
return EQUAL;
}
public boolean isTypeMismatch() {
return _isTypeMismatch;
}
public boolean isLessThan() {
return _isLessThan;
}
public boolean isEqual() {
return _isEqual;
}
public boolean isGreaterThan() {
return _isGreaterThan;
}
public String toString() {
StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" [");
sb.append(formatAsString());
sb.append("]");
return sb.toString();
}
private String formatAsString() {
if(_isTypeMismatch) {
return "TYPE_MISMATCH";
}
if(_isLessThan) {
return "LESS_THAN";
}
if(_isEqual) {
return "EQUAL";
}
if(_isGreaterThan) {
return "GREATER_THAN";
}
// toString must be reliable
return "??error??";
}
}
public interface LookupValueComparer {
/**
* @return one of 4 instances or <tt>CompareResult</tt>: <tt>LESS_THAN</tt>, <tt>EQUAL</tt>,
* <tt>GREATER_THAN</tt> or <tt>TYPE_MISMATCH</tt>
*/
CompareResult compareTo(ValueEval other);
}
private static abstract class LookupValueComparerBase implements LookupValueComparer {
private final Class _targetClass;
protected LookupValueComparerBase(ValueEval targetValue) {
if(targetValue == null) {
throw new RuntimeException("targetValue cannot be null");
}
_targetClass = targetValue.getClass();
}
public final CompareResult compareTo(ValueEval other) {
if (other == null) {
throw new RuntimeException("compare to value cannot be null");
}
if (_targetClass != other.getClass()) {
return CompareResult.TYPE_MISMATCH;
}
if (_targetClass == StringEval.class) {
}
return compareSameType(other);
}
public String toString() {
StringBuffer sb = new StringBuffer(64);
sb.append(getClass().getName()).append(" [");
sb.append(getValueAsString());
sb.append("]");
return sb.toString();
}
protected abstract CompareResult compareSameType(ValueEval other);
/** used only for debug purposes */
protected abstract String getValueAsString();
}
private static final class StringLookupComparer extends LookupValueComparerBase {
private String _value;
protected StringLookupComparer(StringEval se) {
super(se);
_value = se.getStringValue();
}
protected CompareResult compareSameType(ValueEval other) {
StringEval se = (StringEval) other;
return CompareResult.valueOf(_value.compareToIgnoreCase(se.getStringValue()));
}
protected String getValueAsString() {
return _value;
}
}
private static final class NumberLookupComparer extends LookupValueComparerBase {
private double _value;
protected NumberLookupComparer(NumberEval ne) {
super(ne);
_value = ne.getNumberValue();
}
protected CompareResult compareSameType(ValueEval other) {
NumberEval ne = (NumberEval) other;
return CompareResult.valueOf(Double.compare(_value, ne.getNumberValue()));
}
protected String getValueAsString() {
return String.valueOf(_value);
}
}
private static final class BooleanLookupComparer extends LookupValueComparerBase {
private boolean _value;
protected BooleanLookupComparer(BoolEval be) {
super(be);
_value = be.getBooleanValue();
}
protected CompareResult compareSameType(ValueEval other) {
BoolEval be = (BoolEval) other;
boolean otherVal = be.getBooleanValue();
if(_value == otherVal) {
return CompareResult.EQUAL;
}
// TRUE > FALSE
if(_value) {
return CompareResult.GREATER_THAN;
}
return CompareResult.LESS_THAN;
}
protected String getValueAsString() {
return String.valueOf(_value);
}
}
/**
* Processes the third argument to VLOOKUP, or HLOOKUP (<b>col_index_num</b>
* or <b>row_index_num</b> respectively).<br>
* Sample behaviour:
* <table border="0" cellpadding="1" cellspacing="2" summary="Sample behaviour">
* <tr><th>Input&nbsp;&nbsp;&nbsp;Return</th><th>Value&nbsp;&nbsp;</th><th>Thrown Error</th></tr>
* <tr><td>5</td><td>4</td><td>&nbsp;</td></tr>
* <tr><td>2.9</td><td>2</td><td>&nbsp;</td></tr>
* <tr><td>"5"</td><td>4</td><td>&nbsp;</td></tr>
* <tr><td>"2.18e1"</td><td>21</td><td>&nbsp;</td></tr>
* <tr><td>"-$2"</td><td>-3</td><td>*</td></tr>
* <tr><td>FALSE</td><td>-1</td><td>*</td></tr>
* <tr><td>TRUE</td><td>0</td><td>&nbsp;</td></tr>
* <tr><td>"TRUE"</td><td>&nbsp;</td><td>#REF!</td></tr>
* <tr><td>"abc"</td><td>&nbsp;</td><td>#REF!</td></tr>
* <tr><td>""</td><td>&nbsp;</td><td>#REF!</td></tr>
* <tr><td>&lt;blank&gt;</td><td>&nbsp;</td><td>#VALUE!</td></tr>
* </table><br/>
*
* * Note - out of range errors (both too high and too low) are handled by the caller.
* @return column or row index as a zero-based value
*
*/
public static int resolveRowOrColIndexArg(ValueEval veRowColIndexArg) throws EvaluationException {
if(veRowColIndexArg == null) {
throw new IllegalArgumentException("argument must not be null");
}
if(veRowColIndexArg instanceof BlankEval) {
throw EvaluationException.invalidValue();
}
if(veRowColIndexArg instanceof StringEval) {
StringEval se = (StringEval) veRowColIndexArg;
String strVal = se.getStringValue();
Double dVal = OperandResolver.parseDouble(strVal);
if(dVal == null) {
// String does not resolve to a number. Raise #VALUE! error.
throw EvaluationException.invalidRef();
// This includes text booleans "TRUE" and "FALSE". They are not valid.
}
// else - numeric value parses OK
}
// actual BoolEval values get interpreted as FALSE->0 and TRUE->1
return OperandResolver.coerceValueToInt(veRowColIndexArg) - 1;
}
/**
* The second argument (table_array) should be an area ref, but can actually be a cell ref, in
* which case it is interpreted as a 1x1 area ref. Other scalar values cause #VALUE! error.
*/
public static AreaEval resolveTableArrayArg(Eval eval) throws EvaluationException {
if (eval instanceof AreaEval) {
return (AreaEval) eval;
}
if(eval instanceof RefEval) {
RefEval refEval = (RefEval) eval;
// Make this cell ref look like a 1x1 area ref.
// It doesn't matter if eval is a 2D or 3D ref, because that detail is never asked of AreaEval.
// This code only requires the value array item.
// anything would be ok for rowIx and colIx, but may as well get it right.
short rowIx = refEval.getRow();
short colIx = refEval.getColumn();
AreaPtg ap = new AreaPtg(rowIx, rowIx, colIx, colIx, false, false, false, false);
ValueEval value = refEval.getInnerValueEval();
return new Area2DEval(ap, new ValueEval[] { value, });
}
throw EvaluationException.invalidValue();
}
/**
* Resolves the last (optional) parameter (<b>range_lookup</b>) to the VLOOKUP and HLOOKUP functions.
* @param rangeLookupArg
* @param srcCellRow
* @param srcCellCol
* @return
* @throws EvaluationException
*/
public static boolean resolveRangeLookupArg(Eval rangeLookupArg, int srcCellRow, short srcCellCol) throws EvaluationException {
if(rangeLookupArg == null) {
// range_lookup arg not provided
return true; // default is TRUE
}
ValueEval valEval = OperandResolver.getSingleValue(rangeLookupArg, srcCellRow, srcCellCol);
if(valEval instanceof BlankEval) {
// Tricky:
// fourth arg supplied but evaluates to blank
// this does not get the default value
return false;
}
if(valEval instanceof BoolEval) {
// Happy day flow
BoolEval boolEval = (BoolEval) valEval;
return boolEval.getBooleanValue();
}
if (valEval instanceof StringEval) {
String stringValue = ((StringEval) valEval).getStringValue();
if(stringValue.length() < 1) {
// More trickiness:
// Empty string is not the same as BlankEval. It causes #VALUE! error
throw EvaluationException.invalidValue();
}
// TODO move parseBoolean to OperandResolver
Boolean b = Countif.parseBoolean(stringValue);
if(b != null) {
// string converted to boolean OK
return b.booleanValue();
}
// Even more trickiness:
// Note - even if the StringEval represents a number value (for example "1"),
// Excel does not resolve it to a boolean.
throw EvaluationException.invalidValue();
// This is in contrast to the code below,, where NumberEvals values (for
// example 0.01) *do* resolve to equivalent boolean values.
}
if (valEval instanceof NumericValueEval) {
NumericValueEval nve = (NumericValueEval) valEval;
// zero is FALSE, everything else is TRUE
return 0.0 != nve.getNumberValue();
}
throw new RuntimeException("Unexpected eval type (" + valEval.getClass().getName() + ")");
}
public static int lookupIndexOfValue(ValueEval lookupValue, ValueVector vector, boolean isRangeLookup) throws EvaluationException {
LookupValueComparer lookupComparer = createLookupComparer(lookupValue);
int result;
if(isRangeLookup) {
result = performBinarySearch(vector, lookupComparer);
} else {
result = lookupIndexOfExactValue(lookupComparer, vector);
}
if(result < 0) {
throw new EvaluationException(ErrorEval.NA);
}
return result;
}
/**
* Finds first (lowest index) exact occurrence of specified value.
* @param lookupValue the value to be found in column or row vector
* @param vector the values to be searched. For VLOOKUP this is the first column of the
* tableArray. For HLOOKUP this is the first row of the tableArray.
* @return zero based index into the vector, -1 if value cannot be found
*/
private static int lookupIndexOfExactValue(LookupValueComparer lookupComparer, ValueVector vector) {
// find first occurrence of lookup value
int size = vector.getSize();
for (int i = 0; i < size; i++) {
if(lookupComparer.compareTo(vector.getItem(i)).isEqual()) {
return i;
}
}
return -1;
}
/**
* Encapsulates some standard binary search functionality so the unusual Excel behaviour can
* be clearly distinguished.
*/
private static final class BinarySearchIndexes {
private int _lowIx;
private int _highIx;
public BinarySearchIndexes(int highIx) {
_lowIx = -1;
_highIx = highIx;
}
/**
* @return -1 if the search range is empty
*/
public int getMidIx() {
int ixDiff = _highIx - _lowIx;
if(ixDiff < 2) {
return -1;
}
return _lowIx + (ixDiff / 2);
}
public int getLowIx() {
return _lowIx;
}
public int getHighIx() {
return _highIx;
}
public void narrowSearch(int midIx, boolean isLessThan) {
if(isLessThan) {
_highIx = midIx;
} else {
_lowIx = midIx;
}
}
}
/**
* Excel has funny behaviour when the some elements in the search vector are the wrong type.
*
*/
private static int performBinarySearch(ValueVector vector, LookupValueComparer lookupComparer) {
// both low and high indexes point to values assumed too low and too high.
BinarySearchIndexes bsi = new BinarySearchIndexes(vector.getSize());
while(true) {
int midIx = bsi.getMidIx();
if(midIx < 0) {
return bsi.getLowIx();
}
CompareResult cr = lookupComparer.compareTo(vector.getItem(midIx));
if(cr.isTypeMismatch()) {
int newMidIx = handleMidValueTypeMismatch(lookupComparer, vector, bsi, midIx);
if(newMidIx < 0) {
continue;
}
midIx = newMidIx;
cr = lookupComparer.compareTo(vector.getItem(midIx));
}
if(cr.isEqual()) {
return findLastIndexInRunOfEqualValues(lookupComparer, vector, midIx, bsi.getHighIx());
}
bsi.narrowSearch(midIx, cr.isLessThan());
}
}
/**
* Excel seems to handle mismatched types initially by just stepping 'mid' ix forward to the
* first compatible value.
* @param midIx 'mid' index (value which has the wrong type)
* @return usually -1, signifying that the BinarySearchIndex has been narrowed to the new mid
* index. Zero or greater signifies that an exact match for the lookup value was found
*/
private static int handleMidValueTypeMismatch(LookupValueComparer lookupComparer, ValueVector vector,
BinarySearchIndexes bsi, int midIx) {
int newMid = midIx;
int highIx = bsi.getHighIx();
while(true) {
newMid++;
if(newMid == highIx) {
// every element from midIx to highIx was the wrong type
// move highIx down to the low end of the mid values
bsi.narrowSearch(midIx, true);
return -1;
}
CompareResult cr = lookupComparer.compareTo(vector.getItem(newMid));
if(cr.isLessThan() && newMid == highIx-1) {
// move highIx down to the low end of the mid values
bsi.narrowSearch(midIx, true);
return -1;
// but only when "newMid == highIx-1"? slightly weird.
// It would seem more efficient to always do this.
}
if(cr.isTypeMismatch()) {
// keep stepping over values until the right type is found
continue;
}
if(cr.isEqual()) {
return newMid;
}
// Note - if moving highIx down (due to lookup<vector[newMid]),
// this execution path only moves highIx it down as far as newMid, not midIx,
// which would be more efficient.
bsi.narrowSearch(newMid, cr.isLessThan());
return -1;
}
}
/**
* Once the binary search has found a single match, (V/H)LOOKUP steps one by one over subsequent
* values to choose the last matching item.
*/
private static int findLastIndexInRunOfEqualValues(LookupValueComparer lookupComparer, ValueVector vector,
int firstFoundIndex, int maxIx) {
for(int i=firstFoundIndex+1; i<maxIx; i++) {
if(!lookupComparer.compareTo(vector.getItem(i)).isEqual()) {
return i-1;
}
}
return maxIx - 1;
}
public static LookupValueComparer createLookupComparer(ValueEval lookupValue) throws EvaluationException {
if (lookupValue instanceof BlankEval) {
// blank eval can never be found in a lookup array
throw new EvaluationException(ErrorEval.NA);
}
if (lookupValue instanceof StringEval) {
return new StringLookupComparer((StringEval) lookupValue);
}
if (lookupValue instanceof NumberEval) {
return new NumberLookupComparer((NumberEval) lookupValue);
}
if (lookupValue instanceof BoolEval) {
return new BooleanLookupComparer((BoolEval) lookupValue);
}
throw new IllegalArgumentException("Bad lookup value type (" + lookupValue.getClass().getName() + ")");
}
}

View File

@ -18,14 +18,17 @@
package org.apache.poi.hssf.record.formula.functions; 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.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.ErrorEval;
import org.apache.poi.hssf.record.formula.eval.Eval; import org.apache.poi.hssf.record.formula.eval.Eval;
import org.apache.poi.hssf.record.formula.eval.EvaluationException;
import org.apache.poi.hssf.record.formula.eval.NumberEval; 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.NumericValueEval;
import org.apache.poi.hssf.record.formula.eval.OperandResolver;
import org.apache.poi.hssf.record.formula.eval.RefEval; 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.StringEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.record.formula.functions.LookupUtils.CompareResult;
import org.apache.poi.hssf.record.formula.functions.LookupUtils.LookupValueComparer;
/** /**
* Implementation for the MATCH() Excel function.<p/> * Implementation for the MATCH() Excel function.<p/>
@ -62,17 +65,6 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval;
*/ */
public final class Match implements Function { 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) { public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) {
@ -82,7 +74,7 @@ public final class Match implements Function {
case 3: case 3:
try { try {
match_type = evaluateMatchTypeArg(args[2], srcCellRow, srcCellCol); match_type = evaluateMatchTypeArg(args[2], srcCellRow, srcCellCol);
} catch (EvalEx e) { } catch (EvaluationException e) {
// Excel/MATCH() seems to have slightly abnormal handling of errors with // Excel/MATCH() seems to have slightly abnormal handling of errors with
// the last parameter. Errors do not propagate up. Every error gets // the last parameter. Errors do not propagate up. Every error gets
// translated into #REF! // translated into #REF!
@ -100,53 +92,16 @@ public final class Match implements Function {
try { try {
ValueEval lookupValue = evaluateLookupValue(args[0], srcCellRow, srcCellCol); ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol);
ValueEval[] lookupRange = evaluateLookupRange(args[1]); ValueEval[] lookupRange = evaluateLookupRange(args[1]);
int index = findIndexOfValue(lookupValue, lookupRange, matchExact, findLargestLessThanOrEqual); int index = findIndexOfValue(lookupValue, lookupRange, matchExact, findLargestLessThanOrEqual);
return new NumberEval(index + 1); // +1 to convert to 1-based return new NumberEval(index + 1); // +1 to convert to 1-based
} catch (EvalEx e) { } catch (EvaluationException e) {
return e.getError(); return e.getErrorEval();
} }
} }
private static ValueEval chooseSingleElementFromArea(AreaEval ae, private static ValueEval[] evaluateLookupRange(Eval eval) throws EvaluationException {
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) { if (eval instanceof RefEval) {
RefEval re = (RefEval) eval; RefEval re = (RefEval) eval;
return new ValueEval[] { re.getInnerValueEval(), }; return new ValueEval[] { re.getInnerValueEval(), };
@ -154,55 +109,36 @@ public final class Match implements Function {
if (eval instanceof AreaEval) { if (eval instanceof AreaEval) {
AreaEval ae = (AreaEval) eval; AreaEval ae = (AreaEval) eval;
if(!ae.isColumn() && !ae.isRow()) { if(!ae.isColumn() && !ae.isRow()) {
throw new EvalEx(ErrorEval.NA); throw new EvaluationException(ErrorEval.NA);
} }
return ae.getValues(); return ae.getValues();
} }
// Error handling for lookup_range arg is also unusual // Error handling for lookup_range arg is also unusual
if(eval instanceof NumericValueEval) { if(eval instanceof NumericValueEval) {
throw new EvalEx(ErrorEval.NA); throw new EvaluationException(ErrorEval.NA);
} }
if (eval instanceof StringEval) { if (eval instanceof StringEval) {
StringEval se = (StringEval) eval; StringEval se = (StringEval) eval;
Double d = parseDouble(se.getStringValue()); Double d = OperandResolver.parseDouble(se.getStringValue());
if(d == null) { if(d == null) {
// plain string // plain string
throw new EvalEx(ErrorEval.VALUE_INVALID); throw new EvaluationException(ErrorEval.VALUE_INVALID);
} }
// else looks like a number // else looks like a number
throw new EvalEx(ErrorEval.NA); throw new EvaluationException(ErrorEval.NA);
} }
throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); 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) private static double evaluateMatchTypeArg(Eval arg, int srcCellRow, short srcCellCol)
throws EvalEx { throws EvaluationException {
Eval match_type = arg; Eval match_type = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
if(arg instanceof AreaEval) {
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) { if(match_type instanceof ErrorEval) {
throw new EvalEx((ErrorEval)match_type); throw new EvaluationException((ErrorEval)match_type);
} }
if(match_type instanceof NumericValueEval) { if(match_type instanceof NumericValueEval) {
NumericValueEval ne = (NumericValueEval) match_type; NumericValueEval ne = (NumericValueEval) match_type;
@ -210,12 +146,12 @@ public final class Match implements Function {
} }
if (match_type instanceof StringEval) { if (match_type instanceof StringEval) {
StringEval se = (StringEval) match_type; StringEval se = (StringEval) match_type;
Double d = parseDouble(se.getStringValue()); Double d = OperandResolver.parseDouble(se.getStringValue());
if(d == null) { if(d == null) {
// plain string // plain string
throw new EvalEx(ErrorEval.VALUE_INVALID); throw new EvaluationException(ErrorEval.VALUE_INVALID);
} }
// if the string parses as a number, it is ok // if the string parses as a number, it is OK
return d.doubleValue(); return d.doubleValue();
} }
throw new RuntimeException("Unexpected match_type type (" + match_type.getClass().getName() + ")"); throw new RuntimeException("Unexpected match_type type (" + match_type.getClass().getName() + ")");
@ -225,88 +161,66 @@ public final class Match implements Function {
* @return zero based index * @return zero based index
*/ */
private static int findIndexOfValue(ValueEval lookupValue, ValueEval[] lookupRange, private static int findIndexOfValue(ValueEval lookupValue, ValueEval[] lookupRange,
boolean matchExact, boolean findLargestLessThanOrEqual) throws EvalEx { boolean matchExact, boolean findLargestLessThanOrEqual) throws EvaluationException {
// TODO - wildcard matching when matchExact and lookupValue is text containing * or ?
LookupValueComparer lookupComparer = createLookupComparer(lookupValue, matchExact);
if(matchExact) { if(matchExact) {
for (int i = 0; i < lookupRange.length; i++) { for (int i = 0; i < lookupRange.length; i++) {
ValueEval lri = lookupRange[i]; if(lookupComparer.compareTo(lookupRange[i]).isEqual()) {
if(lri.getClass() != lookupValue.getClass()) {
continue;
}
if(compareValues(lookupValue, lri) == 0) {
return i; return i;
} }
} }
} else { throw new EvaluationException(ErrorEval.NA);
// Note - backward iteration }
if(findLargestLessThanOrEqual) { if(findLargestLessThanOrEqual) {
// Note - backward iteration
for (int i = lookupRange.length - 1; i>=0; i--) { for (int i = lookupRange.length - 1; i>=0; i--) {
ValueEval lri = lookupRange[i]; CompareResult cmp = lookupComparer.compareTo(lookupRange[i]);
if(lri.getClass() != lookupValue.getClass()) { if(cmp.isTypeMismatch()) {
continue; continue;
} }
int cmp = compareValues(lookupValue, lri); if(!cmp.isLessThan()) {
if(cmp == 0) {
return i;
}
if(cmp > 0) {
return i; return i;
} }
} }
} else { throw new EvaluationException(ErrorEval.NA);
// find smallest greater than or equal to }
// else - find smallest greater than or equal to
// TODO - is binary search used for (match_type==+1) ?
for (int i = 0; i<lookupRange.length; i++) { for (int i = 0; i<lookupRange.length; i++) {
ValueEval lri = lookupRange[i]; CompareResult cmp = lookupComparer.compareTo(lookupRange[i]);
if(lri.getClass() != lookupValue.getClass()) { if(cmp.isEqual()) {
continue;
}
int cmp = compareValues(lookupValue, lri);
if(cmp == 0) {
return i; return i;
} }
if(cmp > 0) { if(cmp.isGreaterThan()) {
if(i<1) { if(i<1) {
throw new EvalEx(ErrorEval.NA); throw new EvaluationException(ErrorEval.NA);
} }
return i-1; return i-1;
} }
} }
} throw new EvaluationException(ErrorEval.NA);
} }
throw new EvalEx(ErrorEval.NA); private static LookupValueComparer createLookupComparer(ValueEval lookupValue, boolean matchExact) throws EvaluationException {
if (matchExact && lookupValue instanceof StringEval) {
String stringValue = ((StringEval) lookupValue).getStringValue();
if(isLookupValueWild(stringValue)) {
throw new RuntimeException("Wildcard lookup values '" + stringValue + "' not supported yet");
} }
}
return LookupUtils.createLookupComparer(lookupValue);
}
/** private static boolean isLookupValueWild(String stringValue) {
* This method can only compare a pair of <tt>NumericValueEval</tt>s, <tt>StringEval</tt>s if(stringValue.indexOf('?') >=0 || stringValue.indexOf('*') >=0) {
* or <tt>BoolEval</tt>s return true;
* @return negative for a&lt;b, positive for a&gt;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) { return false;
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() + ")");
} }
} }

View File

@ -1,25 +1,123 @@
/* /* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one or more Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0 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 not use this file except in compliance with
* the License. You may obtain a copy of the License at the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and See the License for the specific language governing permissions and
* limitations under the License. limitations under the License.
*/ ==================================================================== */
/*
* Created on May 15, 2005
*
*/
package org.apache.poi.hssf.record.formula.functions; package org.apache.poi.hssf.record.formula.functions;
public class Vlookup extends NotImplementedFunction { 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.EvaluationException;
import org.apache.poi.hssf.record.formula.eval.OperandResolver;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
import org.apache.poi.hssf.record.formula.functions.LookupUtils.ValueVector;
/**
* Implementation of the VLOOKUP() function.<p/>
*
* VLOOKUP finds a row in a lookup table by the first column value and returns the value from another column.
*
* <b>Syntax</b>:<br/>
* <b>VLOOKUP</b>(<b>lookup_value</b>, <b>table_array</b>, <b>col_index_num</b>, range_lookup)<p/>
*
* <b>lookup_value</b> The value to be found in the first column of the table array.<br/>
* <b>table_array</> An area reference for the lookup data. <br/>
* <b>col_index_num</b> a 1 based index specifying which column value of the lookup data will be returned.<br/>
* <b>range_lookup</b> If TRUE (default), VLOOKUP finds the largest value less than or equal to
* the lookup_value. If FALSE, only exact matches will be considered<br/>
*
* @author Josh Micich
*/
public final class Vlookup implements Function {
private static final class ColumnVector implements ValueVector {
private final AreaEval _tableArray;
private final int _size;
private final int _columnAbsoluteIndex;
private final int _firstRowAbsoluteIndex;
public ColumnVector(AreaEval tableArray, int columnIndex) {
_columnAbsoluteIndex = tableArray.getFirstColumn() + columnIndex;
if(!tableArray.containsColumn((short)_columnAbsoluteIndex)) {
int lastColIx = tableArray.getLastColumn() - tableArray.getFirstColumn();
throw new IllegalArgumentException("Specified column index (" + columnIndex
+ ") is outside the allowed range (0.." + lastColIx + ")");
}
_tableArray = tableArray;
_size = tableArray.getLastRow() - tableArray.getFirstRow() + 1;
if(_size < 1) {
throw new RuntimeException("bad table array size zero");
}
_firstRowAbsoluteIndex = tableArray.getFirstRow();
}
public ValueEval getItem(int index) {
if(index>_size) {
throw new ArrayIndexOutOfBoundsException("Specified index (" + index
+ ") is outside the allowed range (0.." + (_size-1) + ")");
}
return _tableArray.getValueAt(_firstRowAbsoluteIndex + index, (short)_columnAbsoluteIndex);
}
public int getSize() {
return _size;
}
}
public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) {
Eval arg3 = null;
switch(args.length) {
case 4:
arg3 = args[3]; // important: assumed array element is never null
case 3:
break;
default:
// wrong number of arguments
return ErrorEval.VALUE_INVALID;
}
try {
// Evaluation order:
// arg0 lookup_value, arg1 table_array, arg3 range_lookup, find lookup value, arg2 col_index, fetch result
ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol);
AreaEval tableArray = LookupUtils.resolveTableArrayArg(args[1]);
boolean isRangeLookup = LookupUtils.resolveRangeLookupArg(arg3, srcCellRow, srcCellCol);
int rowIndex = LookupUtils.lookupIndexOfValue(lookupValue, new ColumnVector(tableArray, 0), isRangeLookup);
ValueEval veColIndex = OperandResolver.getSingleValue(args[2], srcCellRow, srcCellCol);
int colIndex = LookupUtils.resolveRowOrColIndexArg(veColIndex);
ValueVector resultCol = createResultColumnVector(tableArray, colIndex);
return resultCol.getItem(rowIndex);
} catch (EvaluationException e) {
return e.getErrorEval();
}
}
/**
* Returns one column from an <tt>AreaEval</tt>
*
* @throws EvaluationException (#VALUE!) if colIndex is negative, (#REF!) if colIndex is too high
*/
private ValueVector createResultColumnVector(AreaEval tableArray, int colIndex) throws EvaluationException {
if(colIndex < 0) {
throw EvaluationException.invalidValue();
}
int nCols = tableArray.getLastColumn() - tableArray.getFirstColumn() + 1;
if(colIndex >= nCols) {
throw EvaluationException.invalidRef();
}
return new ColumnVector(tableArray, colIndex);
}
} }

View File

@ -0,0 +1,385 @@
/*
* 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 java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
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.usermodel.HSSFFormulaEvaluator.CellValue;
import org.apache.poi.hssf.util.CellReference;
/**
* Tests lookup functions (VLOOKUP, HLOOKUP, LOOKUP, MATCH) as loaded from a test data spreadsheet.<p/>
* These tests have been separated from the common function and operator tests because the lookup
* functions have more complex test cases and test data setup.
*
* Tests for bug fixes and specific/tricky behaviour can be found in the corresponding test class
* (<tt>TestXxxx</tt>) of the target (<tt>Xxxx</tt>) implementor, where execution can be observed
* more easily.
*
* @author Josh Micich
*/
public final class TestLookupFunctionsFromSpreadsheet 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 = "LookupFunctionsTestCaseData.xls";
/** Name of the first sheet in the spreadsheet (contains comments) */
public final static String README_SHEET_NAME = "Read Me";
/** Row (zero-based) in each sheet where the evaluation cases start. */
public static final int START_TEST_CASES_ROW_INDEX = 4; // Row '5'
/** Index of the column that contains the function names */
public static final short COLUMN_INDEX_MARKER = 0; // Column 'A'
public static final short COLUMN_INDEX_EVALUATION = 1; // Column 'B'
public static final short COLUMN_INDEX_EXPECTED_RESULT = 2; // Column 'C'
public static final short COLUMN_ROW_COMMENT = 3; // Column 'D'
/** Used to indicate when there are no more test cases on the current sheet */
public static final String TEST_CASES_END_MARKER = "<end>";
/** Used to indicate that the test on the current row should be ignored */
public static final String SKIP_CURRENT_TEST_CASE_MARKER = "<skip>";
}
// Note - multiple failures are aggregated before ending.
// If one or more functions fail, a single AssertionFailedError is thrown at the end
private int _sheetFailureCount;
private int _sheetSuccessCount;
private int _evaluationFailureCount;
private int _evaluationSuccessCount;
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_ERROR) {
confirmErrorResult(msg, expected.getErrorCellValue(), actual);
return;
}
if(actual.getCellType() == HSSFCell.CELL_TYPE_ERROR) {
throw unexpectedError(msg, expected, actual.getErrorValue());
}
if(actual.getCellType() != expected.getCellType()) {
throw wrongTypeError(msg, expected, actual);
}
switch (expected.getCellType()) {
case HSSFCell.CELL_TYPE_BOOLEAN:
assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue());
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(expected.getNumericCellValue(), actual.getNumberValue(), 0.0);
break;
case HSSFCell.CELL_TYPE_STRING:
assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getRichTextStringValue().getString());
break;
}
}
private static AssertionFailedError wrongTypeError(String msgPrefix, HSSFCell expectedCell, CellValue actualValue) {
return new AssertionFailedError(msgPrefix + " Result type mismatch. Evaluated result was "
+ formatValue(actualValue)
+ " but the expected result was "
+ formatValue(expectedCell)
);
}
private static AssertionFailedError unexpectedError(String msgPrefix, HSSFCell expected, int actualErrorCode) {
return new AssertionFailedError(msgPrefix + " Error code ("
+ ErrorEval.getText(actualErrorCode)
+ ") was evaluated, but the expected result was "
+ formatValue(expected)
);
}
private static void confirmErrorResult(String msgPrefix, int expectedErrorCode, CellValue actual) {
if(actual.getCellType() != HSSFCell.CELL_TYPE_ERROR) {
throw new AssertionFailedError(msgPrefix + " Expected cell error ("
+ ErrorEval.getText(expectedErrorCode) + ") but actual value was "
+ formatValue(actual));
}
if(expectedErrorCode != actual.getErrorValue()) {
throw new AssertionFailedError(msgPrefix + " Expected cell error code ("
+ ErrorEval.getText(expectedErrorCode)
+ ") but actual error code was ("
+ ErrorEval.getText(actual.getErrorValue())
+ ")");
}
}
private static String formatValue(HSSFCell expecedCell) {
switch (expecedCell.getCellType()) {
case HSSFCell.CELL_TYPE_BLANK: return "<blank>";
case HSSFCell.CELL_TYPE_BOOLEAN: return String.valueOf(expecedCell.getBooleanCellValue());
case HSSFCell.CELL_TYPE_NUMERIC: return String.valueOf(expecedCell.getNumericCellValue());
case HSSFCell.CELL_TYPE_STRING: return expecedCell.getRichStringCellValue().getString();
}
throw new RuntimeException("Unexpected cell type of expected value (" + expecedCell.getCellType() + ")");
}
private static String formatValue(CellValue actual) {
switch (actual.getCellType()) {
case HSSFCell.CELL_TYPE_BLANK: return "<blank>";
case HSSFCell.CELL_TYPE_BOOLEAN: return String.valueOf(actual.getBooleanValue());
case HSSFCell.CELL_TYPE_NUMERIC: return String.valueOf(actual.getNumberValue());
case HSSFCell.CELL_TYPE_STRING: return actual.getRichTextStringValue().getString();
}
throw new RuntimeException("Unexpected cell type of evaluated value (" + actual.getCellType() + ")");
}
protected void setUp() throws Exception {
_sheetFailureCount = 0;
_sheetSuccessCount = 0;
_evaluationFailureCount = 0;
_evaluationSuccessCount = 0;
}
public void testFunctionsFromTestSpreadsheet() {
String filePath = System.getProperty("HSSF.testdata.path")+ "/" + SS.FILENAME;
HSSFWorkbook workbook;
try {
FileInputStream fin = new FileInputStream( filePath );
workbook = new HSSFWorkbook( fin );
} catch (IOException e) {
throw new RuntimeException(e);
}
confirmReadMeSheet(workbook);
int nSheets = workbook.getNumberOfSheets();
for(int i=1; i< nSheets; i++) {
int sheetResult = processTestSheet(workbook, i, workbook.getSheetName(i));
switch(sheetResult) {
case Result.ALL_EVALUATIONS_SUCCEEDED: _sheetSuccessCount ++; break;
case Result.SOME_EVALUATIONS_FAILED: _sheetFailureCount ++; break;
}
}
// confirm results
String successMsg = "There were "
+ _sheetSuccessCount + " successful sheets(s) and "
+ _evaluationSuccessCount + " function(s) without error";
if(_sheetFailureCount > 0) {
String msg = _sheetFailureCount + " sheets(s) failed with "
+ _evaluationFailureCount + " evaluation(s). " + successMsg;
throw new AssertionFailedError(msg);
}
if(false) { // normally no output for successful tests
System.out.println(getClass().getName() + ": " + successMsg);
}
}
private int processTestSheet(HSSFWorkbook workbook, int sheetIndex, String sheetName) {
HSSFSheet sheet = workbook.getSheetAt(sheetIndex);
HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, workbook);
int maxRows = sheet.getLastRowNum()+1;
int result = Result.NO_EVALUATIONS_FOUND; // so far
String currentGroupComment = null;
for(int rowIndex=SS.START_TEST_CASES_ROW_INDEX; rowIndex<maxRows; rowIndex++) {
HSSFRow r = sheet.getRow(rowIndex);
String newMarkerValue = getMarkerColumnValue(r);
if(r == null) {
continue;
}
if(SS.TEST_CASES_END_MARKER.equalsIgnoreCase(newMarkerValue)) {
// normal exit point
return result;
}
if(SS.SKIP_CURRENT_TEST_CASE_MARKER.equalsIgnoreCase(newMarkerValue)) {
// currently disabled test case row
continue;
}
if(newMarkerValue != null) {
currentGroupComment = newMarkerValue;
}
HSSFCell c = r.getCell(SS.COLUMN_INDEX_EVALUATION);
if (c == null || c.getCellType() != HSSFCell.CELL_TYPE_FORMULA) {
continue;
}
evaluator.setCurrentRow(r);
CellValue actualValue = evaluator.evaluate(c);
HSSFCell expectedValueCell = r.getCell(SS.COLUMN_INDEX_EXPECTED_RESULT);
String rowComment = getRowCommentColumnValue(r);
String msgPrefix = formatTestCaseDetails(sheetName, r.getRowNum(), c, currentGroupComment, rowComment);
try {
confirmExpectedResult(msgPrefix, expectedValueCell, actualValue);
_evaluationSuccessCount ++;
if(result != Result.SOME_EVALUATIONS_FAILED) {
result = Result.ALL_EVALUATIONS_SUCCEEDED;
}
} catch (RuntimeException e) {
_evaluationFailureCount ++;
printShortStackTrace(System.err, e);
result = Result.SOME_EVALUATIONS_FAILED;
} catch (AssertionFailedError e) {
_evaluationFailureCount ++;
printShortStackTrace(System.err, e);
result = Result.SOME_EVALUATIONS_FAILED;
}
}
throw new RuntimeException("Missing end marker '" + SS.TEST_CASES_END_MARKER
+ "' on sheet '" + sheetName + "'");
}
private static String formatTestCaseDetails(String sheetName, int rowNum, HSSFCell c, String currentGroupComment,
String rowComment) {
StringBuffer sb = new StringBuffer();
CellReference cr = new CellReference(sheetName, rowNum, c.getCellNum(), false, false);
sb.append(cr.formatAsString());
sb.append(" {=").append(c.getCellFormula()).append("}");
if(currentGroupComment != null) {
sb.append(" '");
sb.append(currentGroupComment);
if(rowComment != null) {
sb.append(" - ");
sb.append(rowComment);
}
sb.append("' ");
} else {
if(rowComment != null) {
sb.append(" '");
sb.append(rowComment);
sb.append("' ");
}
}
return sb.toString();
}
/**
* Asserts that the 'read me' comment page exists, and has this class' name in one of the
* cells. This back-link is to make it easy to find this class if a reader encounters the
* spreadsheet first.
*/
private void confirmReadMeSheet(HSSFWorkbook workbook) {
String firstSheetName = workbook.getSheetName(0);
if(!firstSheetName.equalsIgnoreCase(SS.README_SHEET_NAME)) {
throw new RuntimeException("First sheet's name was '" + firstSheetName + "' but expected '" + SS.README_SHEET_NAME + "'");
}
HSSFSheet sheet = workbook.getSheetAt(0);
String specifiedClassName = sheet.getRow(2).getCell((short)0).getRichStringCellValue().getString();
assertEquals("Test class name in spreadsheet comment", getClass().getName(), specifiedClassName);
}
/**
* Useful to keep output concise when expecting many failures to be reported by this test case
*/
private static void printShortStackTrace(PrintStream ps, Throwable e) {
StackTraceElement[] stes = e.getStackTrace();
int startIx = 0;
// skip any top frames inside junit.framework.Assert
while(startIx<stes.length) {
if(!stes[startIx].getClassName().equals(Assert.class.getName())) {
break;
}
startIx++;
}
// skip bottom frames (part of junit framework)
int endIx = startIx+1;
while(endIx < stes.length) {
if(stes[endIx].getClassName().equals(TestCase.class.getName())) {
break;
}
endIx++;
}
if(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; i<endIx; i++) {
ps.println("\tat " + stes[i].toString());
}
}
private static String getRowCommentColumnValue(HSSFRow r) {
return getCellTextValue(r, SS.COLUMN_ROW_COMMENT, "row comment");
}
private static String getMarkerColumnValue(HSSFRow r) {
return getCellTextValue(r, SS.COLUMN_INDEX_MARKER, "marker");
}
/**
* @return <code>null</code> if cell is missing, empty or blank
*/
private static String getCellTextValue(HSSFRow r, int colIndex, String columnName) {
if(r == null) {
return null;
}
HSSFCell cell = r.getCell((short) colIndex);
if(cell == null) {
return null;
}
if(cell.getCellType() == HSSFCell.CELL_TYPE_BLANK) {
return null;
}
if(cell.getCellType() == HSSFCell.CELL_TYPE_STRING) {
return cell.getRichStringCellValue().getString();
}
throw new RuntimeException("Bad cell type for '" + columnName + "' column: ("
+ cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")");
}
}