]> source.dussan.org Git - poi.git/commitdiff
Patch from Josh from bug #44450 - VLookup and HLookup support, and improvements to...
authorNick Burch <nick@apache.org>
Fri, 22 Feb 2008 11:23:50 +0000 (11:23 +0000)
committerNick Burch <nick@apache.org>
Fri, 22 Feb 2008 11:23:50 +0000 (11:23 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@630160 13f79535-47bb-0310-9956-ffa450edef68

14 files changed:
src/documentation/content/xdocs/changes.xml
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/hssf/record/formula/ErrPtg.java
src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Match.java
src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java
src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls [new file with mode: 0755]

index f6d2de4e292c0b8bc6d5ddbb2946b88b0226d35f..285dcdea57039d71aa06595c6244bab726837d30 100644 (file)
@@ -36,6 +36,7 @@
 
                <!-- Don't forget to update status.xml too! -->
         <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">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>
index 8c21cbd94d55716fb91b988c3ecfe534d206e98c..18229083e7253e211670592c0c9a26fd6a28695b 100644 (file)
@@ -33,6 +33,7 @@
        <!-- Don't forget to update changes.xml too! -->
     <changes>
         <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">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>
index 34bad6f32c359e2a3085c2c9625c73be3c4af8e3..26cc2e027bc9f9147a747e5ba807124722ffb459 100644 (file)
@@ -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;
+    
+    /** <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;
     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;
     }
 }
index 1f5ec13c3a437916f6c2bd5244f4608f5f427f5e..89c25d1e8727b2030c2d0c1eb8c7d12b9a76d313 100644 (file)
    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
+    }
+    
+    /** <b>#NULL!</b>  - Intersection of two cell ranges is empty */
+    public static final int ERROR_NULL = 0x00;
+    /** <b>#DIV/0!</b> - Division by zero */
+    public static final int ERROR_DIV_0 = 0x07;
+    /** <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;
+    }
 }
index f57976d3ed674872b62c44a67c94862ed1321353..56e4db1b22d9218094ae01e4b81464d3f8687a4b 100644 (file)
 * 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 &lt; amolweb at ya hoo dot com &gt;
- * 
+ *
  */
 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 {
-        /** <b>#NULL!</b>  - Intersection of two cell ranges is empty */
-        public static final int NULL = 0x00;
-        /** <b>#DIV/0!</b> - Division by zero */
-        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 "???";
-        }
-    }
+
+    // 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(ErrorCode.NULL); 
+    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(ErrorCode.DIV_0);
+    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(ErrorCode.VALUE);
+    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(ErrorCode.REF);
+    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(ErrorCode.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(ErrorCode.NUM);
+    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(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();
     }
index 01af4e8436611b49ccf83a7863834ece594a6899..27a9c6a62720b92ae56e90a80b9cfe74fce8956d 100644 (file)
 * 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 &lt; amolweb at ya hoo dot com &gt;
  *  
  */
-public class StringEval implements StringValueEval {
+public final class StringEval implements StringValueEval {
 
     public static final StringEval EMPTY_INSTANCE = new StringEval("");
     
-    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();
+    }
 }
index b692f01ea2da2eabb4cd4b622c885e6f7c51a0ed..46c12236b9b3b51a446d4b93bad65a73477efaa8 100644 (file)
 * 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 <code>null</code>, possibly empty string.
+     */
+    String getStringValue();
 }
