You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LookupUtils.java 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.formula.functions;
  16. import java.util.HashMap;
  17. import java.util.HashSet;
  18. import java.util.Iterator;
  19. import java.util.Map;
  20. import java.util.Spliterator;
  21. import java.util.Spliterators;
  22. import java.util.regex.Matcher;
  23. import java.util.regex.Pattern;
  24. import org.apache.poi.ss.formula.TwoDEval;
  25. import org.apache.poi.ss.formula.eval.BlankEval;
  26. import org.apache.poi.ss.formula.eval.BoolEval;
  27. import org.apache.poi.ss.formula.eval.ErrorEval;
  28. import org.apache.poi.ss.formula.eval.EvaluationException;
  29. import org.apache.poi.ss.formula.eval.MissingArgEval;
  30. import org.apache.poi.ss.formula.eval.NumberEval;
  31. import org.apache.poi.ss.formula.eval.NumericValueEval;
  32. import org.apache.poi.ss.formula.eval.OperandResolver;
  33. import org.apache.poi.ss.formula.eval.RefEval;
  34. import org.apache.poi.ss.formula.eval.StringEval;
  35. import org.apache.poi.ss.formula.eval.ValueEval;
  36. import org.apache.poi.util.Internal;
  37. /**
  38. * Common functionality used by VLOOKUP, HLOOKUP, LOOKUP and MATCH
  39. */
  40. @Internal
  41. public final class LookupUtils {
  42. public enum MatchMode {
  43. ExactMatch(0),
  44. ExactMatchFallbackToSmallerValue(-1),
  45. ExactMatchFallbackToLargerValue(1),
  46. WildcardMatch(2);
  47. private final int intValue;
  48. MatchMode(final int intValue) {
  49. this.intValue = intValue;
  50. }
  51. public int getIntValue() { return intValue; }
  52. }
  53. public enum SearchMode {
  54. IterateForward(1),
  55. IterateBackward(-1),
  56. BinarySearchForward(2),
  57. BinarySearchBackward(-2);
  58. private final int intValue;
  59. SearchMode(final int intValue) {
  60. this.intValue = intValue;
  61. }
  62. public int getIntValue() { return intValue; }
  63. }
  64. private static Map<Integer, MatchMode> matchModeMap = new HashMap<>();
  65. private static Map<Integer, SearchMode> searchModeMap = new HashMap<>();
  66. static {
  67. for (MatchMode mode : MatchMode.values()) {
  68. matchModeMap.put(mode.getIntValue(), mode);
  69. }
  70. for (SearchMode mode : SearchMode.values()) {
  71. searchModeMap.put(mode.getIntValue(), mode);
  72. }
  73. }
  74. public static MatchMode matchMode(int m) {
  75. MatchMode mode = matchModeMap.get(m);
  76. if (mode == null) {
  77. throw new IllegalArgumentException("unknown match mode " + m);
  78. }
  79. return mode;
  80. }
  81. public static SearchMode searchMode(int s) {
  82. SearchMode mode = searchModeMap.get(s);
  83. if (mode == null) {
  84. throw new IllegalArgumentException("unknown search mode " + s);
  85. }
  86. return mode;
  87. }
  88. /**
  89. * Represents a single row or column within an {@code AreaEval}.
  90. */
  91. public interface ValueVector {
  92. ValueEval getItem(int index);
  93. int getSize();
  94. default Iterator<Integer> indexIterator() {
  95. return new Iterator<Integer>() {
  96. int pos = 0;
  97. @Override
  98. public boolean hasNext() {
  99. return pos < getSize();
  100. }
  101. @Override
  102. public Integer next() {
  103. return pos++;
  104. }
  105. };
  106. }
  107. /**
  108. * @since POI 5.2.0
  109. */
  110. default Spliterator<Integer> indexSpliterator() {
  111. return Spliterators.spliterator(indexIterator(), getSize(), 0);
  112. }
  113. default Iterator<Integer> reverseIndexIterator() {
  114. return new Iterator<Integer>() {
  115. int pos = getSize() - 1;
  116. @Override
  117. public boolean hasNext() {
  118. return pos >= 0;
  119. }
  120. @Override
  121. public Integer next() {
  122. return pos--;
  123. }
  124. };
  125. }
  126. /**
  127. * @since POI 5.2.0
  128. */
  129. default Spliterator<Integer> reverseIndexSpliterator() {
  130. return Spliterators.spliterator(reverseIndexIterator(), getSize(), 0);
  131. }
  132. }
  133. private static final class RowVector implements ValueVector {
  134. private final TwoDEval _tableArray;
  135. private final int _size;
  136. private final int _rowIndex;
  137. public RowVector(TwoDEval tableArray, int rowIndex) {
  138. _rowIndex = rowIndex;
  139. int lastRowIx = tableArray.getHeight() - 1;
  140. if(rowIndex < 0 || rowIndex > lastRowIx) {
  141. throw new IllegalArgumentException("Specified row index (" + rowIndex
  142. + ") is outside the allowed range (0.." + lastRowIx + ")");
  143. }
  144. _tableArray = tableArray;
  145. _size = tableArray.getWidth();
  146. }
  147. @Override
  148. public ValueEval getItem(int index) {
  149. if(index > _size) {
  150. throw new ArrayIndexOutOfBoundsException("Specified index (" + index
  151. + ") is outside the allowed range (0.." + (_size-1) + ")");
  152. }
  153. return _tableArray.getValue(_rowIndex, index);
  154. }
  155. @Override
  156. public int getSize() {
  157. return _size;
  158. }
  159. }
  160. private static final class ColumnVector implements ValueVector {
  161. private final TwoDEval _tableArray;
  162. private final int _size;
  163. private final int _columnIndex;
  164. public ColumnVector(TwoDEval tableArray, int columnIndex) {
  165. _columnIndex = columnIndex;
  166. int lastColIx = tableArray.getWidth()-1;
  167. if(columnIndex < 0 || columnIndex > lastColIx) {
  168. throw new IllegalArgumentException("Specified column index (" + columnIndex
  169. + ") is outside the allowed range (0.." + lastColIx + ")");
  170. }
  171. _tableArray = tableArray;
  172. _size = _tableArray.getHeight();
  173. }
  174. @Override
  175. public ValueEval getItem(int index) {
  176. if(index > _size) {
  177. throw new ArrayIndexOutOfBoundsException("Specified index (" + index
  178. + ") is outside the allowed range (0.." + (_size-1) + ")");
  179. }
  180. return _tableArray.getValue(index, _columnIndex);
  181. }
  182. @Override
  183. public int getSize() {
  184. return _size;
  185. }
  186. }
  187. private static final class SheetVector implements ValueVector {
  188. private final RefEval _re;
  189. private final int _size;
  190. public SheetVector(RefEval re) {
  191. _size = re.getNumberOfSheets();
  192. _re = re;
  193. }
  194. @Override
  195. public ValueEval getItem(int index) {
  196. if(index >= _size) {
  197. throw new ArrayIndexOutOfBoundsException("Specified index (" + index
  198. + ") is outside the allowed range (0.." + (_size-1) + ")");
  199. }
  200. int sheetIndex = _re.getFirstSheetIndex() + index;
  201. return _re.getInnerValueEval(sheetIndex);
  202. }
  203. @Override
  204. public int getSize() {
  205. return _size;
  206. }
  207. }
  208. public static ValueVector createRowVector(TwoDEval tableArray, int relativeRowIndex) {
  209. return new RowVector(tableArray, relativeRowIndex);
  210. }
  211. public static ValueVector createColumnVector(TwoDEval tableArray, int relativeColumnIndex) {
  212. return new ColumnVector(tableArray, relativeColumnIndex);
  213. }
  214. /**
  215. * @return {@code null} if the supplied area is neither a single row nor a single column
  216. */
  217. public static ValueVector createVector(TwoDEval ae) {
  218. if (ae.isColumn()) {
  219. return createColumnVector(ae, 0);
  220. }
  221. if (ae.isRow()) {
  222. return createRowVector(ae, 0);
  223. }
  224. return null;
  225. }
  226. public static ValueVector createVector(RefEval re) {
  227. return new SheetVector(re);
  228. }
  229. /**
  230. * Enumeration to support <b>4</b> valued comparison results.<p>
  231. * Excel lookup functions have complex behaviour in the case where the lookup array has mixed
  232. * types, and/or is unordered. Contrary to suggestions in some Excel documentation, there
  233. * does not appear to be a universal ordering across types. The binary search algorithm used
  234. * changes behaviour when the evaluated 'mid' value has a different type to the lookup value.<p>
  235. *
  236. * A simple int might have done the same job, but there is risk in confusion with the well
  237. * known {@code Comparable.compareTo()} and {@code Comparator.compare()} which both use
  238. * a ubiquitous 3 value result encoding.
  239. */
  240. public static final class CompareResult {
  241. private final boolean _isTypeMismatch;
  242. private final boolean _isLessThan;
  243. private final boolean _isEqual;
  244. private final boolean _isGreaterThan;
  245. private CompareResult(boolean isTypeMismatch, int simpleCompareResult) {
  246. if(isTypeMismatch) {
  247. _isTypeMismatch = true;
  248. _isLessThan = false;
  249. _isEqual = false;
  250. _isGreaterThan = false;
  251. } else {
  252. _isTypeMismatch = false;
  253. _isLessThan = simpleCompareResult < 0;
  254. _isEqual = simpleCompareResult == 0;
  255. _isGreaterThan = simpleCompareResult > 0;
  256. }
  257. }
  258. public static final CompareResult TYPE_MISMATCH = new CompareResult(true, 0);
  259. public static final CompareResult LESS_THAN = new CompareResult(false, -1);
  260. public static final CompareResult EQUAL = new CompareResult(false, 0);
  261. public static final CompareResult GREATER_THAN = new CompareResult(false, +1);
  262. public static CompareResult valueOf(int simpleCompareResult) {
  263. if(simpleCompareResult < 0) {
  264. return LESS_THAN;
  265. }
  266. if(simpleCompareResult > 0) {
  267. return GREATER_THAN;
  268. }
  269. return EQUAL;
  270. }
  271. public static CompareResult valueOf(boolean matches) {
  272. if(matches) {
  273. return EQUAL ;
  274. }
  275. return LESS_THAN;
  276. }
  277. public boolean isTypeMismatch() {
  278. return _isTypeMismatch;
  279. }
  280. public boolean isLessThan() {
  281. return _isLessThan;
  282. }
  283. public boolean isEqual() {
  284. return _isEqual;
  285. }
  286. public boolean isGreaterThan() {
  287. return _isGreaterThan;
  288. }
  289. public String toString() {
  290. return getClass().getName() + " [" +
  291. formatAsString() +
  292. "]";
  293. }
  294. private String formatAsString() {
  295. if(_isTypeMismatch) {
  296. return "TYPE_MISMATCH";
  297. }
  298. if(_isLessThan) {
  299. return "LESS_THAN";
  300. }
  301. if(_isEqual) {
  302. return "EQUAL";
  303. }
  304. if(_isGreaterThan) {
  305. return "GREATER_THAN";
  306. }
  307. // toString must be reliable
  308. return "??error??";
  309. }
  310. }
  311. public interface LookupValueComparer {
  312. /**
  313. * @return one of 4 instances or {@code CompareResult}: {@code LESS_THAN}, {@code EQUAL},
  314. * {@code GREATER_THAN} or {@code TYPE_MISMATCH}
  315. */
  316. CompareResult compareTo(ValueEval other);
  317. }
  318. private static abstract class LookupValueComparerBase implements LookupValueComparer {
  319. private final Class<? extends ValueEval> _targetClass;
  320. protected LookupValueComparerBase(ValueEval targetValue) {
  321. if(targetValue == null) {
  322. throw new RuntimeException("targetValue cannot be null");
  323. }
  324. _targetClass = targetValue.getClass();
  325. }
  326. @Override
  327. public final CompareResult compareTo(ValueEval other) {
  328. if (other == null) {
  329. throw new RuntimeException("compare to value cannot be null");
  330. }
  331. if (_targetClass != other.getClass()) {
  332. return CompareResult.TYPE_MISMATCH;
  333. }
  334. return compareSameType(other);
  335. }
  336. public String toString() {
  337. return getClass().getName() + " [" +
  338. getValueAsString() +
  339. "]";
  340. }
  341. protected abstract CompareResult compareSameType(ValueEval other);
  342. /** used only for debug purposes */
  343. protected abstract String getValueAsString();
  344. }
  345. private static class StringLookupComparer extends LookupValueComparerBase {
  346. protected final String _value;
  347. protected final Pattern _wildCardPattern;
  348. protected final boolean _matchExact;
  349. protected final boolean _isMatchFunction;
  350. protected StringLookupComparer(StringEval se, boolean matchExact, boolean isMatchFunction) {
  351. super(se);
  352. _value = se.getStringValue();
  353. _wildCardPattern = Countif.StringMatcher.getWildCardPattern(_value);
  354. _matchExact = matchExact;
  355. _isMatchFunction = isMatchFunction;
  356. }
  357. protected String convertToString(ValueEval other) {
  358. StringEval se = (StringEval) other;
  359. return se.getStringValue();
  360. }
  361. @Override
  362. protected CompareResult compareSameType(ValueEval other) {
  363. String stringValue = convertToString(other);
  364. if (_wildCardPattern != null && (_isMatchFunction || !_matchExact)) {
  365. Matcher matcher = _wildCardPattern.matcher(stringValue);
  366. boolean matches = matcher.matches();
  367. return CompareResult.valueOf(matches);
  368. }
  369. return CompareResult.valueOf(_value.compareToIgnoreCase(stringValue));
  370. }
  371. @Override
  372. protected String getValueAsString() {
  373. return _value;
  374. }
  375. }
  376. private static final class TolerantStringLookupComparer extends StringLookupComparer {
  377. static StringEval convertToStringEval(ValueEval eval) {
  378. if (eval instanceof StringEval) {
  379. return (StringEval)eval;
  380. }
  381. String sv = OperandResolver.coerceValueToString(eval);
  382. return new StringEval(sv);
  383. }
  384. protected TolerantStringLookupComparer(ValueEval eval, boolean matchExact, boolean isMatchFunction) {
  385. super(convertToStringEval(eval), matchExact, isMatchFunction);
  386. }
  387. @Override
  388. protected String convertToString(ValueEval other) {
  389. return OperandResolver.coerceValueToString(other);
  390. }
  391. }
  392. private static final class NumberLookupComparer extends LookupValueComparerBase {
  393. private final double _value;
  394. protected NumberLookupComparer(NumberEval ne) {
  395. super(ne);
  396. _value = ne.getNumberValue();
  397. }
  398. @Override
  399. protected CompareResult compareSameType(ValueEval other) {
  400. NumberEval ne = (NumberEval) other;
  401. return CompareResult.valueOf(Double.compare(_value, ne.getNumberValue()));
  402. }
  403. @Override
  404. protected String getValueAsString() {
  405. return String.valueOf(_value);
  406. }
  407. }
  408. private static final class BooleanLookupComparer extends LookupValueComparerBase {
  409. private final boolean _value;
  410. protected BooleanLookupComparer(BoolEval be) {
  411. super(be);
  412. _value = be.getBooleanValue();
  413. }
  414. @Override
  415. protected CompareResult compareSameType(ValueEval other) {
  416. BoolEval be = (BoolEval) other;
  417. boolean otherVal = be.getBooleanValue();
  418. if(_value == otherVal) {
  419. return CompareResult.EQUAL;
  420. }
  421. // TRUE > FALSE
  422. if(_value) {
  423. return CompareResult.GREATER_THAN;
  424. }
  425. return CompareResult.LESS_THAN;
  426. }
  427. @Override
  428. protected String getValueAsString() {
  429. return String.valueOf(_value);
  430. }
  431. }
  432. /**
  433. * Processes the third argument to VLOOKUP, or HLOOKUP (<b>col_index_num</b>
  434. * or <b>row_index_num</b> respectively).<br>
  435. * Sample behaviour:
  436. * <table>
  437. * <caption>Sample behaviour</caption>
  438. * <tr><th>Input&nbsp;&nbsp;&nbsp;Return</th><th>Value&nbsp;&nbsp;</th><th>Thrown Error</th></tr>
  439. * <tr><td>5</td><td>4</td><td>&nbsp;</td></tr>
  440. * <tr><td>2.9</td><td>2</td><td>&nbsp;</td></tr>
  441. * <tr><td>"5"</td><td>4</td><td>&nbsp;</td></tr>
  442. * <tr><td>"2.18e1"</td><td>21</td><td>&nbsp;</td></tr>
  443. * <tr><td>"-$2"</td><td>-3</td><td>*</td></tr>
  444. * <tr><td>FALSE</td><td>-1</td><td>*</td></tr>
  445. * <tr><td>TRUE</td><td>0</td><td>&nbsp;</td></tr>
  446. * <tr><td>"TRUE"</td><td>&nbsp;</td><td>#REF!</td></tr>
  447. * <tr><td>"abc"</td><td>&nbsp;</td><td>#REF!</td></tr>
  448. * <tr><td>""</td><td>&nbsp;</td><td>#REF!</td></tr>
  449. * <tr><td>&lt;blank&gt;</td><td>&nbsp;</td><td>#VALUE!</td></tr>
  450. * </table><br>
  451. *
  452. * Note - out of range errors (result index too high) are handled by the caller.
  453. * @return column or row index as a zero-based value, never negative.
  454. * @throws EvaluationException when the specified arg cannot be coerced to a non-negative integer
  455. */
  456. public static int resolveRowOrColIndexArg(ValueEval rowColIndexArg, int srcCellRow, int srcCellCol) throws EvaluationException {
  457. if(rowColIndexArg == null) {
  458. throw new IllegalArgumentException("argument must not be null");
  459. }
  460. ValueEval veRowColIndexArg;
  461. try {
  462. veRowColIndexArg = OperandResolver.getSingleValue(rowColIndexArg, srcCellRow, (short)srcCellCol);
  463. } catch (EvaluationException e) {
  464. // All errors get translated to #REF!
  465. throw EvaluationException.invalidRef();
  466. }
  467. int oneBasedIndex;
  468. if(veRowColIndexArg instanceof StringEval) {
  469. StringEval se = (StringEval) veRowColIndexArg;
  470. String strVal = se.getStringValue();
  471. Double dVal = OperandResolver.parseDouble(strVal);
  472. if(dVal == null) {
  473. // String does not resolve to a number. Raise #REF! error.
  474. throw EvaluationException.invalidRef();
  475. // This includes text booleans "TRUE" and "FALSE". They are not valid.
  476. }
  477. // else - numeric value parses OK
  478. }
  479. // actual BoolEval values get interpreted as FALSE->0 and TRUE->1
  480. oneBasedIndex = OperandResolver.coerceValueToInt(veRowColIndexArg);
  481. if (oneBasedIndex < 1) {
  482. // note this is asymmetric with the errors when the index is too large (#REF!)
  483. throw EvaluationException.invalidValue();
  484. }
  485. return oneBasedIndex - 1; // convert to zero based
  486. }
  487. /**
  488. * The second argument (table_array) should be an area ref, but can actually be a cell ref, in
  489. * which case it is interpreted as a 1x1 area ref. Other scalar values cause #VALUE! error.
  490. */
  491. public static TwoDEval resolveTableArrayArg(ValueEval eval) throws EvaluationException {
  492. if (eval instanceof TwoDEval) {
  493. return (TwoDEval) eval;
  494. }
  495. if(eval instanceof RefEval) {
  496. RefEval refEval = (RefEval) eval;
  497. // Make this cell ref look like a 1x1 area ref.
  498. // It doesn't matter if eval is a 2D or 3D ref, because that detail is never asked of AreaEval.
  499. return refEval.offset(0, 0, 0, 0);
  500. }
  501. throw EvaluationException.invalidValue();
  502. }
  503. /**
  504. * Resolves the last (optional) parameter (<b>range_lookup</b>) to the VLOOKUP and HLOOKUP functions.
  505. * @param rangeLookupArg must not be {@code null}
  506. */
  507. public static boolean resolveRangeLookupArg(ValueEval rangeLookupArg, int srcCellRow, int srcCellCol) throws EvaluationException {
  508. ValueEval valEval = OperandResolver.getSingleValue(rangeLookupArg, srcCellRow, srcCellCol);
  509. if(valEval == MissingArgEval.instance) {
  510. // Tricky:
  511. // forth arg exists but is not supplied: "=VLOOKUP(A1,A2:A4,2,)"
  512. return false;
  513. }
  514. if(valEval instanceof BlankEval) {
  515. // Tricky:
  516. // fourth arg supplied but evaluates to blank
  517. // this does not get the default value
  518. return false;
  519. }
  520. if(valEval instanceof BoolEval) {
  521. // Happy day flow
  522. BoolEval boolEval = (BoolEval) valEval;
  523. return boolEval.getBooleanValue();
  524. }
  525. if (valEval instanceof StringEval) {
  526. String stringValue = ((StringEval) valEval).getStringValue();
  527. if(stringValue.length() < 1) {
  528. // More trickiness:
  529. // Empty string is not the same as BlankEval. It causes #VALUE! error
  530. throw EvaluationException.invalidValue();
  531. }
  532. // TODO move parseBoolean to OperandResolver
  533. Boolean b = Countif.parseBoolean(stringValue);
  534. if(b != null) {
  535. // string converted to boolean OK
  536. return b;
  537. }
  538. // Even more trickiness:
  539. // Note - even if the StringEval represents a number value (for example "1"),
  540. // Excel does not resolve it to a boolean.
  541. throw EvaluationException.invalidValue();
  542. // This is in contrast to the code below,, where NumberEvals values (for
  543. // example 0.01) *do* resolve to equivalent boolean values.
  544. }
  545. if (valEval instanceof NumericValueEval) {
  546. NumericValueEval nve = (NumericValueEval) valEval;
  547. // zero is FALSE, everything else is TRUE
  548. return 0.0 != nve.getNumberValue();
  549. }
  550. throw new RuntimeException("Unexpected eval type (" + valEval + ")");
  551. }
  552. public static int lookupFirstIndexOfValue(ValueEval lookupValue, ValueVector vector, boolean isRangeLookup) throws EvaluationException {
  553. LookupValueComparer lookupComparer = createLookupComparer(lookupValue, isRangeLookup, false);
  554. int result;
  555. if(isRangeLookup) {
  556. result = performBinarySearch(vector, lookupComparer);
  557. } else {
  558. result = lookupFirstIndexOfValue(lookupComparer, vector, MatchMode.ExactMatch);
  559. }
  560. if(result < 0) {
  561. throw new EvaluationException(ErrorEval.NA);
  562. }
  563. return result;
  564. }
  565. public static int xlookupIndexOfValue(ValueEval lookupValue, ValueVector vector, MatchMode matchMode, SearchMode searchMode) throws EvaluationException {
  566. ValueEval modifiedLookup = lookupValue;
  567. if (lookupValue instanceof StringEval &&
  568. (matchMode == MatchMode.ExactMatchFallbackToLargerValue || matchMode == MatchMode.ExactMatchFallbackToSmallerValue)) {
  569. String lookupText = ((StringEval)lookupValue).getStringValue();
  570. StringBuilder sb = new StringBuilder(lookupText.length());
  571. boolean containsWildcard = false;
  572. for (char c : lookupText.toCharArray()) {
  573. switch (c) {
  574. case '~':
  575. case '?':
  576. case '*':
  577. containsWildcard = true;
  578. break;
  579. default:
  580. sb.append(c);
  581. }
  582. if (containsWildcard)
  583. break;
  584. }
  585. if (containsWildcard) {
  586. modifiedLookup = new StringEval(sb.toString());
  587. }
  588. }
  589. LookupValueComparer lookupComparer = createTolerantLookupComparer(modifiedLookup, matchMode != MatchMode.WildcardMatch, true);
  590. int result;
  591. if (searchMode == SearchMode.BinarySearchForward) {
  592. result = binarySearchIndexOfValue(lookupComparer, vector, matchMode, false);
  593. } else if (searchMode == SearchMode.BinarySearchBackward) {
  594. result = binarySearchIndexOfValue(lookupComparer, vector, matchMode, true);
  595. } else if (searchMode == SearchMode.IterateBackward) {
  596. result = lookupLastIndexOfValue(lookupComparer, vector, matchMode);
  597. } else {
  598. result = lookupFirstIndexOfValue(lookupComparer, vector, matchMode);
  599. }
  600. if(result < 0) {
  601. throw new EvaluationException(ErrorEval.NA);
  602. }
  603. return result;
  604. }
  605. /**
  606. * Finds first (lowest index) matching occurrence of specified value.
  607. * @param lookupComparer the value to be found in column or row vector
  608. * @param vector the values to be searched. For VLOOKUP this is the first column of the
  609. * tableArray. For HLOOKUP this is the first row of the tableArray.
  610. * @param matchMode
  611. * @return zero based index into the vector, -1 if value cannot be found
  612. */
  613. private static int lookupFirstIndexOfValue(LookupValueComparer lookupComparer, ValueVector vector,
  614. MatchMode matchMode) {
  615. return lookupIndexOfValue(lookupComparer, vector, matchMode, false);
  616. }
  617. /**
  618. * Finds last (greatest index) matching occurrence of specified value.
  619. * @param lookupComparer the value to be found in column or row vector
  620. * @param vector the values to be searched. For VLOOKUP this is the first column of the
  621. * tableArray. For HLOOKUP this is the first row of the tableArray.
  622. * @param matchMode
  623. * @return zero based index into the vector, -1 if value cannot be found
  624. */
  625. private static int lookupLastIndexOfValue(LookupValueComparer lookupComparer, ValueVector vector,
  626. MatchMode matchMode) {
  627. return lookupIndexOfValue(lookupComparer, vector, matchMode, true);
  628. }
  629. private static int lookupIndexOfValue(LookupValueComparer lookupComparer, ValueVector vector,
  630. MatchMode matchMode, boolean reverse) {
  631. int bestMatchIdx = -1;
  632. ValueEval bestMatchEval = null;
  633. Iterator<Integer> idxIter = reverse ? vector.reverseIndexIterator() : vector.indexIterator();
  634. while (idxIter.hasNext()) {
  635. int i = idxIter.next();
  636. ValueEval valueEval = vector.getItem(i);
  637. CompareResult result = lookupComparer.compareTo(valueEval);
  638. if (result.isEqual()) {
  639. return i;
  640. }
  641. switch (matchMode) {
  642. case ExactMatchFallbackToLargerValue:
  643. if (result.isLessThan()) {
  644. if (bestMatchEval == null) {
  645. bestMatchIdx = i;
  646. bestMatchEval = valueEval;
  647. } else {
  648. LookupValueComparer matchComparer = createTolerantLookupComparer(valueEval, true, true);
  649. if (matchComparer.compareTo(bestMatchEval).isLessThan()) {
  650. bestMatchIdx = i;
  651. bestMatchEval = valueEval;
  652. }
  653. }
  654. }
  655. break;
  656. case ExactMatchFallbackToSmallerValue:
  657. if (result.isGreaterThan()) {
  658. if (bestMatchEval == null) {
  659. bestMatchIdx = i;
  660. bestMatchEval = valueEval;
  661. } else {
  662. LookupValueComparer matchComparer = createTolerantLookupComparer(valueEval, true, true);
  663. if (matchComparer.compareTo(bestMatchEval).isGreaterThan()) {
  664. bestMatchIdx = i;
  665. bestMatchEval = valueEval;
  666. }
  667. }
  668. }
  669. break;
  670. }
  671. }
  672. return bestMatchIdx;
  673. }
  674. private static int binarySearchIndexOfValue(LookupValueComparer lookupComparer, ValueVector vector,
  675. MatchMode matchMode, boolean reverse) {
  676. int bestMatchIdx = -1;
  677. ValueEval bestMatchEval = null;
  678. HashSet<Integer> alreadySearched = new HashSet<>();
  679. BinarySearchIndexes bsi = new BinarySearchIndexes(vector.getSize());
  680. while (true) {
  681. int i = bsi.getMidIx();
  682. if(i < 0 || alreadySearched.contains(i)) {
  683. return bestMatchIdx;
  684. }
  685. alreadySearched.add(i);
  686. ValueEval valueEval = vector.getItem(i);
  687. CompareResult result = lookupComparer.compareTo(valueEval);
  688. if (result.isEqual()) {
  689. return i;
  690. }
  691. switch (matchMode) {
  692. case ExactMatchFallbackToLargerValue:
  693. if (result.isLessThan()) {
  694. if (bestMatchEval == null) {
  695. bestMatchIdx = i;
  696. bestMatchEval = valueEval;
  697. } else {
  698. LookupValueComparer matchComparer = createTolerantLookupComparer(valueEval, true, true);
  699. if (matchComparer.compareTo(bestMatchEval).isLessThan()) {
  700. bestMatchIdx = i;
  701. bestMatchEval = valueEval;
  702. }
  703. }
  704. }
  705. break;
  706. case ExactMatchFallbackToSmallerValue:
  707. if (result.isGreaterThan()) {
  708. if (bestMatchEval == null) {
  709. bestMatchIdx = i;
  710. bestMatchEval = valueEval;
  711. } else {
  712. LookupValueComparer matchComparer = createTolerantLookupComparer(valueEval, true, true);
  713. if (matchComparer.compareTo(bestMatchEval).isGreaterThan()) {
  714. bestMatchIdx = i;
  715. bestMatchEval = valueEval;
  716. }
  717. }
  718. }
  719. break;
  720. }
  721. if (result.isTypeMismatch()) {
  722. int newIdx = handleMidValueTypeMismatch(lookupComparer, vector, bsi, i, reverse);
  723. if (newIdx >= 0) {
  724. return newIdx;
  725. }
  726. } else if (reverse) {
  727. bsi.narrowSearch(i, result.isGreaterThan());
  728. } else {
  729. bsi.narrowSearch(i, result.isLessThan());
  730. }
  731. }
  732. }
  733. /**
  734. * Encapsulates some standard binary search functionality so the unusual Excel behaviour can
  735. * be clearly distinguished.
  736. */
  737. private static final class BinarySearchIndexes {
  738. private int _lowIx;
  739. private int _highIx;
  740. public BinarySearchIndexes(int highIx) {
  741. _lowIx = -1;
  742. _highIx = highIx;
  743. }
  744. /**
  745. * @return -1 if the search range is empty
  746. */
  747. public int getMidIx() {
  748. int ixDiff = _highIx - _lowIx;
  749. if(ixDiff < 2) {
  750. return -1;
  751. }
  752. return _lowIx + (ixDiff / 2);
  753. }
  754. public int getLowIx() {
  755. return _lowIx;
  756. }
  757. public int getHighIx() {
  758. return _highIx;
  759. }
  760. public void narrowSearch(int midIx, boolean isLessThan) {
  761. if(isLessThan) {
  762. _highIx = midIx;
  763. } else {
  764. _lowIx = midIx;
  765. }
  766. }
  767. }
  768. /**
  769. * Excel has funny behaviour when the some elements in the search vector are the wrong type.
  770. *
  771. */
  772. private static int performBinarySearch(ValueVector vector, LookupValueComparer lookupComparer) {
  773. // both low and high indexes point to values assumed too low and too high.
  774. BinarySearchIndexes bsi = new BinarySearchIndexes(vector.getSize());
  775. while(true) {
  776. int midIx = bsi.getMidIx();
  777. if(midIx < 0) {
  778. return bsi.getLowIx();
  779. }
  780. CompareResult cr = lookupComparer.compareTo(vector.getItem(midIx));
  781. if(cr.isTypeMismatch()) {
  782. int newMidIx = handleMidValueTypeMismatch(lookupComparer, vector, bsi, midIx, false);
  783. if(newMidIx < 0) {
  784. continue;
  785. }
  786. midIx = newMidIx;
  787. cr = lookupComparer.compareTo(vector.getItem(midIx));
  788. }
  789. if(cr.isEqual()) {
  790. return findLastIndexInRunOfEqualValues(lookupComparer, vector, midIx, bsi.getHighIx());
  791. }
  792. bsi.narrowSearch(midIx, cr.isLessThan());
  793. }
  794. }
  795. /**
  796. * Excel seems to handle mismatched types initially by just stepping 'mid' ix forward to the
  797. * first compatible value.
  798. * @param midIx 'mid' index (value which has the wrong type)
  799. * @param reverse the data is sorted in reverse order
  800. * @return usually -1, signifying that the BinarySearchIndex has been narrowed to the new mid
  801. * index. Zero or greater signifies that an exact match for the lookup value was found
  802. */
  803. private static int handleMidValueTypeMismatch(LookupValueComparer lookupComparer, ValueVector vector,
  804. BinarySearchIndexes bsi, int midIx, boolean reverse) {
  805. int newMid = midIx;
  806. int highIx = bsi.getHighIx();
  807. while(true) {
  808. newMid++;
  809. if(newMid == highIx) {
  810. // every element from midIx to highIx was the wrong type
  811. // move highIx down to the low end of the mid values
  812. bsi.narrowSearch(midIx, true);
  813. return -1;
  814. }
  815. CompareResult cr = lookupComparer.compareTo(vector.getItem(newMid));
  816. if(cr.isLessThan() && !reverse && newMid == highIx-1) {
  817. // move highIx down to the low end of the mid values
  818. bsi.narrowSearch(midIx, true);
  819. return -1;
  820. // but only when "newMid == highIx-1"? slightly weird.
  821. // It would seem more efficient to always do this.
  822. } else if(cr.isGreaterThan() && reverse && newMid == highIx-1) {
  823. // move highIx down to the low end of the mid values
  824. bsi.narrowSearch(midIx, true);
  825. return -1;
  826. // but only when "newMid == highIx-1"? slightly weird.
  827. // It would seem more efficient to always do this.
  828. }
  829. if(cr.isTypeMismatch()) {
  830. // keep stepping over values until the right type is found
  831. continue;
  832. }
  833. if(cr.isEqual()) {
  834. return newMid;
  835. }
  836. // Note - if moving highIx down (due to lookup<vector[newMid]),
  837. // this execution path only moves highIx it down as far as newMid, not midIx,
  838. // which would be more efficient.
  839. if (reverse) {
  840. bsi.narrowSearch(newMid, cr.isGreaterThan());
  841. } else {
  842. bsi.narrowSearch(newMid, cr.isLessThan());
  843. }
  844. return -1;
  845. }
  846. }
  847. /**
  848. * Once the binary search has found a single match, (V/H)LOOKUP steps one by one over subsequent
  849. * values to choose the last matching item.
  850. */
  851. private static int findLastIndexInRunOfEqualValues(LookupValueComparer lookupComparer, ValueVector vector,
  852. int firstFoundIndex, int maxIx) {
  853. for(int i=firstFoundIndex+1; i<maxIx; i++) {
  854. if(!lookupComparer.compareTo(vector.getItem(i)).isEqual()) {
  855. return i-1;
  856. }
  857. }
  858. return maxIx - 1;
  859. }
  860. static LookupValueComparer createLookupComparer(ValueEval lookupValue, boolean matchExact, boolean isMatchFunction) {
  861. if (lookupValue == BlankEval.instance) {
  862. // blank eval translates to zero
  863. // Note - a blank eval in the lookup column/row never matches anything
  864. // empty string in the lookup column/row can only be matched by explicit empty string
  865. return new NumberLookupComparer(NumberEval.ZERO);
  866. }
  867. if (lookupValue instanceof StringEval) {
  868. //TODO eventually here return a WildcardStringLookupComparer
  869. return new StringLookupComparer((StringEval) lookupValue, matchExact, isMatchFunction);
  870. }
  871. if (lookupValue instanceof NumberEval) {
  872. return new NumberLookupComparer((NumberEval) lookupValue);
  873. }
  874. if (lookupValue instanceof BoolEval) {
  875. return new BooleanLookupComparer((BoolEval) lookupValue);
  876. }
  877. throw new IllegalArgumentException("Bad lookup value type (" + lookupValue.getClass().getName() + ")");
  878. }
  879. private static LookupValueComparer createTolerantLookupComparer(ValueEval lookupValue, boolean matchExact, boolean isMatchFunction) {
  880. if (lookupValue == BlankEval.instance) {
  881. return new TolerantStringLookupComparer(new StringEval(""), matchExact, isMatchFunction);
  882. }
  883. if (lookupValue instanceof BoolEval) {
  884. return new BooleanLookupComparer((BoolEval) lookupValue);
  885. }
  886. if (matchExact && lookupValue instanceof NumberEval) {
  887. return new NumberLookupComparer((NumberEval) lookupValue);
  888. }
  889. return new TolerantStringLookupComparer(lookupValue, matchExact, isMatchFunction);
  890. }
  891. }