From 7cdfaaa61d9c001cdd109f0a3e09831272ee4dd7 Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Fri, 22 Feb 2008 11:23:50 +0000 Subject: [PATCH] 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 --- src/documentation/content/xdocs/changes.xml | 1 + src/documentation/content/xdocs/status.xml | 1 + .../poi/hssf/record/formula/ErrPtg.java | 79 +-- .../hssf/usermodel/HSSFErrorConstants.java | 76 ++- .../hssf/record/formula/eval/ErrorEval.java | 133 +++-- .../hssf/record/formula/eval/StringEval.java | 21 +- .../record/formula/eval/StringValueEval.java | 10 +- .../record/formula/functions/Hlookup.java | 140 ++++- .../hssf/record/formula/functions/Lookup.java | 113 +++- .../record/formula/functions/LookupUtils.java | 530 ++++++++++++++++++ .../hssf/record/formula/functions/Match.java | 220 +++----- .../record/formula/functions/Vlookup.java | 140 ++++- .../TestLookupFunctionsFromSpreadsheet.java | 385 +++++++++++++ .../hssf/data/LookupFunctionsTestCaseData.xls | Bin 0 -> 39936 bytes 14 files changed, 1495 insertions(+), 354 deletions(-) create mode 100644 src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java create mode 100644 src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java create mode 100755 src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index f6d2de4e2..285dcdea5 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -36,6 +36,7 @@ + 44450 - Support for Lookup, HLookup and VLookup functions 44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly 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 44371 - Support for the Offset function diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 8c21cbd94..18229083e 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -33,6 +33,7 @@ + 44450 - Support for Lookup, HLookup and VLookup functions 44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly 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 44371 - Support for the Offset function diff --git a/src/java/org/apache/poi/hssf/record/formula/ErrPtg.java b/src/java/org/apache/poi/hssf/record/formula/ErrPtg.java index 34bad6f32..26cc2e027 100644 --- a/src/java/org/apache/poi/hssf/record/formula/ErrPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/ErrPtg.java @@ -26,66 +26,67 @@ import org.apache.poi.hssf.usermodel.HSSFErrorConstants; /** * @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; + + /** #NULL! - Intersection of two cell ranges is empty */ + public static final ErrPtg NULL_INTERSECTION = new ErrPtg(EC.ERROR_NULL); + /** #DIV/0! - Division by zero */ + public static final ErrPtg DIV_ZERO = new ErrPtg(EC.ERROR_DIV_0); + /** #VALUE! - Wrong type of operand */ + public static final ErrPtg VALUE_INVALID = new ErrPtg(EC.ERROR_VALUE); + /** #REF! - Illegal or deleted cell reference */ + public static final ErrPtg REF_INVALID = new ErrPtg(EC.ERROR_REF); + /** #NAME? - Wrong function or range name */ + public static final ErrPtg NAME_INVALID = new ErrPtg(EC.ERROR_NAME); + /** #NUM! - Value range overflow */ + public static final ErrPtg NUM_ERROR = new ErrPtg(EC.ERROR_NUM); + /** #N/A - Argument or function not available */ + public static final ErrPtg N_A = new ErrPtg(EC.ERROR_NA); + + public static final short sid = 0x1c; private static final int SIZE = 2; - private byte field_1_error_code; + private int field_1_error_code; /** 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) - { - field_1_error_code = in.readByte(); + + public ErrPtg(RecordInputStream in) { + this(in.readByte()); } public void writeBytes(byte [] array, int offset) { 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) - { - 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. - return "#N/A"; + public String toFormulaString(Workbook book) { + return HSSFErrorConstants.getText(field_1_error_code); } - public int getSize() - { + public int getSize() { return SIZE; } - public byte getDefaultOperandClass() - { + public byte getDefaultOperandClass() { return Ptg.CLASS_VALUE; } public Object clone() { - ErrPtg ptg = new ErrPtg(); - ptg.field_1_error_code = field_1_error_code; - return ptg; + return new ErrPtg(field_1_error_code); + } + + public int getErrorCode() { + return field_1_error_code; } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java b/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java index 1f5ec13c3..89c25d1e8 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java @@ -15,26 +15,68 @@ limitations under the License. ==================================================================== */ - -/* - * HSSFErrorConstants.java - * - * Created on January 19, 2002, 9:30 AM - */ 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 */ - -public interface HSSFErrorConstants -{ - 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! - public static final byte ERROR_REF = 0x17; // #REF! - public static final byte ERROR_NAME = 0x1d; // #NAME? - public static final byte ERROR_NUM = 0x24; // #NUM! - public static final byte ERROR_NA = 0x2a; // #N/A +public final class HSSFErrorConstants { + private HSSFErrorConstants() { + // no instances of this class + } + + /** #NULL! - Intersection of two cell ranges is empty */ + public static final int ERROR_NULL = 0x00; + /** #DIV/0! - Division by zero */ + public static final int ERROR_DIV_0 = 0x07; + /** #VALUE! - Wrong type of operand */ + public static final int ERROR_VALUE = 0x0F; + /** #REF! - Illegal or deleted cell reference */ + public static final int ERROR_REF = 0x17; + /** #NAME? - Wrong function or range name */ + public static final int ERROR_NAME = 0x1D; + /** #NUM! - Value range overflow */ + public static final int ERROR_NUM = 0x24; + /** #N/A - 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 true 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; + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java index f57976d3e..56e4db1b2 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java @@ -14,112 +14,105 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + 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 > - * + * */ public final class ErrorEval implements ValueEval { - /** - * Contains raw Excel error codes (as defined in OOO's excelfileformat.pdf (2.5.6) - */ - private static final class ErrorCode { - /** #NULL! - Intersection of two cell ranges is empty */ - public static final int NULL = 0x00; - /** #DIV/0! - Division by zero */ - public static final int DIV_0 = 0x07; - /** #VALUE! - Wrong type of operand */ - public static final int VALUE = 0x0F; - /** #REF! - Illegal or deleted cell reference */ - public static final int REF = 0x17; - /** #NAME? - Wrong function or range name */ - public static final int NAME = 0x1D; - /** #NUM! - Value range overflow */ - public static final int NUM = 0x24; - /** #N/A - Argument or function not available */ - public static final int N_A = 0x2A; - - public static final String getText(int errorCode) { - switch(errorCode) { - case NULL: return "#NULL!"; - case DIV_0: return "#DIV/0!"; - case VALUE: return "#VALUE!"; - case REF: return "#REF!"; - case NAME: return "#NAME?"; - case NUM: return "#NUM!"; - case N_A: return "#N/A"; - } - return "???"; - } - } + + // convenient access to namespace + private static final HSSFErrorConstants EC = null; /** #NULL! - Intersection of two cell ranges is empty */ - public static final ErrorEval NULL_INTERSECTION = new ErrorEval(ErrorCode.NULL); + public static final ErrorEval NULL_INTERSECTION = new ErrorEval(EC.ERROR_NULL); /** #DIV/0! - Division by zero */ - public static final ErrorEval DIV_ZERO = new ErrorEval(ErrorCode.DIV_0); + public static final ErrorEval DIV_ZERO = new ErrorEval(EC.ERROR_DIV_0); /** #VALUE! - Wrong type of operand */ - public static final ErrorEval VALUE_INVALID = new ErrorEval(ErrorCode.VALUE); + public static final ErrorEval VALUE_INVALID = new ErrorEval(EC.ERROR_VALUE); /** #REF! - Illegal or deleted cell reference */ - public static final ErrorEval REF_INVALID = new ErrorEval(ErrorCode.REF); + public static final ErrorEval REF_INVALID = new ErrorEval(EC.ERROR_REF); /** #NAME? - Wrong function or range name */ - public static final ErrorEval NAME_INVALID = new ErrorEval(ErrorCode.NAME); + public static final ErrorEval NAME_INVALID = new ErrorEval(EC.ERROR_NAME); /** #NUM! - Value range overflow */ - public static final ErrorEval NUM_ERROR = new ErrorEval(ErrorCode.NUM); + public static final ErrorEval NUM_ERROR = new ErrorEval(EC.ERROR_NUM); /** #N/A - Argument or function not available */ - public static final ErrorEval NA = new ErrorEval(ErrorCode.N_A); + 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; - /** - * Translates an Excel internal error code into the corresponding POI ErrorEval instance + * @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. + */ + public static final ErrorEval UNKNOWN_ERROR = new ErrorEval(-20); + public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(FUNCTION_NOT_IMPLEMENTED_CODE); + // Note - Excel does not seem to represent this condition with an error code + public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(CIRCULAR_REF_ERROR_CODE); + + + /** + * Translates an Excel internal error code into the corresponding POI ErrorEval instance * @param errorCode */ public static ErrorEval valueOf(int errorCode) { switch(errorCode) { - case ErrorCode.NULL: return NULL_INTERSECTION; - case ErrorCode.DIV_0: return DIV_ZERO; - case ErrorCode.VALUE: return VALUE_INVALID; -// case ErrorCode.REF: return REF_INVALID; - case ErrorCode.REF: return UNKNOWN_ERROR; - case ErrorCode.NAME: return NAME_INVALID; - case ErrorCode.NUM: return NUM_ERROR; - case ErrorCode.N_A: return NA; - - // these cases probably shouldn't be coming through here - // but (as of Jan-2008) a lot of code depends on it. -// case -20: return UNKNOWN_ERROR; -// case -30: return FUNCTION_NOT_IMPLEMENTED; -// case -60: return CIRCULAR_REF_ERROR; + case HSSFErrorConstants.ERROR_NULL: return NULL_INTERSECTION; + case HSSFErrorConstants.ERROR_DIV_0: return DIV_ZERO; + case HSSFErrorConstants.ERROR_VALUE: return VALUE_INVALID; + case HSSFErrorConstants.ERROR_REF: return REF_INVALID; + case HSSFErrorConstants.ERROR_NAME: return NAME_INVALID; + case HSSFErrorConstants.ERROR_NUM: return NUM_ERROR; + case HSSFErrorConstants.ERROR_NA: return NA; + // non-std errors (conditions modeled as errors by POI) + case CIRCULAR_REF_ERROR_CODE: return CIRCULAR_REF_ERROR; + case FUNCTION_NOT_IMPLEMENTED_CODE: return FUNCTION_NOT_IMPLEMENTED; } throw new RuntimeException("Unexpected error code (" + errorCode + ")"); } - - // POI internal error codes - public static final ErrorEval UNKNOWN_ERROR = new ErrorEval(-20); - public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(-30); - // Note - Excel does not seem to represent this condition with an error code - public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(-60); + /** + * Converts error codes to text. Handles non-standard error codes OK. + * For debug/test purposes (and for formatting error messages). + * @return the String representation of the specified Excel error code. + */ + 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 */ private ErrorEval(int errorCode) { - this.errorCode = errorCode; + _errorCode = errorCode; } public int getErrorCode() { - return errorCode; + return _errorCode; } public String toString() { StringBuffer sb = new StringBuffer(64); sb.append(getClass().getName()).append(" ["); - sb.append(ErrorCode.getText(errorCode)); + sb.append(getText(_errorCode)); sb.append("]"); return sb.toString(); } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java index 01af4e843..27a9c6a62 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; 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 > * */ -public class StringEval implements StringValueEval { +public final class StringEval implements StringValueEval { public static final StringEval EMPTY_INSTANCE = new StringEval(""); - private String value; + private final String value; public StringEval(Ptg ptg) { - this.value = ((StringPtg) ptg).getValue(); + this(((StringPtg) ptg).getValue()); } public StringEval(String value) { + if(value == null) { + throw new IllegalArgumentException("value must not be null"); + } this.value = value; } public String getStringValue() { return value; } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(value); + sb.append("]"); + return sb.toString(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java index b692f01ea..46c12236b 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + 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 String getStringValue(); + /** + * @return never null, possibly empty string. + */ + String getStringValue(); } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java index 8bac3d0c0..40ed1da49 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java @@ -1,25 +1,123 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -/* - * Created on May 15, 2005 - * - */ +/* ==================================================================== + 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; -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.

+ * + * HLOOKUP finds a column in a lookup table by the first row value and returns the value from another row. + * + * Syntax:
+ * HLOOKUP(lookup_value, table_array, row_index_num, range_lookup)

+ * + * lookup_value The value to be found in the first column of the table array.
+ * table_array An area reference for the lookup data.
+ * row_index_num a 1 based index specifying which row value of the lookup data will be returned.
+ * range_lookup If TRUE (default), HLOOKUP finds the largest value less than or equal to + * the lookup_value. If FALSE, only exact matches will be considered
+ * + * @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 AreaEval + * + * @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); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java index f98ccca7e..be1d0d0f9 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java @@ -1,25 +1,96 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -/* - * Created on May 15, 2005 - * - */ +/* ==================================================================== + 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; -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.

+ * + * LOOKUP finds an index row in a lookup table by the first column value and returns the value from another column. + * + * Syntax:
+ * VLOOKUP(lookup_value, lookup_vector, result_vector)

+ * + * lookup_value The value to be found in the lookup vector.
+ * lookup_vector An area reference for the lookup data.
+ * result_vector Single row or single column area reference from which the result value is chosen.
+ * + * @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()); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java new file mode 100644 index 000000000..66123b298 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java @@ -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 AreaEval. + */ + public interface ValueVector { + ValueEval getItem(int index); + int getSize(); + } + /** + * Enumeration to support 4 valued comparison results.

+ * 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.

+ * + * A simple int might have done the same job, but there is risk in confusion with the well + * known Comparable.compareTo() and Comparator.compare() 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 CompareResult: LESS_THAN, EQUAL, + * GREATER_THAN or TYPE_MISMATCH + */ + 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 (col_index_num + * or row_index_num respectively).
+ * Sample behaviour: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Input   ReturnValue  Thrown Error
54 
2.92 
"5"4 
"2.18e1"21 
"-$2"-3*
FALSE-1*
TRUE0 
"TRUE" #REF!
"abc" #REF!
"" #REF!
<blank> #VALUE!