index 8bac3d0c02029f1c2c4eeaca1354c7506eb48db3..40ed1da4903ab12507c2a8e69ffe52522a20dedb 100644 (file)
-/*
-* 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.<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);
+       }
 }
index f98ccca7e3a02826a34c3f930be9ed9d9c96950d..be1d0d0f940f47a32d55a69cd43231d0fcb1a297 100644 (file)
@@ -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.<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());
+       }
 }
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 (file)
index 0000000..66123b2
--- /dev/null
@@ -0,0 +1,530 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hssf.record.formula.functions;
+
+import org.apache.poi.hssf.record.formula.AreaPtg;
+import org.apache.poi.hssf.record.formula.eval.Area2DEval;
+import org.apache.poi.hssf.record.formula.eval.AreaEval;
+import org.apache.poi.hssf.record.formula.eval.BlankEval;
+import org.apache.poi.hssf.record.formula.eval.BoolEval;
+import org.apache.poi.hssf.record.formula.eval.ErrorEval;
+import org.apache.poi.hssf.record.formula.eval.Eval;
+import org.apache.poi.hssf.record.formula.eval.EvaluationException;
+import org.apache.poi.hssf.record.formula.eval.NumberEval;
+import org.apache.poi.hssf.record.formula.eval.NumericValueEval;
+import org.apache.poi.hssf.record.formula.eval.OperandResolver;
+import org.apache.poi.hssf.record.formula.eval.RefEval;
+import org.apache.poi.hssf.record.formula.eval.StringEval;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+
+/**
+ * Common functionality used by VLOOKUP, HLOOKUP, LOOKUP and MATCH
+ * 
+ * @author Josh Micich
+ */
+final class LookupUtils {
+       
+       /**
+        * Represents a single row or column within an <tt>AreaEval</tt>.
+        */
+       public interface ValueVector {
+               ValueEval getItem(int index);
+               int getSize();
+       }
+       /**
+        * Enumeration to support <b>4</b> valued comparison results.<p/>
+        * Excel lookup functions have complex behaviour in the case where the lookup array has mixed 
+        * types, and/or is unordered.  Contrary to suggestions in some Excel documentation, there
+        * does not appear to be a universal ordering across types.  The binary search algorithm used
+        * changes behaviour when the evaluated 'mid' value has a different type to the lookup value.<p/>
+        * 
+        * A simple int might have done the same job, but there is risk in confusion with the well 
+        * known <tt>Comparable.compareTo()</tt> and <tt>Comparator.compare()</tt> which both use
+        * a ubiquitous 3 value result encoding.
+        */
+       public static final class CompareResult {
+               private final boolean _isTypeMismatch;
+               private final boolean _isLessThan;
+               private final boolean _isEqual;
+               private final boolean _isGreaterThan;
+
+               private CompareResult(boolean isTypeMismatch, int simpleCompareResult) {
+                       if(isTypeMismatch) {
+                               _isTypeMismatch = true;
+                               _isLessThan = false;
+                               _isEqual = false;
+                               _isGreaterThan = false;
+                       } else {
+                               _isTypeMismatch = false;
+                               _isLessThan = simpleCompareResult < 0;
+                               _isEqual = simpleCompareResult == 0;
+                               _isGreaterThan = simpleCompareResult > 0;
+                       }
+               }
+               public static final CompareResult TYPE_MISMATCH = new CompareResult(true, 0);
+               public static final CompareResult LESS_THAN = new CompareResult(false, -1);
+               public static final CompareResult EQUAL = new CompareResult(false, 0);
+               public static final CompareResult GREATER_THAN = new CompareResult(false, +1);
+               
+               public static final CompareResult valueOf(int simpleCompareResult) {
+                       if(simpleCompareResult < 0) {
+                               return LESS_THAN;
+                       }
+                       if(simpleCompareResult > 0) {
+                               return GREATER_THAN;
+                       }
+                       return EQUAL;
+               }
+               
+               public boolean isTypeMismatch() {
+                       return _isTypeMismatch;
+               }
+               public boolean isLessThan() {
+                       return _isLessThan;
+               }
+               public boolean isEqual() {
+                       return _isEqual;
+               }
+               public boolean isGreaterThan() {
+                       return _isGreaterThan;
+               }
+               public String toString() {
+                       StringBuffer sb = new StringBuffer(64);
+                       sb.append(getClass().getName()).append(" [");
+                       sb.append(formatAsString());
+                       sb.append("]");
+                       return sb.toString();
+               }
+
+               private String formatAsString() {
+                       if(_isTypeMismatch) {
+                               return "TYPE_MISMATCH";
+                       }
+                       if(_isLessThan) {
+                               return "LESS_THAN";
+                       }
+                       if(_isEqual) {
+                               return "EQUAL";
+                       }
+                       if(_isGreaterThan) {
+                               return "GREATER_THAN";
+                       }
+                       // toString must be reliable
+                       return "??error??";
+               }
+       }
+       
+       public interface LookupValueComparer {
+               /**
+                * @return one of 4 instances or <tt>CompareResult</tt>: <tt>LESS_THAN</tt>, <tt>EQUAL</tt>, 
+                * <tt>GREATER_THAN</tt> or <tt>TYPE_MISMATCH</tt>
+                */
+               CompareResult compareTo(ValueEval other);
+       }
+       
+       private static abstract class LookupValueComparerBase implements LookupValueComparer {
+               
+               private final Class _targetClass;
+               protected LookupValueComparerBase(ValueEval targetValue) {
+                       if(targetValue == null) {
+                               throw new RuntimeException("targetValue cannot be null");
+                       }
+                       _targetClass = targetValue.getClass();
+               }
+               public final CompareResult compareTo(ValueEval other) {
+                       if (other == null) {
+                               throw new RuntimeException("compare to value cannot be null");
+                       }
+                       if (_targetClass != other.getClass()) {
+                               return CompareResult.TYPE_MISMATCH;
+                       }
+                       if (_targetClass == StringEval.class) {
+                               
+                       }
+                       return compareSameType(other);
+               }
+               public String toString() {
+                       StringBuffer sb = new StringBuffer(64);
+                       sb.append(getClass().getName()).append(" [");
+                       sb.append(getValueAsString());
+                       sb.append("]");
+                       return sb.toString();
+               }
+               protected abstract CompareResult compareSameType(ValueEval other);
+               /** used only for debug purposes */
+               protected abstract String getValueAsString();
+       }
+       
+       private static final class StringLookupComparer extends LookupValueComparerBase {
+               private String _value;
+
+               protected StringLookupComparer(StringEval se) {
+                       super(se);
+                       _value = se.getStringValue();
+               }
+               protected CompareResult compareSameType(ValueEval other) {
+                       StringEval se = (StringEval) other;
+                       return CompareResult.valueOf(_value.compareToIgnoreCase(se.getStringValue()));
+               }
+               protected String getValueAsString() {
+                       return _value;
+               }
+       }
+       private static final class NumberLookupComparer extends LookupValueComparerBase {
+               private double _value;
+
+               protected NumberLookupComparer(NumberEval ne) {
+                       super(ne);
+                       _value = ne.getNumberValue();
+               }
+               protected CompareResult compareSameType(ValueEval other) {
+                       NumberEval ne = (NumberEval) other;
+                       return CompareResult.valueOf(Double.compare(_value, ne.getNumberValue()));
+               }
+               protected String getValueAsString() {
+                       return String.valueOf(_value);
+               }
+       }
+       private static final class BooleanLookupComparer extends LookupValueComparerBase {
+               private boolean _value;
+
+               protected BooleanLookupComparer(BoolEval be) {
+                       super(be);
+                       _value = be.getBooleanValue();
+               }
+               protected CompareResult compareSameType(ValueEval other) {
+                       BoolEval be = (BoolEval) other;
+                       boolean otherVal = be.getBooleanValue();
+                       if(_value == otherVal) {
+                               return CompareResult.EQUAL;
+                       }
+                       // TRUE > FALSE
+                       if(_value) {
+                               return CompareResult.GREATER_THAN;
+                       }
+                       return CompareResult.LESS_THAN;
+               }
+               protected String getValueAsString() {
+                       return String.valueOf(_value);
+               }
+       }
+       
+       /**
+        * Processes the third argument to VLOOKUP, or HLOOKUP (<b>col_index_num</b> 
+        * or <b>row_index_num</b> respectively).<br>
+        * Sample behaviour:
+        *    <table border="0" cellpadding="1" cellspacing="2" summary="Sample behaviour">
+        *      <tr><th>Input&nbsp;&nbsp;&nbsp;Return</th><th>Value&nbsp;&nbsp;</th><th>Thrown Error</th></tr>
+        *      <tr><td>5</td><td>4</td><td>&nbsp;</td></tr>
+        *      <tr><td>2.9</td><td>2</td><td>&nbsp;</td></tr>
+        *      <tr><td>"5"</td><td>4</td><td>&nbsp;</td></tr>
+        *      <tr><td>"2.18e1"</td><td>21</td><td>&nbsp;</td></tr>
+        *      <tr><td>"-$2"</td><td>-3</td><td>*</td></tr>
+        *      <tr><td>FALSE</td><td>-1</td><td>*</td></tr>
+        *      <tr><td>TRUE</td><td>0</td><td>&nbsp;</td></tr>
+        *      <tr><td>"TRUE"</td><td>&nbsp;</td><td>#REF!</td></tr>
+        *      <tr><td>"abc"</td><td>&nbsp;</td><td>#REF!</td></tr>
+        *      <tr><td>""</td><td>&nbsp;</td><td>#REF!</td></tr>
+        *      <tr><td>&lt;blank&gt;</td><td>&nbsp;</td><td>#VALUE!</td></tr>
+        *    </table><br/>
+        *    
+        *  * Note - out of range errors (both too high and too low) are handled by the caller. 
+        * @return column or row index as a zero-based value
+        * 
+        */
+       public static int resolveRowOrColIndexArg(ValueEval veRowColIndexArg) throws EvaluationException {
+               if(veRowColIndexArg == null) {
+                       throw new IllegalArgumentException("argument must not be null");
+               }
+               if(veRowColIndexArg instanceof BlankEval) {
+                       throw EvaluationException.invalidValue(); 
+               }
+               if(veRowColIndexArg instanceof StringEval) {
+                       StringEval se = (StringEval) veRowColIndexArg;
+                       String strVal = se.getStringValue();
+                       Double dVal = OperandResolver.parseDouble(strVal);
+                       if(dVal == null) {
+                               // String does not resolve to a number. Raise #VALUE! error.
+                               throw EvaluationException.invalidRef(); 
+                               // This includes text booleans "TRUE" and "FALSE".  They are not valid.
+                       }
+                       // else - numeric value parses OK
+               }
+               // actual BoolEval values get interpreted as FALSE->0 and TRUE->1
+               return OperandResolver.coerceValueToInt(veRowColIndexArg) - 1;
+       }
+       
+       
+       
+       /**
+        * The second argument (table_array) should be an area ref, but can actually be a cell ref, in
+        * which case it is interpreted as a 1x1 area ref.  Other scalar values cause #VALUE! error.
+        */
+       public static AreaEval resolveTableArrayArg(Eval eval) throws EvaluationException {
+               if (eval instanceof AreaEval) {
+                       return (AreaEval) eval;
+               }
+               
+               if(eval instanceof RefEval) {
+                       RefEval refEval = (RefEval) eval;
+                       // Make this cell ref look like a 1x1 area ref.
+                       
+                       // It doesn't matter if eval is a 2D or 3D ref, because that detail is never asked of AreaEval.
+                       // This code only requires the value array item. 
+                       // anything would be ok for rowIx and colIx, but may as well get it right.
+                       short rowIx = refEval.getRow();
+                       short colIx = refEval.getColumn();
+                       AreaPtg ap = new AreaPtg(rowIx, rowIx, colIx, colIx, false, false, false, false);
+                       ValueEval value = refEval.getInnerValueEval();
+                       return new Area2DEval(ap, new ValueEval[] { value, });
+               }
+               throw EvaluationException.invalidValue();
+       }
+       
+
+       /**
+        * Resolves the last (optional) parameter (<b>range_lookup</b>) to the VLOOKUP and HLOOKUP functions. 
+        * @param rangeLookupArg
+        * @param srcCellRow
+        * @param srcCellCol
+        * @return
+        * @throws EvaluationException
+        */
+       public static boolean resolveRangeLookupArg(Eval rangeLookupArg, int srcCellRow, short srcCellCol) throws EvaluationException {
+               if(rangeLookupArg == null) {
+                       // range_lookup arg not provided
+                       return true; // default is TRUE
+               }
+               ValueEval valEval = OperandResolver.getSingleValue(rangeLookupArg, srcCellRow, srcCellCol);
+               if(valEval instanceof BlankEval) {
+                       // Tricky:
+                       // fourth arg supplied but evaluates to blank
+                       // this does not get the default value
+                       return false;
+               }
+               if(valEval instanceof BoolEval) {
+                       // Happy day flow 
+                       BoolEval boolEval = (BoolEval) valEval;
+                       return boolEval.getBooleanValue();
+               }
+
+               if (valEval instanceof StringEval) {
+                       String stringValue = ((StringEval) valEval).getStringValue();
+                       if(stringValue.length() < 1) {
+                               // More trickiness:
+                               // Empty string is not the same as BlankEval.  It causes #VALUE! error 
+                               throw EvaluationException.invalidValue();
+                       }
+                       // TODO move parseBoolean to OperandResolver
+                       Boolean b = Countif.parseBoolean(stringValue);
+                       if(b != null) {
+                               // string converted to boolean OK
+                               return b.booleanValue();
+                       }
+                       // Even more trickiness:
+                       // Note - even if the StringEval represents a number value (for example "1"), 
+                       // Excel does not resolve it to a boolean.  
+                       throw EvaluationException.invalidValue();
+                       // This is in contrast to the code below,, where NumberEvals values (for 
+                       // example 0.01) *do* resolve to equivalent boolean values.
+               }
+               if (valEval instanceof NumericValueEval) {
+                       NumericValueEval nve = (NumericValueEval) valEval;
+                       // zero is FALSE, everything else is TRUE
+                       return 0.0 != nve.getNumberValue();
+               }
+               throw new RuntimeException("Unexpected eval type (" + valEval.getClass().getName() + ")");
+       }
+       
+       public static int lookupIndexOfValue(ValueEval lookupValue, ValueVector vector, boolean isRangeLookup) throws EvaluationException {
+               LookupValueComparer lookupComparer = createLookupComparer(lookupValue);
+               int result;
+               if(isRangeLookup) {
+                       result = performBinarySearch(vector, lookupComparer);
+               } else {
+                       result = lookupIndexOfExactValue(lookupComparer, vector);
+               }
+               if(result < 0) {
+                       throw new EvaluationException(ErrorEval.NA);
+               }
+               return result;
+       }
+       
+       
+       /**
+        * Finds first (lowest index) exact occurrence of specified value.
+        * @param lookupValue the value to be found in column or row vector
+        * @param vector the values to be searched. For VLOOKUP this is the first column of the 
+        *      tableArray. For HLOOKUP this is the first row of the tableArray. 
+        * @return zero based index into the vector, -1 if value cannot be found
+        */
+       private static int lookupIndexOfExactValue(LookupValueComparer lookupComparer, ValueVector vector) {
+
+               // find first occurrence of lookup value
+               int size = vector.getSize();
+               for (int i = 0; i < size; i++) {
+                       if(lookupComparer.compareTo(vector.getItem(i)).isEqual()) {
+                               return i;
+                       }
+               }
+               return -1;
+       }
+
+       
+       /**
+        * Encapsulates some standard binary search functionality so the unusual Excel behaviour can
+        * be clearly distinguished. 
+        */
+       private static final class BinarySearchIndexes {
+
+               private int _lowIx;
+               private int _highIx;
+
+               public BinarySearchIndexes(int highIx) {
+                       _lowIx = -1;
+                       _highIx = highIx;
+               }
+
+               /**
+                * @return -1 if the search range is empty
+                */
+               public int getMidIx() {
+                       int ixDiff = _highIx - _lowIx;
+                       if(ixDiff < 2) {
+                               return -1;
+                       }
+                       return _lowIx + (ixDiff / 2);
+               }
+
+               public int getLowIx() {
+                       return _lowIx;
+               }
+               public int getHighIx() {
+                       return _highIx;
+               }
+               public void narrowSearch(int midIx, boolean isLessThan) {
+                       if(isLessThan) {
+                               _highIx = midIx;
+                       } else {
+                               _lowIx = midIx;
+                       }
+               }
+       }
+       /**
+        * Excel has funny behaviour when the some elements in the search vector are the wrong type.
+        * 
+        */
+       private static int performBinarySearch(ValueVector vector, LookupValueComparer lookupComparer) {
+               // both low and high indexes point to values assumed too low and too high.
+               BinarySearchIndexes bsi = new BinarySearchIndexes(vector.getSize());
+
+               while(true) {
+                       int midIx = bsi.getMidIx();
+                       
+                       if(midIx < 0) {
+                               return bsi.getLowIx();
+                       }
+                       CompareResult cr = lookupComparer.compareTo(vector.getItem(midIx));
+                       if(cr.isTypeMismatch()) {
+                               int newMidIx = handleMidValueTypeMismatch(lookupComparer, vector, bsi, midIx);
+                               if(newMidIx < 0) {
+                                       continue;
+                               }
+                               midIx = newMidIx;
+                               cr = lookupComparer.compareTo(vector.getItem(midIx));
+                       }
+                       if(cr.isEqual()) {
+                               return findLastIndexInRunOfEqualValues(lookupComparer, vector, midIx, bsi.getHighIx());
+                       }
+                       bsi.narrowSearch(midIx, cr.isLessThan());
+               }
+       }
+       /**
+        * Excel seems to handle mismatched types initially by just stepping 'mid' ix forward to the 
+        * first compatible value.
+        * @param midIx 'mid' index (value which has the wrong type)
+        * @return usually -1, signifying that the BinarySearchIndex has been narrowed to the new mid 
+        * index.  Zero or greater signifies that an exact match for the lookup value was found
+        */
+       private static int handleMidValueTypeMismatch(LookupValueComparer lookupComparer, ValueVector vector,
+                       BinarySearchIndexes bsi, int midIx) {
+               int newMid = midIx;
+               int highIx = bsi.getHighIx();
+               
+               while(true) {
+                       newMid++;
+                       if(newMid == highIx) {
+                               // every element from midIx to highIx was the wrong type
+                               // move highIx down to the low end of the mid values
+                               bsi.narrowSearch(midIx, true);
+                               return -1;
+                       }
+                       CompareResult cr = lookupComparer.compareTo(vector.getItem(newMid));
+                       if(cr.isLessThan() && newMid == highIx-1) {
+                               // move highIx down to the low end of the mid values
+                               bsi.narrowSearch(midIx, true);
+                               return -1;
+                               // but only when "newMid == highIx-1"? slightly weird.
+                               // It would seem more efficient to always do this.
+                       }
+                       if(cr.isTypeMismatch()) {
+                               // keep stepping over values until the right type is found
+                               continue;
+                       }
+                       if(cr.isEqual()) {
+                               return newMid;
+                       }
+                       // Note - if moving highIx down (due to lookup<vector[newMid]),
+                       // this execution path only moves highIx it down as far as newMid, not midIx,
+                       // which would be more efficient.
+                       bsi.narrowSearch(newMid, cr.isLessThan());
+                       return -1;
+               }
+       }
+       /**
+        * Once the binary search has found a single match, (V/H)LOOKUP steps one by one over subsequent
+        * values to choose the last matching item.
+        */
+       private static int findLastIndexInRunOfEqualValues(LookupValueComparer lookupComparer, ValueVector vector,
+                               int firstFoundIndex, int maxIx) {
+               for(int i=firstFoundIndex+1; i<maxIx; i++) {
+                       if(!lookupComparer.compareTo(vector.getItem(i)).isEqual()) {
+                               return i-1;
+                       }
+               }
+               return maxIx - 1;
+       }
+
+       public static LookupValueComparer createLookupComparer(ValueEval lookupValue) throws EvaluationException {
+               
+               if (lookupValue instanceof BlankEval) {
+                       // blank eval can never be found in a lookup array 
+                       throw new EvaluationException(ErrorEval.NA);
+               }
+               if (lookupValue instanceof StringEval) {
+                       return new StringLookupComparer((StringEval) lookupValue);
+               }
+               if (lookupValue instanceof NumberEval) {
+                       return new NumberLookupComparer((NumberEval) lookupValue);
+               }
+               if (lookupValue instanceof BoolEval) {
+                       return new BooleanLookupComparer((BoolEval) lookupValue);
+               }
+               throw new IllegalArgumentException("Bad lookup value type (" + lookupValue.getClass().getName() + ")");
+       }
+}
index a3ad49b194a15d70b89db3e6f6f8713fc94c646e..a2a12cdba8cf656144a2c2d72b36ffc47364d040 100644 (file)
 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.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;
