123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- /* ====================================================================
- 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.ss.formula.functions;
-
- import org.apache.poi.ss.formula.eval.BlankEval;
- import org.apache.poi.ss.formula.eval.BoolEval;
- import org.apache.poi.ss.formula.eval.ErrorEval;
- import org.apache.poi.ss.formula.eval.EvaluationException;
- import org.apache.poi.ss.formula.eval.NumberEval;
- import org.apache.poi.ss.formula.eval.NumericValueEval;
- import org.apache.poi.ss.formula.eval.OperandResolver;
- import org.apache.poi.ss.formula.eval.RefEval;
- import org.apache.poi.ss.formula.eval.StringEval;
- import org.apache.poi.ss.formula.eval.ValueEval;
- import org.apache.poi.ss.formula.TwoDEval;
-
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
-
- /**
- * Common functionality used by VLOOKUP, HLOOKUP, LOOKUP and MATCH
- *
- * @author Josh Micich
- * @author Cedric Walter at innoveo.com
- */
- final class LookupUtils {
-
- /**
- * Represents a single row or column within an <tt>AreaEval</tt>.
- */
- public interface ValueVector {
- ValueEval getItem(int index);
- int getSize();
- }
-
-
- private static final class RowVector implements ValueVector {
-
- private final TwoDEval _tableArray;
- private final int _size;
- private final int _rowIndex;
-
- public RowVector(TwoDEval tableArray, int rowIndex) {
- _rowIndex = rowIndex;
- int lastRowIx = tableArray.getHeight() - 1;
- if(rowIndex < 0 || rowIndex > lastRowIx) {
- throw new IllegalArgumentException("Specified row index (" + rowIndex
- + ") is outside the allowed range (0.." + lastRowIx + ")");
- }
- _tableArray = tableArray;
- _size = tableArray.getWidth();
- }
-
- public ValueEval getItem(int index) {
- if(index > _size) {
- throw new ArrayIndexOutOfBoundsException("Specified index (" + index
- + ") is outside the allowed range (0.." + (_size-1) + ")");
- }
- return _tableArray.getValue(_rowIndex, index);
- }
- public int getSize() {
- return _size;
- }
- }
-
- private static final class ColumnVector implements ValueVector {
-
- private final TwoDEval _tableArray;
- private final int _size;
- private final int _columnIndex;
-
- public ColumnVector(TwoDEval tableArray, int columnIndex) {
- _columnIndex = columnIndex;
- int lastColIx = tableArray.getWidth()-1;
- if(columnIndex < 0 || columnIndex > lastColIx) {
- throw new IllegalArgumentException("Specified column index (" + columnIndex
- + ") is outside the allowed range (0.." + lastColIx + ")");
- }
- _tableArray = tableArray;
- _size = _tableArray.getHeight();
- }
-
- public ValueEval getItem(int index) {
- if(index > _size) {
- throw new ArrayIndexOutOfBoundsException("Specified index (" + index
- + ") is outside the allowed range (0.." + (_size-1) + ")");
- }
- return _tableArray.getValue(index, _columnIndex);
- }
- public int getSize() {
- return _size;
- }
- }
-
- public static ValueVector createRowVector(TwoDEval tableArray, int relativeRowIndex) {
- return new RowVector(tableArray, relativeRowIndex);
- }
- public static ValueVector createColumnVector(TwoDEval tableArray, int relativeColumnIndex) {
- return new ColumnVector(tableArray, relativeColumnIndex);
- }
- /**
- * @return <code>null</code> if the supplied area is neither a single row nor a single colum
- */
- public static ValueVector createVector(TwoDEval ae) {
- if (ae.isColumn()) {
- return createColumnVector(ae, 0);
- }
- if (ae.isRow()) {
- return createRowVector(ae, 0);
- }
- return null;
- }
-
- /**
- * 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 static final CompareResult valueOf(boolean matches) {
- if(matches) {
- return EQUAL ;
- }
- return LESS_THAN;
- }
-
-
- 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<? extends ValueEval> _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;
- }
- 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;
- private final Pattern _wildCardPattern;
- private boolean _matchExact;
- private boolean _isMatchFunction;
-
- protected StringLookupComparer(StringEval se, boolean matchExact, boolean isMatchFunction) {
- super(se);
- _value = se.getStringValue();
- _wildCardPattern = Countif.StringMatcher.getWildCardPattern(_value);
- _matchExact = matchExact;
- _isMatchFunction = isMatchFunction;
- }
-
- protected CompareResult compareSameType(ValueEval other) {
- StringEval se = (StringEval) other;
-
- String stringValue = se.getStringValue();
- if (_wildCardPattern != null) {
- Matcher matcher = _wildCardPattern.matcher(stringValue);
- boolean matches = matcher.matches();
-
- if (_isMatchFunction ||
- !_matchExact) {
- return CompareResult.valueOf(matches);
- }
- }
-
- return CompareResult.valueOf(_value.compareToIgnoreCase(stringValue));
- }
- protected String getValueAsString() {
- return _value;
- }
- }
- private static final class NumberLookupComparer extends LookupValueComparerBase {
- private double _value;
-
- protected NumberLookupComparer(NumberEval ne) {
- super(ne);
- _value = ne.getNumberValue();
- }
- protected CompareResult compareSameType(ValueEval other) {
- NumberEval ne = (NumberEval) other;
- return CompareResult.valueOf(Double.compare(_value, ne.getNumberValue()));
- }
- protected String getValueAsString() {
- return String.valueOf(_value);
- }
- }
- private static final class BooleanLookupComparer extends LookupValueComparerBase {
- private boolean _value;
-
- protected BooleanLookupComparer(BoolEval be) {
- super(be);
- _value = be.getBooleanValue();
- }
- protected CompareResult compareSameType(ValueEval other) {
- BoolEval be = (BoolEval) other;
- boolean otherVal = be.getBooleanValue();
- if(_value == otherVal) {
- return CompareResult.EQUAL;
- }
- // TRUE > FALSE
- if(_value) {
- return CompareResult.GREATER_THAN;
- }
- return CompareResult.LESS_THAN;
- }
- protected String getValueAsString() {
- return String.valueOf(_value);
- }
- }
-
- /**
- * Processes the third argument to VLOOKUP, or HLOOKUP (<b>col_index_num</b>
- * or <b>row_index_num</b> respectively).<br>
- * Sample behaviour:
- * <table border="0" cellpadding="1" cellspacing="2" summary="Sample behaviour">
- * <tr><th>Input Return</th><th>Value </th><th>Thrown Error</th></tr>
- * <tr><td>5</td><td>4</td><td> </td></tr>
- * <tr><td>2.9</td><td>2</td><td> </td></tr>
- * <tr><td>"5"</td><td>4</td><td> </td></tr>
- * <tr><td>"2.18e1"</td><td>21</td><td> </td></tr>
- * <tr><td>"-$2"</td><td>-3</td><td>*</td></tr>
- * <tr><td>FALSE</td><td>-1</td><td>*</td></tr>
- * <tr><td>TRUE</td><td>0</td><td> </td></tr>
- * <tr><td>"TRUE"</td><td> </td><td>#REF!</td></tr>
- * <tr><td>"abc"</td><td> </td><td>#REF!</td></tr>
- * <tr><td>""</td><td> </td><td>#REF!</td></tr>
- * <tr><td><blank></td><td> </td><td>#VALUE!</td></tr>
- * </table><br/>
- *
- * Note - out of range errors (result index too high) are handled by the caller.
- * @return column or row index as a zero-based value, never negative.
- * @throws EvaluationException when the specified arg cannot be coerced to a non-negative integer
- */
- public static int resolveRowOrColIndexArg(ValueEval rowColIndexArg, int srcCellRow, int srcCellCol) throws EvaluationException {
- if(rowColIndexArg == null) {
- throw new IllegalArgumentException("argument must not be null");
- }
-
- ValueEval veRowColIndexArg;
- try {
- veRowColIndexArg = OperandResolver.getSingleValue(rowColIndexArg, srcCellRow, (short)srcCellCol);
- } catch (EvaluationException e) {
- // All errors get translated to #REF!
- throw EvaluationException.invalidRef();
- }
- int oneBasedIndex;
- 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 #REF! 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
- oneBasedIndex = OperandResolver.coerceValueToInt(veRowColIndexArg);
- if (oneBasedIndex < 1) {
- // note this is asymmetric with the errors when the index is too large (#REF!)
- throw EvaluationException.invalidValue();
- }
- return oneBasedIndex - 1; // convert to zero based
- }
-
-
-
- /**
- * 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 TwoDEval resolveTableArrayArg(ValueEval eval) throws EvaluationException {
- if (eval instanceof TwoDEval) {
- return (TwoDEval) 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.
- return refEval.offset(0, 0, 0, 0);
- }
- throw EvaluationException.invalidValue();
- }
-
-
- /**
- * Resolves the last (optional) parameter (<b>range_lookup</b>) to the VLOOKUP and HLOOKUP functions.
- * @param rangeLookupArg must not be <code>null</code>
- */
- public static boolean resolveRangeLookupArg(ValueEval rangeLookupArg, int srcCellRow, int srcCellCol) throws EvaluationException {
-
- 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, isRangeLookup, false);
- 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 lookupComparer 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, boolean matchExact, boolean isMatchFunction) {
-
- if (lookupValue == BlankEval.instance) {
- // blank eval translates to zero
- // Note - a blank eval in the lookup column/row never matches anything
- // empty string in the lookup column/row can only be matched by explicit empty string
- return new NumberLookupComparer(NumberEval.ZERO);
- }
- if (lookupValue instanceof StringEval) {
- //TODO eventually here return a WildcardStringLookupComparer
- return new StringLookupComparer((StringEval) lookupValue, matchExact, isMatchFunction);
- }
- 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() + ")");
- }
- }
|