+ * + * * 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 (range_lookup) 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 @@ -62,17 +65,6 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval; */ 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) { @@ -82,7 +74,7 @@ public final class Match implements Function { case 3: try { match_type = evaluateMatchTypeArg(args[2], srcCellRow, srcCellCol); - } catch (EvalEx e) { + } catch (EvaluationException e) { // Excel/MATCH() seems to have slightly abnormal handling of errors with // the last parameter. Errors do not propagate up. Every error gets // translated into #REF! @@ -100,53 +92,16 @@ public final class Match implements Function { try { - ValueEval lookupValue = evaluateLookupValue(args[0], srcCellRow, srcCellCol); + ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); ValueEval[] lookupRange = evaluateLookupRange(args[1]); int index = findIndexOfValue(lookupValue, lookupRange, matchExact, findLargestLessThanOrEqual); return new NumberEval(index + 1); // +1 to convert to 1-based - } catch (EvalEx e) { - return e.getError(); + } catch (EvaluationException e) { + return e.getErrorEval(); } } - private static ValueEval chooseSingleElementFromArea(AreaEval ae, - int srcCellRow, short srcCellCol) throws EvalEx { - if (ae.isColumn()) { - if(ae.isRow()) { - return ae.getValues()[0]; - } - if(!ae.containsRow(srcCellRow)) { - throw new EvalEx(ErrorEval.VALUE_INVALID); - } - return ae.getValueAt(srcCellRow, ae.getFirstColumn()); - } - if(!ae.isRow()) { - throw new EvalEx(ErrorEval.VALUE_INVALID); - } - if(!ae.containsColumn(srcCellCol)) { - throw new EvalEx(ErrorEval.VALUE_INVALID); - } - return ae.getValueAt(ae.getFirstRow(), srcCellCol); - - } - - private static ValueEval evaluateLookupValue(Eval eval, int srcCellRow, short srcCellCol) - throws EvalEx { - if (eval instanceof RefEval) { - RefEval re = (RefEval) eval; - return re.getInnerValueEval(); - } - if (eval instanceof AreaEval) { - return chooseSingleElementFromArea((AreaEval) eval, srcCellRow, srcCellCol); - } - if (eval instanceof ValueEval) { - return (ValueEval) eval; - } - throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); - } - - - private static ValueEval[] evaluateLookupRange(Eval eval) throws EvalEx { + private static ValueEval[] evaluateLookupRange(Eval eval) throws EvaluationException { if (eval instanceof RefEval) { RefEval re = (RefEval) eval; return new ValueEval[] { re.getInnerValueEval(), }; @@ -154,55 +109,36 @@ public final class Match implements Function { if (eval instanceof AreaEval) { AreaEval ae = (AreaEval) eval; if(!ae.isColumn() && !ae.isRow()) { - throw new EvalEx(ErrorEval.NA); + throw new EvaluationException(ErrorEval.NA); } return ae.getValues(); } // Error handling for lookup_range arg is also unusual if(eval instanceof NumericValueEval) { - throw new EvalEx(ErrorEval.NA); + throw new EvaluationException(ErrorEval.NA); } if (eval instanceof StringEval) { StringEval se = (StringEval) eval; - Double d = parseDouble(se.getStringValue()); + Double d = OperandResolver.parseDouble(se.getStringValue()); if(d == null) { // plain string - throw new EvalEx(ErrorEval.VALUE_INVALID); + throw new EvaluationException(ErrorEval.VALUE_INVALID); } // else looks like a number - throw new EvalEx(ErrorEval.NA); + throw new EvaluationException(ErrorEval.NA); } throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); } - private static Double parseDouble(String stringValue) { - // TODO find better home for parseDouble - return Countif.parseDouble(stringValue); - } - - private static double evaluateMatchTypeArg(Eval arg, int srcCellRow, short srcCellCol) - throws EvalEx { - Eval match_type = arg; - if(arg instanceof 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(); - } + throws EvaluationException { + Eval match_type = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); + if(match_type instanceof ErrorEval) { - throw new EvalEx((ErrorEval)match_type); + throw new EvaluationException((ErrorEval)match_type); } if(match_type instanceof NumericValueEval) { NumericValueEval ne = (NumericValueEval) match_type; @@ -210,12 +146,12 @@ public final class Match implements Function { } if (match_type instanceof StringEval) { StringEval se = (StringEval) match_type; - Double d = parseDouble(se.getStringValue()); + Double d = OperandResolver.parseDouble(se.getStringValue()); if(d == null) { // 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(); } 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 */ private static int findIndexOfValue(ValueEval lookupValue, ValueEval[] lookupRange, - boolean matchExact, boolean findLargestLessThanOrEqual) throws EvalEx { - // TODO - wildcard matching when matchExact and lookupValue is text containing * or ? + boolean matchExact, boolean findLargestLessThanOrEqual) throws EvaluationException { + + LookupValueComparer lookupComparer = createLookupComparer(lookupValue, matchExact); + if(matchExact) { for (int i = 0; i < lookupRange.length; i++) { - ValueEval lri = lookupRange[i]; - if(lri.getClass() != lookupValue.getClass()) { - continue; - } - if(compareValues(lookupValue, lri) == 0) { + if(lookupComparer.compareTo(lookupRange[i]).isEqual()) { return i; } } - } else { + throw new EvaluationException(ErrorEval.NA); + } + + if(findLargestLessThanOrEqual) { // Note - backward iteration - if(findLargestLessThanOrEqual) { - for (int i = lookupRange.length - 1; i>=0; i--) { - ValueEval lri = lookupRange[i]; - if(lri.getClass() != lookupValue.getClass()) { - continue; - } - int cmp = compareValues(lookupValue, lri); - if(cmp == 0) { - return i; - } - if(cmp > 0) { - return i; - } + for (int i = lookupRange.length - 1; i>=0; i--) { + CompareResult cmp = lookupComparer.compareTo(lookupRange[i]); + if(cmp.isTypeMismatch()) { + continue; } - } else { - // find smallest greater than or equal to - for (int i = 0; i 0) { - if(i<1) { - throw new EvalEx(ErrorEval.NA); - } - return i-1; - } + if(!cmp.isLessThan()) { + return i; } - + } + throw new EvaluationException(ErrorEval.NA); + } + + // else - find smallest greater than or equal to + // TODO - is binary search used for (match_type==+1) ? + for (int i = 0; iNumericValueEvals, StringEvals - * or BoolEvals - * @return negative for a<b, positive for a>b and 0 for a = b - */ - private static int compareValues(ValueEval a, ValueEval b) { - if (a instanceof StringEval) { - StringEval sa = (StringEval) a; - StringEval sb = (StringEval) b; - return sa.getStringValue().compareToIgnoreCase(sb.getStringValue()); + private static boolean isLookupValueWild(String stringValue) { + if(stringValue.indexOf('?') >=0 || stringValue.indexOf('*') >=0) { + return true; } - if (a instanceof NumericValueEval) { - NumericValueEval na = (NumericValueEval) a; - NumericValueEval nb = (NumericValueEval) b; - return Double.compare(na.getNumberValue(), nb.getNumberValue()); - } - if (a instanceof BoolEval) { - boolean ba = ((BoolEval) a).getBooleanValue(); - boolean bb = ((BoolEval) b).getBooleanValue(); - if(ba == bb) { - return 0; - } - // TRUE > FALSE - if(ba) { - return +1; - } - return -1; - } - throw new RuntimeException("bad eval type (" + a.getClass().getName() + ")"); + return false; } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java index ad8b88daf..7d27491df 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java @@ -1,25 +1,123 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -/* - * Created on May 15, 2005 - * - */ +/* ==================================================================== + 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; -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.

+ * + * VLOOKUP finds a row in a lookup table by the first column value and returns the value from another column. + * + * Syntax:
+ * VLOOKUP(lookup_value, table_array, col_index_num, range_lookup)

+ * + * lookup_value The value to be found in the first column of the table array.
+ * table_array An area reference for the lookup data.
+ * col_index_num a 1 based index specifying which column value of the lookup data will be returned.
+ * range_lookup If TRUE (default), VLOOKUP finds the largest value less than or equal to + * the lookup_value. If FALSE, only exact matches will be considered
+ * + * @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 AreaEval + * + * @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); + } } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java new file mode 100644 index 000000000..071ca0f7d --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java @@ -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.

+ * 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 + * (TestXxxx) of the target (Xxxx) 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 = ""; + /** Used to indicate that the test on the current row should be ignored */ + public static final String SKIP_CURRENT_TEST_CASE_MARKER = ""; + + } + + // 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 ""; + 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 ""; + 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= endIx) { + // something went wrong. just print the whole stack trace + e.printStackTrace(ps); + } + endIx -= 4; // skip 4 frames of reflection invocation + ps.println(e.toString()); + for(int i=startIx; inull if cell is missing, empty or blank + */ + private static String 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) + ")"); + } +} diff --git a/src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls b/src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls new file mode 100755 index 0000000000000000000000000000000000000000..f4b35fb93504540b85455ff37341318cbcd74e1e GIT binary patch literal 39936 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 literal 0 HcmV?d00001