+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/>
@@ -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();
-               }
-       }
-
-       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);
+               } catch (EvaluationException e) {
+                       return e.getErrorEval();
                }
-               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;
+                               }
+                               if(!cmp.isLessThan()) {
+                                       return i;
                                }
-                       } else {
-                               // find smallest greater than or equal to
-                               for (int i = 0; i<lookupRange.length; i++) {
-                                       ValueEval lri = lookupRange[i];
-                                       if(lri.getClass() != lookupValue.getClass()) {
-                                               continue;
-                                       }
-                                       int cmp = compareValues(lookupValue, lri);
-                                       if(cmp == 0) {
-                                               return i;
-                                       }
-                                       if(cmp > 0) {
-                                               if(i<1) {
-                                                       throw new EvalEx(ErrorEval.NA);
-                                               }
-                                               return i-1;
-                                       }
+                       }
+                       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; i<lookupRange.length; i++) {
+                       CompareResult cmp = lookupComparer.compareTo(lookupRange[i]);
+                       if(cmp.isEqual()) {
+                               return i;
+                       }
+                       if(cmp.isGreaterThan()) {
+                               if(i<1) {
+                                       throw new EvaluationException(ErrorEval.NA);
                                }
-                               
+                               return i-1;
                        }
                }
 
-               throw new EvalEx(ErrorEval.NA);
+               throw new EvaluationException(ErrorEval.NA);
        }
 
