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:
parent
08a1a952b8
commit
7cdfaaa61d
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
public interface HSSFErrorConstants
|
private HSSFErrorConstants() {
|
||||||
{
|
// no instances of this class
|
||||||
public static final byte ERROR_NULL = 0x00; // #NULL!
|
}
|
||||||
public static final byte ERROR_DIV_0 = 0x07; // #DIV/0!
|
|
||||||
public static final byte ERROR_VALUE = 0x0f; // #VALUE!
|
/** <b>#NULL!</b> - Intersection of two cell ranges is empty */
|
||||||
public static final byte ERROR_REF = 0x17; // #REF!
|
public static final int ERROR_NULL = 0x00;
|
||||||
public static final byte ERROR_NAME = 0x1d; // #NAME?
|
/** <b>#DIV/0!</b> - Division by zero */
|
||||||
public static final byte ERROR_NUM = 0x24; // #NUM!
|
public static final int ERROR_DIV_0 = 0x07;
|
||||||
public static final byte ERROR_NA = 0x2a; // #N/A
|
/** <b>#VALUE!</b> - Wrong type of operand */
|
||||||
|
public static final int ERROR_VALUE = 0x0F;
|
||||||
|
/** <b>#REF!</b> - Illegal or deleted cell reference */
|
||||||
|
public static final int ERROR_REF = 0x17;
|
||||||
|
/** <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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 < amolweb at ya hoo dot com >
|
* @author Amol S. Deshmukh < amolweb at ya hoo dot com >
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
@ -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 < amolweb at ya hoo dot com >
|
* @author Amol S. Deshmukh < amolweb at ya hoo dot com >
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 Return</th><th>Value </th><th>Thrown Error</th></tr>
|
||||||
|
* <tr><td>5</td><td>4</td><td> </td></tr>
|
||||||
|
* <tr><td>2.9</td><td>2</td><td> </td></tr>
|
||||||
|
* <tr><td>"5"</td><td>4</td><td> </td></tr>
|
||||||
|
* <tr><td>"2.18e1"</td><td>21</td><td> </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> </td></tr>
|
||||||
|
* <tr><td>"TRUE"</td><td> </td><td>#REF!</td></tr>
|
||||||
|
* <tr><td>"abc"</td><td> </td><td>#REF!</td></tr>
|
||||||
|
* <tr><td>""</td><td> </td><td>#REF!</td></tr>
|
||||||
|
* <tr><td><blank></td><td> </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() + ")");
|
||||||
|
}
|
||||||
|
}
|
@ -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<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) {
|
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() + ")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) + ")");
|
||||||
|
}
|
||||||
|
}
|
BIN
src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls
Executable file
BIN
src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls
Executable file
Binary file not shown.
Loading…
Reference in New Issue
Block a user