LookupRef.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Calculation;
  3. use PhpOffice\PhpSpreadsheet\Cell\Cell;
  4. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  5. class LookupRef
  6. {
  7. /**
  8. * CELL_ADDRESS.
  9. *
  10. * Creates a cell address as text, given specified row and column numbers.
  11. *
  12. * Excel Function:
  13. * =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText])
  14. *
  15. * @param mixed $row Row number to use in the cell reference
  16. * @param mixed $column Column number to use in the cell reference
  17. * @param int $relativity Flag indicating the type of reference to return
  18. * 1 or omitted Absolute
  19. * 2 Absolute row; relative column
  20. * 3 Relative row; absolute column
  21. * 4 Relative
  22. * @param bool $referenceStyle A logical value that specifies the A1 or R1C1 reference style.
  23. * TRUE or omitted CELL_ADDRESS returns an A1-style reference
  24. * FALSE CELL_ADDRESS returns an R1C1-style reference
  25. * @param string $sheetText Optional Name of worksheet to use
  26. *
  27. * @return string
  28. */
  29. public static function cellAddress($row, $column, $relativity = 1, $referenceStyle = true, $sheetText = '')
  30. {
  31. $row = Functions::flattenSingleValue($row);
  32. $column = Functions::flattenSingleValue($column);
  33. $relativity = Functions::flattenSingleValue($relativity);
  34. $sheetText = Functions::flattenSingleValue($sheetText);
  35. if (($row < 1) || ($column < 1)) {
  36. return Functions::VALUE();
  37. }
  38. if ($sheetText > '') {
  39. if (strpos($sheetText, ' ') !== false) {
  40. $sheetText = "'" . $sheetText . "'";
  41. }
  42. $sheetText .= '!';
  43. }
  44. if ((!is_bool($referenceStyle)) || $referenceStyle) {
  45. $rowRelative = $columnRelative = '$';
  46. $column = Coordinate::stringFromColumnIndex($column);
  47. if (($relativity == 2) || ($relativity == 4)) {
  48. $columnRelative = '';
  49. }
  50. if (($relativity == 3) || ($relativity == 4)) {
  51. $rowRelative = '';
  52. }
  53. return $sheetText . $columnRelative . $column . $rowRelative . $row;
  54. }
  55. if (($relativity == 2) || ($relativity == 4)) {
  56. $column = '[' . $column . ']';
  57. }
  58. if (($relativity == 3) || ($relativity == 4)) {
  59. $row = '[' . $row . ']';
  60. }
  61. return $sheetText . 'R' . $row . 'C' . $column;
  62. }
  63. /**
  64. * COLUMN.
  65. *
  66. * Returns the column number of the given cell reference
  67. * If the cell reference is a range of cells, COLUMN returns the column numbers of each column in the reference as a horizontal array.
  68. * If cell reference is omitted, and the function is being called through the calculation engine, then it is assumed to be the
  69. * reference of the cell in which the COLUMN function appears; otherwise this function returns 0.
  70. *
  71. * Excel Function:
  72. * =COLUMN([cellAddress])
  73. *
  74. * @param null|array|string $cellAddress A reference to a range of cells for which you want the column numbers
  75. *
  76. * @return int|int[]
  77. */
  78. public static function COLUMN($cellAddress = null)
  79. {
  80. if ($cellAddress === null || trim($cellAddress) === '') {
  81. return 0;
  82. }
  83. if (is_array($cellAddress)) {
  84. foreach ($cellAddress as $columnKey => $value) {
  85. $columnKey = preg_replace('/[^a-z]/i', '', $columnKey);
  86. return (int) Coordinate::columnIndexFromString($columnKey);
  87. }
  88. } else {
  89. if (strpos($cellAddress, '!') !== false) {
  90. list($sheet, $cellAddress) = explode('!', $cellAddress);
  91. }
  92. if (strpos($cellAddress, ':') !== false) {
  93. list($startAddress, $endAddress) = explode(':', $cellAddress);
  94. $startAddress = preg_replace('/[^a-z]/i', '', $startAddress);
  95. $endAddress = preg_replace('/[^a-z]/i', '', $endAddress);
  96. $returnValue = [];
  97. do {
  98. $returnValue[] = (int) Coordinate::columnIndexFromString($startAddress);
  99. } while ($startAddress++ != $endAddress);
  100. return $returnValue;
  101. }
  102. $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress);
  103. return (int) Coordinate::columnIndexFromString($cellAddress);
  104. }
  105. }
  106. /**
  107. * COLUMNS.
  108. *
  109. * Returns the number of columns in an array or reference.
  110. *
  111. * Excel Function:
  112. * =COLUMNS(cellAddress)
  113. *
  114. * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells for which you want the number of columns
  115. *
  116. * @return int The number of columns in cellAddress
  117. */
  118. public static function COLUMNS($cellAddress = null)
  119. {
  120. if ($cellAddress === null || $cellAddress === '') {
  121. return 1;
  122. } elseif (!is_array($cellAddress)) {
  123. return Functions::VALUE();
  124. }
  125. reset($cellAddress);
  126. $isMatrix = (is_numeric(key($cellAddress)));
  127. list($columns, $rows) = Calculation::getMatrixDimensions($cellAddress);
  128. if ($isMatrix) {
  129. return $rows;
  130. }
  131. return $columns;
  132. }
  133. /**
  134. * ROW.
  135. *
  136. * Returns the row number of the given cell reference
  137. * If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference as a vertical array.
  138. * If cell reference is omitted, and the function is being called through the calculation engine, then it is assumed to be the
  139. * reference of the cell in which the ROW function appears; otherwise this function returns 0.
  140. *
  141. * Excel Function:
  142. * =ROW([cellAddress])
  143. *
  144. * @param null|array|string $cellAddress A reference to a range of cells for which you want the row numbers
  145. *
  146. * @return int or array of integer
  147. */
  148. public static function ROW($cellAddress = null)
  149. {
  150. if ($cellAddress === null || trim($cellAddress) === '') {
  151. return 0;
  152. }
  153. if (is_array($cellAddress)) {
  154. foreach ($cellAddress as $columnKey => $rowValue) {
  155. foreach ($rowValue as $rowKey => $cellValue) {
  156. return (int) preg_replace('/\D/', '', $rowKey);
  157. }
  158. }
  159. } else {
  160. if (strpos($cellAddress, '!') !== false) {
  161. list($sheet, $cellAddress) = explode('!', $cellAddress);
  162. }
  163. if (strpos($cellAddress, ':') !== false) {
  164. list($startAddress, $endAddress) = explode(':', $cellAddress);
  165. $startAddress = preg_replace('/\D/', '', $startAddress);
  166. $endAddress = preg_replace('/\D/', '', $endAddress);
  167. $returnValue = [];
  168. do {
  169. $returnValue[][] = (int) $startAddress;
  170. } while ($startAddress++ != $endAddress);
  171. return $returnValue;
  172. }
  173. list($cellAddress) = explode(':', $cellAddress);
  174. return (int) preg_replace('/\D/', '', $cellAddress);
  175. }
  176. }
  177. /**
  178. * ROWS.
  179. *
  180. * Returns the number of rows in an array or reference.
  181. *
  182. * Excel Function:
  183. * =ROWS(cellAddress)
  184. *
  185. * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells for which you want the number of rows
  186. *
  187. * @return int The number of rows in cellAddress
  188. */
  189. public static function ROWS($cellAddress = null)
  190. {
  191. if ($cellAddress === null || $cellAddress === '') {
  192. return 1;
  193. } elseif (!is_array($cellAddress)) {
  194. return Functions::VALUE();
  195. }
  196. reset($cellAddress);
  197. $isMatrix = (is_numeric(key($cellAddress)));
  198. list($columns, $rows) = Calculation::getMatrixDimensions($cellAddress);
  199. if ($isMatrix) {
  200. return $columns;
  201. }
  202. return $rows;
  203. }
  204. /**
  205. * HYPERLINK.
  206. *
  207. * Excel Function:
  208. * =HYPERLINK(linkURL,displayName)
  209. *
  210. * @category Logical Functions
  211. *
  212. * @param string $linkURL Value to check, is also the value returned when no error
  213. * @param string $displayName Value to return when testValue is an error condition
  214. * @param Cell $pCell The cell to set the hyperlink in
  215. *
  216. * @return mixed The value of $displayName (or $linkURL if $displayName was blank)
  217. */
  218. public static function HYPERLINK($linkURL = '', $displayName = null, Cell $pCell = null)
  219. {
  220. $linkURL = ($linkURL === null) ? '' : Functions::flattenSingleValue($linkURL);
  221. $displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
  222. if ((!is_object($pCell)) || (trim($linkURL) == '')) {
  223. return Functions::REF();
  224. }
  225. if ((is_object($displayName)) || trim($displayName) == '') {
  226. $displayName = $linkURL;
  227. }
  228. $pCell->getHyperlink()->setUrl($linkURL);
  229. $pCell->getHyperlink()->setTooltip($displayName);
  230. return $displayName;
  231. }
  232. /**
  233. * INDIRECT.
  234. *
  235. * Returns the reference specified by a text string.
  236. * References are immediately evaluated to display their contents.
  237. *
  238. * Excel Function:
  239. * =INDIRECT(cellAddress)
  240. *
  241. * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010
  242. *
  243. * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
  244. * @param Cell $pCell The current cell (containing this formula)
  245. *
  246. * @return mixed The cells referenced by cellAddress
  247. *
  248. * @todo Support for the optional a1 parameter introduced in Excel 2010
  249. */
  250. public static function INDIRECT($cellAddress = null, Cell $pCell = null)
  251. {
  252. $cellAddress = Functions::flattenSingleValue($cellAddress);
  253. if ($cellAddress === null || $cellAddress === '') {
  254. return Functions::REF();
  255. }
  256. $cellAddress1 = $cellAddress;
  257. $cellAddress2 = null;
  258. if (strpos($cellAddress, ':') !== false) {
  259. list($cellAddress1, $cellAddress2) = explode(':', $cellAddress);
  260. }
  261. if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) ||
  262. (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches)))) {
  263. if (!preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $cellAddress1, $matches)) {
  264. return Functions::REF();
  265. }
  266. if (strpos($cellAddress, '!') !== false) {
  267. list($sheetName, $cellAddress) = explode('!', $cellAddress);
  268. $sheetName = trim($sheetName, "'");
  269. $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
  270. } else {
  271. $pSheet = $pCell->getWorksheet();
  272. }
  273. return Calculation::getInstance()->extractNamedRange($cellAddress, $pSheet, false);
  274. }
  275. if (strpos($cellAddress, '!') !== false) {
  276. list($sheetName, $cellAddress) = explode('!', $cellAddress);
  277. $sheetName = trim($sheetName, "'");
  278. $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
  279. } else {
  280. $pSheet = $pCell->getWorksheet();
  281. }
  282. return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false);
  283. }
  284. /**
  285. * OFFSET.
  286. *
  287. * Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells.
  288. * The reference that is returned can be a single cell or a range of cells. You can specify the number of rows and
  289. * the number of columns to be returned.
  290. *
  291. * Excel Function:
  292. * =OFFSET(cellAddress, rows, cols, [height], [width])
  293. *
  294. * @param null|array|string $cellAddress The reference from which you want to base the offset. Reference must refer to a cell or
  295. * range of adjacent cells; otherwise, OFFSET returns the #VALUE! error value.
  296. * @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to.
  297. * Using 5 as the rows argument specifies that the upper-left cell in the reference is
  298. * five rows below reference. Rows can be positive (which means below the starting reference)
  299. * or negative (which means above the starting reference).
  300. * @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell of the result
  301. * to refer to. Using 5 as the cols argument specifies that the upper-left cell in the
  302. * reference is five columns to the right of reference. Cols can be positive (which means
  303. * to the right of the starting reference) or negative (which means to the left of the
  304. * starting reference).
  305. * @param mixed $height The height, in number of rows, that you want the returned reference to be. Height must be a positive number.
  306. * @param mixed $width The width, in number of columns, that you want the returned reference to be. Width must be a positive number.
  307. * @param null|Cell $pCell
  308. *
  309. * @return string A reference to a cell or range of cells
  310. */
  311. public static function OFFSET($cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, Cell $pCell = null)
  312. {
  313. $rows = Functions::flattenSingleValue($rows);
  314. $columns = Functions::flattenSingleValue($columns);
  315. $height = Functions::flattenSingleValue($height);
  316. $width = Functions::flattenSingleValue($width);
  317. if ($cellAddress == null) {
  318. return 0;
  319. }
  320. if (!is_object($pCell)) {
  321. return Functions::REF();
  322. }
  323. $sheetName = null;
  324. if (strpos($cellAddress, '!')) {
  325. list($sheetName, $cellAddress) = explode('!', $cellAddress);
  326. $sheetName = trim($sheetName, "'");
  327. }
  328. if (strpos($cellAddress, ':')) {
  329. list($startCell, $endCell) = explode(':', $cellAddress);
  330. } else {
  331. $startCell = $endCell = $cellAddress;
  332. }
  333. list($startCellColumn, $startCellRow) = Coordinate::coordinateFromString($startCell);
  334. list($endCellColumn, $endCellRow) = Coordinate::coordinateFromString($endCell);
  335. $startCellRow += $rows;
  336. $startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1;
  337. $startCellColumn += $columns;
  338. if (($startCellRow <= 0) || ($startCellColumn < 0)) {
  339. return Functions::REF();
  340. }
  341. $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1;
  342. if (($width != null) && (!is_object($width))) {
  343. $endCellColumn = $startCellColumn + $width - 1;
  344. } else {
  345. $endCellColumn += $columns;
  346. }
  347. $startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
  348. if (($height != null) && (!is_object($height))) {
  349. $endCellRow = $startCellRow + $height - 1;
  350. } else {
  351. $endCellRow += $rows;
  352. }
  353. if (($endCellRow <= 0) || ($endCellColumn < 0)) {
  354. return Functions::REF();
  355. }
  356. $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1);
  357. $cellAddress = $startCellColumn . $startCellRow;
  358. if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) {
  359. $cellAddress .= ':' . $endCellColumn . $endCellRow;
  360. }
  361. if ($sheetName !== null) {
  362. $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
  363. } else {
  364. $pSheet = $pCell->getWorksheet();
  365. }
  366. return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false);
  367. }
  368. /**
  369. * CHOOSE.
  370. *
  371. * Uses lookup_value to return a value from the list of value arguments.
  372. * Use CHOOSE to select one of up to 254 values based on the lookup_value.
  373. *
  374. * Excel Function:
  375. * =CHOOSE(index_num, value1, [value2], ...)
  376. *
  377. * @param mixed $index_num Specifies which value argument is selected.
  378. * Index_num must be a number between 1 and 254, or a formula or reference to a cell containing a number
  379. * between 1 and 254.
  380. * @param mixed $value1... Value1 is required, subsequent values are optional.
  381. * Between 1 to 254 value arguments from which CHOOSE selects a value or an action to perform based on
  382. * index_num. The arguments can be numbers, cell references, defined names, formulas, functions, or
  383. * text.
  384. *
  385. * @return mixed The selected value
  386. */
  387. public static function CHOOSE(...$chooseArgs)
  388. {
  389. $chosenEntry = Functions::flattenArray(array_shift($chooseArgs));
  390. $entryCount = count($chooseArgs) - 1;
  391. if (is_array($chosenEntry)) {
  392. $chosenEntry = array_shift($chosenEntry);
  393. }
  394. if ((is_numeric($chosenEntry)) && (!is_bool($chosenEntry))) {
  395. --$chosenEntry;
  396. } else {
  397. return Functions::VALUE();
  398. }
  399. $chosenEntry = floor($chosenEntry);
  400. if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
  401. return Functions::VALUE();
  402. }
  403. if (is_array($chooseArgs[$chosenEntry])) {
  404. return Functions::flattenArray($chooseArgs[$chosenEntry]);
  405. }
  406. return $chooseArgs[$chosenEntry];
  407. }
  408. /**
  409. * MATCH.
  410. *
  411. * The MATCH function searches for a specified item in a range of cells
  412. *
  413. * Excel Function:
  414. * =MATCH(lookup_value, lookup_array, [match_type])
  415. *
  416. * @param mixed $lookupValue The value that you want to match in lookup_array
  417. * @param mixed $lookupArray The range of cells being searched
  418. * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below. If match_type is 1 or -1, the list has to be ordered.
  419. *
  420. * @return int The relative position of the found item
  421. */
  422. public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
  423. {
  424. $lookupArray = Functions::flattenArray($lookupArray);
  425. $lookupValue = Functions::flattenSingleValue($lookupValue);
  426. $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
  427. // MATCH is not case sensitive
  428. $lookupValue = strtolower($lookupValue);
  429. // Lookup_value type has to be number, text, or logical values
  430. if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
  431. return Functions::NA();
  432. }
  433. // Match_type is 0, 1 or -1
  434. if (($matchType !== 0) && ($matchType !== -1) && ($matchType !== 1)) {
  435. return Functions::NA();
  436. }
  437. // Lookup_array should not be empty
  438. $lookupArraySize = count($lookupArray);
  439. if ($lookupArraySize <= 0) {
  440. return Functions::NA();
  441. }
  442. // Lookup_array should contain only number, text, or logical values, or empty (null) cells
  443. foreach ($lookupArray as $i => $lookupArrayValue) {
  444. // check the type of the value
  445. if ((!is_numeric($lookupArrayValue)) && (!is_string($lookupArrayValue)) &&
  446. (!is_bool($lookupArrayValue)) && ($lookupArrayValue !== null)
  447. ) {
  448. return Functions::NA();
  449. }
  450. // Convert strings to lowercase for case-insensitive testing
  451. if (is_string($lookupArrayValue)) {
  452. $lookupArray[$i] = strtolower($lookupArrayValue);
  453. }
  454. if (($lookupArrayValue === null) && (($matchType == 1) || ($matchType == -1))) {
  455. $lookupArray = array_slice($lookupArray, 0, $i - 1);
  456. }
  457. }
  458. if ($matchType == 1) {
  459. // If match_type is 1 the list has to be processed from last to first
  460. $lookupArray = array_reverse($lookupArray);
  461. $keySet = array_reverse(array_keys($lookupArray));
  462. }
  463. // **
  464. // find the match
  465. // **
  466. if ($matchType == 0 || $matchType == 1) {
  467. foreach ($lookupArray as $i => $lookupArrayValue) {
  468. if (($matchType == 0) && ($lookupArrayValue == $lookupValue)) {
  469. // exact match
  470. return ++$i;
  471. } elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) {
  472. $i = array_search($i, $keySet);
  473. // The current value is the (first) match
  474. return $i + 1;
  475. }
  476. }
  477. } else {
  478. // matchType = -1
  479. // "Special" case: since the array it's supposed to be ordered in descending order, the
  480. // Excel algorithm gives up immediately if the first element is smaller than the searched value
  481. if ($lookupArray[0] < $lookupValue) {
  482. return Functions::NA();
  483. }
  484. $maxValueKey = null;
  485. // The basic algorithm is:
  486. // Iterate and keep the highest match until the next element is smaller than the searched value.
  487. // Return immediately if perfect match is found
  488. foreach ($lookupArray as $i => $lookupArrayValue) {
  489. if ($lookupArrayValue == $lookupValue) {
  490. // Another "special" case. If a perfect match is found,
  491. // the algorithm gives up immediately
  492. return $i + 1;
  493. } elseif ($lookupArrayValue >= $lookupValue) {
  494. $maxValueKey = $i + 1;
  495. }
  496. }
  497. if ($maxValueKey !== null) {
  498. return $maxValueKey;
  499. }
  500. }
  501. // Unsuccessful in finding a match, return #N/A error value
  502. return Functions::NA();
  503. }
  504. /**
  505. * INDEX.
  506. *
  507. * Uses an index to choose a value from a reference or array
  508. *
  509. * Excel Function:
  510. * =INDEX(range_array, row_num, [column_num])
  511. *
  512. * @param mixed $arrayValues A range of cells or an array constant
  513. * @param mixed $rowNum The row in array from which to return a value. If row_num is omitted, column_num is required.
  514. * @param mixed $columnNum The column in array from which to return a value. If column_num is omitted, row_num is required.
  515. *
  516. * @return mixed the value of a specified cell or array of cells
  517. */
  518. public static function INDEX($arrayValues, $rowNum = 0, $columnNum = 0)
  519. {
  520. $rowNum = Functions::flattenSingleValue($rowNum);
  521. $columnNum = Functions::flattenSingleValue($columnNum);
  522. if (($rowNum < 0) || ($columnNum < 0)) {
  523. return Functions::VALUE();
  524. }
  525. if (!is_array($arrayValues) || ($rowNum > count($arrayValues))) {
  526. return Functions::REF();
  527. }
  528. $rowKeys = array_keys($arrayValues);
  529. $columnKeys = @array_keys($arrayValues[$rowKeys[0]]);
  530. if ($columnNum > count($columnKeys)) {
  531. return Functions::VALUE();
  532. } elseif ($columnNum == 0) {
  533. if ($rowNum == 0) {
  534. return $arrayValues;
  535. }
  536. $rowNum = $rowKeys[--$rowNum];
  537. $returnArray = [];
  538. foreach ($arrayValues as $arrayColumn) {
  539. if (is_array($arrayColumn)) {
  540. if (isset($arrayColumn[$rowNum])) {
  541. $returnArray[] = $arrayColumn[$rowNum];
  542. } else {
  543. return [$rowNum => $arrayValues[$rowNum]];
  544. }
  545. } else {
  546. return $arrayValues[$rowNum];
  547. }
  548. }
  549. return $returnArray;
  550. }
  551. $columnNum = $columnKeys[--$columnNum];
  552. if ($rowNum > count($rowKeys)) {
  553. return Functions::VALUE();
  554. } elseif ($rowNum == 0) {
  555. return $arrayValues[$columnNum];
  556. }
  557. $rowNum = $rowKeys[--$rowNum];
  558. return $arrayValues[$rowNum][$columnNum];
  559. }
  560. /**
  561. * TRANSPOSE.
  562. *
  563. * @param array $matrixData A matrix of values
  564. *
  565. * @return array
  566. *
  567. * Unlike the Excel TRANSPOSE function, which will only work on a single row or column, this function will transpose a full matrix
  568. */
  569. public static function TRANSPOSE($matrixData)
  570. {
  571. $returnMatrix = [];
  572. if (!is_array($matrixData)) {
  573. $matrixData = [[$matrixData]];
  574. }
  575. $column = 0;
  576. foreach ($matrixData as $matrixRow) {
  577. $row = 0;
  578. foreach ($matrixRow as $matrixCell) {
  579. $returnMatrix[$row][$column] = $matrixCell;
  580. ++$row;
  581. }
  582. ++$column;
  583. }
  584. return $returnMatrix;
  585. }
  586. private static function vlookupSort($a, $b)
  587. {
  588. reset($a);
  589. $firstColumn = key($a);
  590. if (($aLower = strtolower($a[$firstColumn])) == ($bLower = strtolower($b[$firstColumn]))) {
  591. return 0;
  592. }
  593. return ($aLower < $bLower) ? -1 : 1;
  594. }
  595. /**
  596. * VLOOKUP
  597. * The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value in the same row based on the index_number.
  598. *
  599. * @param mixed $lookup_value The value that you want to match in lookup_array
  600. * @param mixed $lookup_array The range of cells being searched
  601. * @param mixed $index_number The column number in table_array from which the matching value must be returned. The first column is 1.
  602. * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
  603. *
  604. * @return mixed The value of the found cell
  605. */
  606. public static function VLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
  607. {
  608. $lookup_value = Functions::flattenSingleValue($lookup_value);
  609. $index_number = Functions::flattenSingleValue($index_number);
  610. $not_exact_match = Functions::flattenSingleValue($not_exact_match);
  611. // index_number must be greater than or equal to 1
  612. if ($index_number < 1) {
  613. return Functions::VALUE();
  614. }
  615. // index_number must be less than or equal to the number of columns in lookup_array
  616. if ((!is_array($lookup_array)) || (empty($lookup_array))) {
  617. return Functions::REF();
  618. }
  619. $f = array_keys($lookup_array);
  620. $firstRow = array_pop($f);
  621. if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array[$firstRow]))) {
  622. return Functions::REF();
  623. }
  624. $columnKeys = array_keys($lookup_array[$firstRow]);
  625. $returnColumn = $columnKeys[--$index_number];
  626. $firstColumn = array_shift($columnKeys);
  627. if (!$not_exact_match) {
  628. uasort($lookup_array, ['self', 'vlookupSort']);
  629. }
  630. $rowNumber = $rowValue = false;
  631. foreach ($lookup_array as $rowKey => $rowData) {
  632. if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn]) && ($rowData[$firstColumn] > $lookup_value)) ||
  633. (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]) && (strtolower($rowData[$firstColumn]) > strtolower($lookup_value)))) {
  634. break;
  635. }
  636. // remember the last key, but only if datatypes match
  637. if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn])) ||
  638. (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]))) {
  639. $rowNumber = $rowKey;
  640. $rowValue = $rowData[$firstColumn];
  641. }
  642. }
  643. if ($rowNumber !== false) {
  644. if ((!$not_exact_match) && ($rowValue != $lookup_value)) {
  645. // if an exact match is required, we have what we need to return an appropriate response
  646. return Functions::NA();
  647. }
  648. // otherwise return the appropriate value
  649. return $lookup_array[$rowNumber][$returnColumn];
  650. }
  651. return Functions::NA();
  652. }
  653. /**
  654. * HLOOKUP
  655. * The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value in the same column based on the index_number.
  656. *
  657. * @param mixed $lookup_value The value that you want to match in lookup_array
  658. * @param mixed $lookup_array The range of cells being searched
  659. * @param mixed $index_number The row number in table_array from which the matching value must be returned. The first row is 1.
  660. * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
  661. *
  662. * @return mixed The value of the found cell
  663. */
  664. public static function HLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
  665. {
  666. $lookup_value = Functions::flattenSingleValue($lookup_value);
  667. $index_number = Functions::flattenSingleValue($index_number);
  668. $not_exact_match = Functions::flattenSingleValue($not_exact_match);
  669. // index_number must be greater than or equal to 1
  670. if ($index_number < 1) {
  671. return Functions::VALUE();
  672. }
  673. // index_number must be less than or equal to the number of columns in lookup_array
  674. if ((!is_array($lookup_array)) || (empty($lookup_array))) {
  675. return Functions::REF();
  676. }
  677. $f = array_keys($lookup_array);
  678. $firstRow = array_pop($f);
  679. if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) {
  680. return Functions::REF();
  681. }
  682. $columnKeys = array_keys($lookup_array[$firstRow]);
  683. $firstkey = $f[0] - 1;
  684. $returnColumn = $firstkey + $index_number;
  685. $firstColumn = array_shift($f);
  686. if (!$not_exact_match) {
  687. $firstRowH = asort($lookup_array[$firstColumn]);
  688. }
  689. $rowNumber = $rowValue = false;
  690. foreach ($lookup_array[$firstColumn] as $rowKey => $rowData) {
  691. if ((is_numeric($lookup_value) && is_numeric($rowData) && ($rowData > $lookup_value)) ||
  692. (!is_numeric($lookup_value) && !is_numeric($rowData) && (strtolower($rowData) > strtolower($lookup_value)))) {
  693. break;
  694. }
  695. $rowNumber = $rowKey;
  696. $rowValue = $rowData;
  697. }
  698. if ($rowNumber !== false) {
  699. if ((!$not_exact_match) && ($rowValue != $lookup_value)) {
  700. // if an exact match is required, we have what we need to return an appropriate response
  701. return Functions::NA();
  702. }
  703. // otherwise return the appropriate value
  704. return $lookup_array[$returnColumn][$rowNumber];
  705. }
  706. return Functions::NA();
  707. }
  708. /**
  709. * LOOKUP
  710. * The LOOKUP function searches for value either from a one-row or one-column range or from an array.
  711. *
  712. * @param mixed $lookup_value The value that you want to match in lookup_array
  713. * @param mixed $lookup_vector The range of cells being searched
  714. * @param null|mixed $result_vector The column from which the matching value must be returned
  715. *
  716. * @return mixed The value of the found cell
  717. */
  718. public static function LOOKUP($lookup_value, $lookup_vector, $result_vector = null)
  719. {
  720. $lookup_value = Functions::flattenSingleValue($lookup_value);
  721. if (!is_array($lookup_vector)) {
  722. return Functions::NA();
  723. }
  724. $lookupRows = count($lookup_vector);
  725. $l = array_keys($lookup_vector);
  726. $l = array_shift($l);
  727. $lookupColumns = count($lookup_vector[$l]);
  728. if ((($lookupRows == 1) && ($lookupColumns > 1)) || (($lookupRows == 2) && ($lookupColumns != 2))) {
  729. $lookup_vector = self::TRANSPOSE($lookup_vector);
  730. $lookupRows = count($lookup_vector);
  731. $l = array_keys($lookup_vector);
  732. $lookupColumns = count($lookup_vector[array_shift($l)]);
  733. }
  734. if ($result_vector === null) {
  735. $result_vector = $lookup_vector;
  736. }
  737. $resultRows = count($result_vector);
  738. $l = array_keys($result_vector);
  739. $l = array_shift($l);
  740. $resultColumns = count($result_vector[$l]);
  741. if ((($resultRows == 1) && ($resultColumns > 1)) || (($resultRows == 2) && ($resultColumns != 2))) {
  742. $result_vector = self::TRANSPOSE($result_vector);
  743. $resultRows = count($result_vector);
  744. $r = array_keys($result_vector);
  745. $resultColumns = count($result_vector[array_shift($r)]);
  746. }
  747. if ($lookupRows == 2) {
  748. $result_vector = array_pop($lookup_vector);
  749. $lookup_vector = array_shift($lookup_vector);
  750. }
  751. if ($lookupColumns != 2) {
  752. foreach ($lookup_vector as &$value) {
  753. if (is_array($value)) {
  754. $k = array_keys($value);
  755. $key1 = $key2 = array_shift($k);
  756. ++$key2;
  757. $dataValue1 = $value[$key1];
  758. } else {
  759. $key1 = 0;
  760. $key2 = 1;
  761. $dataValue1 = $value;
  762. }
  763. $dataValue2 = array_shift($result_vector);
  764. if (is_array($dataValue2)) {
  765. $dataValue2 = array_shift($dataValue2);
  766. }
  767. $value = [$key1 => $dataValue1, $key2 => $dataValue2];
  768. }
  769. unset($value);
  770. }
  771. return self::VLOOKUP($lookup_value, $lookup_vector, 2);
  772. }
  773. }