-
-       /**
-        * This method can only compare a pair of <tt>NumericValueEval</tt>s, <tt>StringEval</tt>s
-        * or <tt>BoolEval</tt>s
-        * @return negative for a&lt;b, positive for a&gt;b and 0 for a = b
-        */
-       private static int compareValues(ValueEval a, ValueEval b) {
-               if (a instanceof StringEval) {
-                       StringEval sa = (StringEval) a;
-                       StringEval sb = (StringEval) b;
-                       return sa.getStringValue().compareToIgnoreCase(sb.getStringValue());
-               }
-               if (a instanceof NumericValueEval) {
-                       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;
+       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 -1;
+                       
+               }
+               return LookupUtils.createLookupComparer(lookupValue);
+       }
+
+       private static boolean isLookupValueWild(String stringValue) {
+               if(stringValue.indexOf('?') >=0 || stringValue.indexOf('*') >=0) {
+                       return true;
                }
-               throw new RuntimeException("bad eval type (" + a.getClass().getName() + ")");
+               return false;
        }
 }
index ad8b88daf945de8399e2fbda95d115e4d26eedef..7d27491df1590a1089c13ec2750d41abad654fb5 100644 (file)
-/*
-* 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.<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);
+       }
 }
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 (file)
index 0000000..071ca0f
--- /dev/null
@@ -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) + ")");
+       }
+}
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 (executable)
index 0000000..f4b35fb
Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls differ