Completed
Push — develop ( 4635d3...bf2dbb )
by Adrien
27:59
created

LookupRef::CHOOSE()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 8
nop 1
dl 0
loc 23
ccs 0
cts 14
cp 0
crap 56
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Cell;
6
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7
8
class LookupRef
9
{
10
    /**
11
     * CELL_ADDRESS.
12
     *
13
     * Creates a cell address as text, given specified row and column numbers.
14
     *
15
     * Excel Function:
16
     *        =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText])
17
     *
18
     * @param mixed $row Row number to use in the cell reference
19
     * @param mixed $column Column number to use in the cell reference
20
     * @param int $relativity Flag indicating the type of reference to return
21
     *                                1 or omitted    Absolute
22
     *                                2                Absolute row; relative column
23
     *                                3                Relative row; absolute column
24
     *                                4                Relative
25
     * @param bool $referenceStyle A logical value that specifies the A1 or R1C1 reference style.
26
     *                                TRUE or omitted        CELL_ADDRESS returns an A1-style reference
27
     *                                FALSE                CELL_ADDRESS returns an R1C1-style reference
28
     * @param string $sheetText Optional Name of worksheet to use
29
     *
30
     * @return string
31
     */
32
    public static function cellAddress($row, $column, $relativity = 1, $referenceStyle = true, $sheetText = '')
33
    {
34
        $row = Functions::flattenSingleValue($row);
35
        $column = Functions::flattenSingleValue($column);
36
        $relativity = Functions::flattenSingleValue($relativity);
37
        $sheetText = Functions::flattenSingleValue($sheetText);
38
39
        if (($row < 1) || ($column < 1)) {
40
            return Functions::VALUE();
41
        }
42
43
        if ($sheetText > '') {
44
            if (strpos($sheetText, ' ') !== false) {
45
                $sheetText = "'" . $sheetText . "'";
46
            }
47
            $sheetText .= '!';
48
        }
49
        if ((!is_bool($referenceStyle)) || $referenceStyle) {
50
            $rowRelative = $columnRelative = '$';
51
            $column = Coordinate::stringFromColumnIndex($column);
52
            if (($relativity == 2) || ($relativity == 4)) {
53
                $columnRelative = '';
54
            }
55
            if (($relativity == 3) || ($relativity == 4)) {
56
                $rowRelative = '';
57
            }
58
59
            return $sheetText . $columnRelative . $column . $rowRelative . $row;
60
        }
61
        if (($relativity == 2) || ($relativity == 4)) {
62
            $column = '[' . $column . ']';
63
        }
64
        if (($relativity == 3) || ($relativity == 4)) {
65
            $row = '[' . $row . ']';
66
        }
67
68
        return $sheetText . 'R' . $row . 'C' . $column;
69
    }
70
71
    /**
72
     * COLUMN.
73
     *
74
     * Returns the column number of the given cell reference
75
     * If the cell reference is a range of cells, COLUMN returns the column numbers of each column in the reference as a horizontal array.
76
     * If cell reference is omitted, and the function is being called through the calculation engine, then it is assumed to be the
77
     *        reference of the cell in which the COLUMN function appears; otherwise this function returns 0.
78
     *
79
     * Excel Function:
80
     *        =COLUMN([cellAddress])
81
     *
82
     * @param null|array|string $cellAddress A reference to a range of cells for which you want the column numbers
83
     *
84
     * @return int|int[]
85
     */
86
    public static function COLUMN($cellAddress = null)
87
    {
88
        if ($cellAddress === null || trim($cellAddress) === '') {
1 ignored issue
show
Bug introduced by
It seems like $cellAddress can also be of type array; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        if ($cellAddress === null || trim(/** @scrutinizer ignore-type */ $cellAddress) === '') {
Loading history...
89
            return 0;
90
        }
91
92
        if (is_array($cellAddress)) {
93
            foreach ($cellAddress as $columnKey => $value) {
94
                $columnKey = preg_replace('/[^a-z]/i', '', $columnKey);
95
96
                return (int) Coordinate::columnIndexFromString($columnKey);
97
            }
98
        } else {
99
            if (strpos($cellAddress, '!') !== false) {
100
                list($sheet, $cellAddress) = explode('!', $cellAddress);
101
            }
102
            if (strpos($cellAddress, ':') !== false) {
103
                list($startAddress, $endAddress) = explode(':', $cellAddress);
104
                $startAddress = preg_replace('/[^a-z]/i', '', $startAddress);
105
                $endAddress = preg_replace('/[^a-z]/i', '', $endAddress);
106
                $returnValue = [];
107
                do {
108
                    $returnValue[] = (int) Coordinate::columnIndexFromString($startAddress);
109
                } while ($startAddress++ != $endAddress);
110
111
                return $returnValue;
112
            }
113
            $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress);
114
115
            return (int) Coordinate::columnIndexFromString($cellAddress);
116
        }
117
    }
118
119
    /**
120
     * COLUMNS.
121
     *
122
     * Returns the number of columns in an array or reference.
123
     *
124
     * Excel Function:
125
     *        =COLUMNS(cellAddress)
126
     *
127
     * @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
128
     *
129
     * @return int The number of columns in cellAddress
130
     */
131 8
    public static function COLUMNS($cellAddress = null)
132
    {
133 8
        if ($cellAddress === null || $cellAddress === '') {
134 2
            return 1;
135 6
        } elseif (!is_array($cellAddress)) {
136 1
            return Functions::VALUE();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...tion\Functions::VALUE() returns the type string which is incompatible with the documented return type integer.
Loading history...
137
        }
138
139 5
        reset($cellAddress);
140 5
        $isMatrix = (is_numeric(key($cellAddress)));
141 5
        list($columns, $rows) = Calculation::getMatrixDimensions($cellAddress);
142
143 5
        if ($isMatrix) {
144 3
            return $rows;
145
        }
146
147 2
        return $columns;
148
    }
149
150
    /**
151
     * ROW.
152
     *
153
     * Returns the row number of the given cell reference
154
     * If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference as a vertical array.
155
     * If cell reference is omitted, and the function is being called through the calculation engine, then it is assumed to be the
156
     *        reference of the cell in which the ROW function appears; otherwise this function returns 0.
157
     *
158
     * Excel Function:
159
     *        =ROW([cellAddress])
160
     *
161
     * @param null|array|string $cellAddress A reference to a range of cells for which you want the row numbers
162
     *
163
     * @return int or array of integer
164
     */
165
    public static function ROW($cellAddress = null)
166
    {
167
        if ($cellAddress === null || trim($cellAddress) === '') {
1 ignored issue
show
Bug introduced by
It seems like $cellAddress can also be of type array; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

167
        if ($cellAddress === null || trim(/** @scrutinizer ignore-type */ $cellAddress) === '') {
Loading history...
168
            return 0;
169
        }
170
171
        if (is_array($cellAddress)) {
172
            foreach ($cellAddress as $columnKey => $rowValue) {
173
                foreach ($rowValue as $rowKey => $cellValue) {
174
                    return (int) preg_replace('/\D/', '', $rowKey);
175
                }
176
            }
177
        } else {
178
            if (strpos($cellAddress, '!') !== false) {
179
                list($sheet, $cellAddress) = explode('!', $cellAddress);
180
            }
181
            if (strpos($cellAddress, ':') !== false) {
182
                list($startAddress, $endAddress) = explode(':', $cellAddress);
183
                $startAddress = preg_replace('/\D/', '', $startAddress);
184
                $endAddress = preg_replace('/\D/', '', $endAddress);
185
                $returnValue = [];
186
                do {
187
                    $returnValue[][] = (int) $startAddress;
188
                } while ($startAddress++ != $endAddress);
189
190
                return $returnValue;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $returnValue returns the type array which is incompatible with the documented return type integer.
Loading history...
191
            }
192
            list($cellAddress) = explode(':', $cellAddress);
193
194
            return (int) preg_replace('/\D/', '', $cellAddress);
195
        }
196
    }
197
198
    /**
199
     * ROWS.
200
     *
201
     * Returns the number of rows in an array or reference.
202
     *
203
     * Excel Function:
204
     *        =ROWS(cellAddress)
205
     *
206
     * @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
207
     * @param null|mixed $cellAddress
208
     *
209
     * @return int The number of rows in cellAddress
210
     */
211 8
    public static function ROWS($cellAddress = null)
212
    {
213 8
        if ($cellAddress === null || $cellAddress === '') {
214 2
            return 1;
215 6
        } elseif (!is_array($cellAddress)) {
216 1
            return Functions::VALUE();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...tion\Functions::VALUE() returns the type string which is incompatible with the documented return type integer.
Loading history...
217
        }
218
219 5
        reset($cellAddress);
220 5
        $isMatrix = (is_numeric(key($cellAddress)));
221 5
        list($columns, $rows) = Calculation::getMatrixDimensions($cellAddress);
222
223 5
        if ($isMatrix) {
224 3
            return $columns;
225
        }
226
227 2
        return $rows;
228
    }
229
230
    /**
231
     * HYPERLINK.
232
     *
233
     * Excel Function:
234
     *        =HYPERLINK(linkURL,displayName)
235
     *
236
     * @category Logical Functions
237
     *
238
     * @param string $linkURL Value to check, is also the value returned when no error
239
     * @param string $displayName Value to return when testValue is an error condition
240
     * @param Cell $pCell The cell to set the hyperlink in
241
     *
242
     * @return mixed The value of $displayName (or $linkURL if $displayName was blank)
243
     */
244 1
    public static function HYPERLINK($linkURL = '', $displayName = null, Cell $pCell = null)
245
    {
246 1
        $linkURL = ($linkURL === null) ? '' : Functions::flattenSingleValue($linkURL);
247 1
        $displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
248
249 1
        if ((!is_object($pCell)) || (trim($linkURL) == '')) {
250
            return Functions::REF();
251
        }
252
253 1
        if ((is_object($displayName)) || trim($displayName) == '') {
254
            $displayName = $linkURL;
255
        }
256
257 1
        $pCell->getHyperlink()->setUrl($linkURL);
258 1
        $pCell->getHyperlink()->setTooltip($displayName);
259
260 1
        return $displayName;
261
    }
262
263
    /**
264
     * INDIRECT.
265
     *
266
     * Returns the reference specified by a text string.
267
     * References are immediately evaluated to display their contents.
268
     *
269
     * Excel Function:
270
     *        =INDIRECT(cellAddress)
271
     *
272
     * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010
273
     *
274
     * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
275
     * @param Cell $pCell The current cell (containing this formula)
276
     *
277
     * @return mixed The cells referenced by cellAddress
278
     *
279
     * @todo    Support for the optional a1 parameter introduced in Excel 2010
280
     */
281
    public static function INDIRECT($cellAddress = null, Cell $pCell = null)
282
    {
283
        $cellAddress = Functions::flattenSingleValue($cellAddress);
284
        if ($cellAddress === null || $cellAddress === '') {
285
            return Functions::REF();
286
        }
287
288
        $cellAddress1 = $cellAddress;
289
        $cellAddress2 = null;
290
        if (strpos($cellAddress, ':') !== false) {
291
            list($cellAddress1, $cellAddress2) = explode(':', $cellAddress);
292
        }
293
294
        if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) ||
295
            (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches)))) {
296
            if (!preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $cellAddress1, $matches)) {
297
                return Functions::REF();
298
            }
299
300
            if (strpos($cellAddress, '!') !== false) {
301
                list($sheetName, $cellAddress) = explode('!', $cellAddress);
302
                $sheetName = trim($sheetName, "'");
303
                $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
1 ignored issue
show
Bug introduced by
The method getWorksheet() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

303
                $pSheet = $pCell->/** @scrutinizer ignore-call */ getWorksheet()->getParent()->getSheetByName($sheetName);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
304
            } else {
305
                $pSheet = $pCell->getWorksheet();
306
            }
307
308
            return Calculation::getInstance()->extractNamedRange($cellAddress, $pSheet, false);
309
        }
310
311
        if (strpos($cellAddress, '!') !== false) {
312
            list($sheetName, $cellAddress) = explode('!', $cellAddress);
313
            $sheetName = trim($sheetName, "'");
314
            $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
315
        } else {
316
            $pSheet = $pCell->getWorksheet();
317
        }
318
319
        return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false);
320
    }
321
322
    /**
323
     * OFFSET.
324
     *
325
     * Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells.
326
     * The reference that is returned can be a single cell or a range of cells. You can specify the number of rows and
327
     * the number of columns to be returned.
328
     *
329
     * Excel Function:
330
     *        =OFFSET(cellAddress, rows, cols, [height], [width])
331
     *
332
     * @param null|array|string $cellAddress The reference from which you want to base the offset. Reference must refer to a cell or
333
     *                                range of adjacent cells; otherwise, OFFSET returns the #VALUE! error value.
334
     * @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to.
335
     *                                Using 5 as the rows argument specifies that the upper-left cell in the reference is
336
     *                                five rows below reference. Rows can be positive (which means below the starting reference)
337
     *                                or negative (which means above the starting reference).
338
     * @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell of the result
339
     *                                to refer to. Using 5 as the cols argument specifies that the upper-left cell in the
340
     *                                reference is five columns to the right of reference. Cols can be positive (which means
341
     *                                to the right of the starting reference) or negative (which means to the left of the
342
     *                                starting reference).
343
     * @param mixed $height The height, in number of rows, that you want the returned reference to be. Height must be a positive number.
344
     * @param mixed $width The width, in number of columns, that you want the returned reference to be. Width must be a positive number.
345
     * @param null|Cell $pCell
346
     *
347
     * @return string A reference to a cell or range of cells
348
     */
349
    public static function OFFSET($cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, Cell $pCell = null)
350
    {
351
        $rows = Functions::flattenSingleValue($rows);
352
        $columns = Functions::flattenSingleValue($columns);
353
        $height = Functions::flattenSingleValue($height);
354
        $width = Functions::flattenSingleValue($width);
355
        if ($cellAddress == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cellAddress of type null|string|array against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
356
            return 0;
357
        }
358
359
        if (!is_object($pCell)) {
360
            return Functions::REF();
361
        }
362
363
        $sheetName = null;
364
        if (strpos($cellAddress, '!')) {
1 ignored issue
show
Bug introduced by
It seems like $cellAddress can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

364
        if (strpos(/** @scrutinizer ignore-type */ $cellAddress, '!')) {
Loading history...
365
            list($sheetName, $cellAddress) = explode('!', $cellAddress);
1 ignored issue
show
Bug introduced by
It seems like $cellAddress can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

365
            list($sheetName, $cellAddress) = explode('!', /** @scrutinizer ignore-type */ $cellAddress);
Loading history...
366
            $sheetName = trim($sheetName, "'");
367
        }
368
        if (strpos($cellAddress, ':')) {
369
            list($startCell, $endCell) = explode(':', $cellAddress);
370
        } else {
371
            $startCell = $endCell = $cellAddress;
372
        }
373
        list($startCellColumn, $startCellRow) = Coordinate::coordinateFromString($startCell);
1 ignored issue
show
Bug introduced by
It seems like $startCell can also be of type array; however, parameter $pCoordinateString of PhpOffice\PhpSpreadsheet...:coordinateFromString() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

373
        list($startCellColumn, $startCellRow) = Coordinate::coordinateFromString(/** @scrutinizer ignore-type */ $startCell);
Loading history...
374
        list($endCellColumn, $endCellRow) = Coordinate::coordinateFromString($endCell);
375
376
        $startCellRow += $rows;
377
        $startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1;
378
        $startCellColumn += $columns;
379
380
        if (($startCellRow <= 0) || ($startCellColumn < 0)) {
381
            return Functions::REF();
382
        }
383
        $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1;
384
        if (($width != null) && (!is_object($width))) {
385
            $endCellColumn = $startCellColumn + $width - 1;
386
        } else {
387
            $endCellColumn += $columns;
388
        }
389
        $startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
390
391
        if (($height != null) && (!is_object($height))) {
392
            $endCellRow = $startCellRow + $height - 1;
393
        } else {
394
            $endCellRow += $rows;
395
        }
396
397
        if (($endCellRow <= 0) || ($endCellColumn < 0)) {
398
            return Functions::REF();
399
        }
400
        $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1);
401
402
        $cellAddress = $startCellColumn . $startCellRow;
403
        if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) {
404
            $cellAddress .= ':' . $endCellColumn . $endCellRow;
405
        }
406
407
        if ($sheetName !== null) {
408
            $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
409
        } else {
410
            $pSheet = $pCell->getWorksheet();
411
        }
412
413
        return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false);
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ddress, $pSheet, false) returns the type array which is incompatible with the documented return type string.
Loading history...
414
    }
415
416
    /**
417
     * CHOOSE.
418
     *
419
     * Uses lookup_value to return a value from the list of value arguments.
420
     * Use CHOOSE to select one of up to 254 values based on the lookup_value.
421
     *
422
     * Excel Function:
423
     *        =CHOOSE(index_num, value1, [value2], ...)
424
     *
425
     * @param mixed $index_num Specifies which value argument is selected.
426
     *                            Index_num must be a number between 1 and 254, or a formula or reference to a cell containing a number
427
     *                                between 1 and 254.
428
     * @param mixed $value1... Value1 is required, subsequent values are optional.
429
     *                            Between 1 to 254 value arguments from which CHOOSE selects a value or an action to perform based on
430
     *                                index_num. The arguments can be numbers, cell references, defined names, formulas, functions, or
431
     *                                text.
432
     *
433
     * @return mixed The selected value
434
     */
435
    public static function CHOOSE(...$chooseArgs)
436
    {
437
        $chosenEntry = Functions::flattenArray(array_shift($chooseArgs));
438
        $entryCount = count($chooseArgs) - 1;
439
440
        if (is_array($chosenEntry)) {
441
            $chosenEntry = array_shift($chosenEntry);
442
        }
443
        if ((is_numeric($chosenEntry)) && (!is_bool($chosenEntry))) {
444
            --$chosenEntry;
445
        } else {
446
            return Functions::VALUE();
447
        }
448
        $chosenEntry = floor($chosenEntry);
449
        if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
450
            return Functions::VALUE();
451
        }
452
453
        if (is_array($chooseArgs[$chosenEntry])) {
454
            return Functions::flattenArray($chooseArgs[$chosenEntry]);
455
        }
456
457
        return $chooseArgs[$chosenEntry];
458
    }
459
460
    /**
461
     * MATCH.
462
     *
463
     * The MATCH function searches for a specified item in a range of cells
464
     *
465
     * Excel Function:
466
     *        =MATCH(lookup_value, lookup_array, [match_type])
467
     *
468
     * @param mixed $lookupValue The value that you want to match in lookup_array
469
     * @param mixed $lookupArray The range of cells being searched
470
     * @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.
471
     *
472
     * @return int The relative position of the found item
473
     */
474 16
    public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
475
    {
476 16
        $lookupArray = Functions::flattenArray($lookupArray);
477 16
        $lookupValue = Functions::flattenSingleValue($lookupValue);
478 16
        $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
479
480
        // MATCH is not case sensitive
481 16
        $lookupValue = strtolower($lookupValue);
482
483
        // Lookup_value type has to be number, text, or logical values
484 16
        if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
485
            return Functions::NA();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ulation\Functions::NA() returns the type string which is incompatible with the documented return type integer.
Loading history...
486
        }
487
488
        // Match_type is 0, 1 or -1
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
489 16
        if (($matchType !== 0) && ($matchType !== -1) && ($matchType !== 1)) {
490
            return Functions::NA();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ulation\Functions::NA() returns the type string which is incompatible with the documented return type integer.
Loading history...
491
        }
492
493
        // Lookup_array should not be empty
494 16
        $lookupArraySize = count($lookupArray);
495 16
        if ($lookupArraySize <= 0) {
496
            return Functions::NA();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ulation\Functions::NA() returns the type string which is incompatible with the documented return type integer.
Loading history...
497
        }
498
499
        // Lookup_array should contain only number, text, or logical values, or empty (null) cells
500 16
        foreach ($lookupArray as $i => $lookupArrayValue) {
501
            //    check the type of the value
502 16
            if ((!is_numeric($lookupArrayValue)) && (!is_string($lookupArrayValue)) &&
503 16
                (!is_bool($lookupArrayValue)) && ($lookupArrayValue !== null)
504
            ) {
505
                return Functions::NA();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ulation\Functions::NA() returns the type string which is incompatible with the documented return type integer.
Loading history...
506
            }
507
            // Convert strings to lowercase for case-insensitive testing
508 16
            if (is_string($lookupArrayValue)) {
509
                $lookupArray[$i] = strtolower($lookupArrayValue);
510
            }
511 16
            if (($lookupArrayValue === null) && (($matchType == 1) || ($matchType == -1))) {
512 16
                $lookupArray = array_slice($lookupArray, 0, $i - 1);
513
            }
514
        }
515
516 16
        if ($matchType == 1) {
517
            // If match_type is 1 the list has to be processed from last to first
518
519 4
            $lookupArray = array_reverse($lookupArray);
520 4
            $keySet = array_reverse(array_keys($lookupArray));
521
        }
522
523
        // **
524
        // find the match
525
        // **
526
527 16
        if ($matchType == 0 || $matchType == 1) {
528 9
            foreach ($lookupArray as $i => $lookupArrayValue) {
529 9
                if (($matchType == 0) && ($lookupArrayValue == $lookupValue)) {
530
                    //    exact match
531 4
                    return ++$i;
532 7
                } elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) {
533 4
                    $i = array_search($i, $keySet);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $keySet does not seem to be defined for all execution paths leading up to this point.
Loading history...
534
535
                    // The current value is the (first) match
536 7
                    return $i + 1;
537
                }
538
            }
539
        } else {
540
            // matchType = -1
541
542
            // "Special" case: since the array it's supposed to be ordered in descending order, the
543
            // Excel algorithm gives up immediately if the first element is smaller than the searched value
544 7
            if ($lookupArray[0] < $lookupValue) {
545 1
                return Functions::NA();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ulation\Functions::NA() returns the type string which is incompatible with the documented return type integer.
Loading history...
546
            }
547
548 6
            $maxValueKey = null;
549
550
            // The basic algorithm is:
551
            // Iterate and keep the highest match until the next element is smaller than the searched value.
552
            // Return immediately if perfect match is found
553 6
            foreach ($lookupArray as $i => $lookupArrayValue) {
554 6
                if ($lookupArrayValue == $lookupValue) {
555
                    // Another "special" case. If a perfect match is found,
556
                    // the algorithm gives up immediately
557 2
                    return $i + 1;
558 5
                } elseif ($lookupArrayValue >= $lookupValue) {
559 5
                    $maxValueKey = $i + 1;
560
                }
561
            }
562
563 4
            if ($maxValueKey !== null) {
564 4
                return $maxValueKey;
565
            }
566
        }
567
568
        // Unsuccessful in finding a match, return #N/A error value
569 2
        return Functions::NA();
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpOffice\PhpSpre...ulation\Functions::NA() returns the type string which is incompatible with the documented return type integer.
Loading history...
570
    }
571
572
    /**
573
     * INDEX.
574
     *
575
     * Uses an index to choose a value from a reference or array
576
     *
577
     * Excel Function:
578
     *        =INDEX(range_array, row_num, [column_num])
579
     *
580
     * @param mixed $arrayValues A range of cells or an array constant
581
     * @param mixed $rowNum The row in array from which to return a value. If row_num is omitted, column_num is required.
582
     * @param mixed $columnNum The column in array from which to return a value. If column_num is omitted, row_num is required.
583
     *
584
     * @return mixed the value of a specified cell or array of cells
585
     */
586 9
    public static function INDEX($arrayValues, $rowNum = 0, $columnNum = 0)
587
    {
588 9
        $rowNum = Functions::flattenSingleValue($rowNum);
589 9
        $columnNum = Functions::flattenSingleValue($columnNum);
590
591 9
        if (($rowNum < 0) || ($columnNum < 0)) {
592 2
            return Functions::VALUE();
593
        }
594
595 7
        if (!is_array($arrayValues) || ($rowNum > count($arrayValues))) {
596 1
            return Functions::REF();
597
        }
598
599 6
        $rowKeys = array_keys($arrayValues);
600 6
        $columnKeys = @array_keys($arrayValues[$rowKeys[0]]);
601
602 6
        if ($columnNum > count($columnKeys)) {
603 1
            return Functions::VALUE();
604 5
        } elseif ($columnNum == 0) {
605 3
            if ($rowNum == 0) {
606 1
                return $arrayValues;
607
            }
608 2
            $rowNum = $rowKeys[--$rowNum];
609 2
            $returnArray = [];
610 2
            foreach ($arrayValues as $arrayColumn) {
611 2
                if (is_array($arrayColumn)) {
612 2
                    if (isset($arrayColumn[$rowNum])) {
613
                        $returnArray[] = $arrayColumn[$rowNum];
614
                    } else {
615 2
                        return [$rowNum => $arrayValues[$rowNum]];
616
                    }
617
                } else {
618
                    return $arrayValues[$rowNum];
619
                }
620
            }
621
622
            return $returnArray;
623
        }
624 2
        $columnNum = $columnKeys[--$columnNum];
625 2
        if ($rowNum > count($rowKeys)) {
626
            return Functions::VALUE();
627 2
        } elseif ($rowNum == 0) {
628
            return $arrayValues[$columnNum];
629
        }
630 2
        $rowNum = $rowKeys[--$rowNum];
631
632 2
        return $arrayValues[$rowNum][$columnNum];
633
    }
634
635
    /**
636
     * TRANSPOSE.
637
     *
638
     * @param array $matrixData A matrix of values
639
     *
640
     * @return array
641
     *
642
     * Unlike the Excel TRANSPOSE function, which will only work on a single row or column, this function will transpose a full matrix
643
     */
644
    public static function TRANSPOSE($matrixData)
645
    {
646
        $returnMatrix = [];
647
        if (!is_array($matrixData)) {
648
            $matrixData = [[$matrixData]];
649
        }
650
651
        $column = 0;
652
        foreach ($matrixData as $matrixRow) {
653
            $row = 0;
654
            foreach ($matrixRow as $matrixCell) {
655
                $returnMatrix[$row][$column] = $matrixCell;
656
                ++$row;
657
            }
658
            ++$column;
659
        }
660
661
        return $returnMatrix;
662
    }
663
664 2
    private static function vlookupSort($a, $b)
665
    {
666 2
        reset($a);
667 2
        $firstColumn = key($a);
668 2
        if (($aLower = strtolower($a[$firstColumn])) == ($bLower = strtolower($b[$firstColumn]))) {
669
            return 0;
670
        }
671
672 2
        return ($aLower < $bLower) ? -1 : 1;
673
    }
674
675
    /**
676
     * VLOOKUP
677
     * 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.
678
     *
679
     * @param mixed $lookup_value The value that you want to match in lookup_array
680
     * @param mixed $lookup_array The range of cells being searched
681
     * @param mixed $index_number The column number in table_array from which the matching value must be returned. The first column is 1.
682
     * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
683
     *
684
     * @return mixed The value of the found cell
685
     */
686 5
    public static function VLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
687
    {
688 5
        $lookup_value = Functions::flattenSingleValue($lookup_value);
689 5
        $index_number = Functions::flattenSingleValue($index_number);
690 5
        $not_exact_match = Functions::flattenSingleValue($not_exact_match);
691
692
        // index_number must be greater than or equal to 1
693 5
        if ($index_number < 1) {
694
            return Functions::VALUE();
695
        }
696
697
        // index_number must be less than or equal to the number of columns in lookup_array
698 5
        if ((!is_array($lookup_array)) || (empty($lookup_array))) {
699
            return Functions::REF();
700
        }
701 5
        $f = array_keys($lookup_array);
702 5
        $firstRow = array_pop($f);
703 5
        if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array[$firstRow]))) {
704
            return Functions::REF();
705
        }
706 5
        $columnKeys = array_keys($lookup_array[$firstRow]);
707 5
        $returnColumn = $columnKeys[--$index_number];
708 5
        $firstColumn = array_shift($columnKeys);
709
710 5
        if (!$not_exact_match) {
711 2
            uasort($lookup_array, ['self', 'vlookupSort']);
712
        }
713
714 5
        $rowNumber = $rowValue = false;
715 5
        foreach ($lookup_array as $rowKey => $rowData) {
716 5
            if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn]) && ($rowData[$firstColumn] > $lookup_value)) ||
717 5
                (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]) && (strtolower($rowData[$firstColumn]) > strtolower($lookup_value)))) {
718 4
                break;
719
            }
720
            // remember the last key, but only if datatypes match
721 5
            if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn])) ||
722 5
                (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]))) {
723 4
                $rowNumber = $rowKey;
724 5
                $rowValue = $rowData[$firstColumn];
725
            }
726
        }
727
728 5
        if ($rowNumber !== false) {
729 4
            if ((!$not_exact_match) && ($rowValue != $lookup_value)) {
730
                //    if an exact match is required, we have what we need to return an appropriate response
731 2
                return Functions::NA();
732
            }
733
            //    otherwise return the appropriate value
734 2
            return $lookup_array[$rowNumber][$returnColumn];
735
        }
736
737 1
        return Functions::NA();
738
    }
739
740
    /**
741
     * HLOOKUP
742
     * 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.
743
     *
744
     * @param mixed $lookup_value The value that you want to match in lookup_array
745
     * @param mixed $lookup_array The range of cells being searched
746
     * @param mixed $index_number The row number in table_array from which the matching value must be returned. The first row is 1.
747
     * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
748
     *
749
     * @return mixed The value of the found cell
750
     */
751 9
    public static function HLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
752
    {
753 9
        $lookup_value = Functions::flattenSingleValue($lookup_value);
754 9
        $index_number = Functions::flattenSingleValue($index_number);
755 9
        $not_exact_match = Functions::flattenSingleValue($not_exact_match);
756
757
        // index_number must be greater than or equal to 1
758 9
        if ($index_number < 1) {
759
            return Functions::VALUE();
760
        }
761
762
        // index_number must be less than or equal to the number of columns in lookup_array
763 9
        if ((!is_array($lookup_array)) || (empty($lookup_array))) {
764
            return Functions::REF();
765
        }
766 9
        $f = array_keys($lookup_array);
767 9
        $firstRow = array_pop($f);
768 9
        if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) {
769
            return Functions::REF();
770
        }
771 9
        $columnKeys = array_keys($lookup_array[$firstRow]);
0 ignored issues
show
Unused Code introduced by
The assignment to $columnKeys is dead and can be removed.
Loading history...
772 9
        $firstkey = $f[0] - 1;
773 9
        $returnColumn = $firstkey + $index_number;
774 9
        $firstColumn = array_shift($f);
775
776 9
        if (!$not_exact_match) {
777 4
            $firstRowH = asort($lookup_array[$firstColumn]);
0 ignored issues
show
Unused Code introduced by
The assignment to $firstRowH is dead and can be removed.
Loading history...
778
        }
779 9
        $rowNumber = $rowValue = false;
780 9
        foreach ($lookup_array[$firstColumn] as $rowKey => $rowData) {
781 9
            if ((is_numeric($lookup_value) && is_numeric($rowData) && ($rowData > $lookup_value)) ||
782 9
                (!is_numeric($lookup_value) && !is_numeric($rowData) && (strtolower($rowData) > strtolower($lookup_value)))) {
783 7
                break;
784
            }
785 9
            $rowNumber = $rowKey;
786 9
            $rowValue = $rowData;
787
        }
788
789 9
        if ($rowNumber !== false) {
790 9
            if ((!$not_exact_match) && ($rowValue != $lookup_value)) {
791
                //  if an exact match is required, we have what we need to return an appropriate response
792 1
                return Functions::NA();
793
            }
794
            //  otherwise return the appropriate value
795 8
            return $lookup_array[$returnColumn][$rowNumber];
796
        }
797
798
        return Functions::NA();
799
    }
800
801
    /**
802
     * LOOKUP
803
     * The LOOKUP function searches for value either from a one-row or one-column range or from an array.
804
     *
805
     * @param mixed $lookup_value The value that you want to match in lookup_array
806
     * @param mixed $lookup_vector The range of cells being searched
807
     * @param null|mixed $result_vector The column from which the matching value must be returned
808
     *
809
     * @return mixed The value of the found cell
810
     */
811
    public static function LOOKUP($lookup_value, $lookup_vector, $result_vector = null)
812
    {
813
        $lookup_value = Functions::flattenSingleValue($lookup_value);
814
815
        if (!is_array($lookup_vector)) {
816
            return Functions::NA();
817
        }
818
        $lookupRows = count($lookup_vector);
819
        $l = array_keys($lookup_vector);
820
        $l = array_shift($l);
821
        $lookupColumns = count($lookup_vector[$l]);
822
        if ((($lookupRows == 1) && ($lookupColumns > 1)) || (($lookupRows == 2) && ($lookupColumns != 2))) {
823
            $lookup_vector = self::TRANSPOSE($lookup_vector);
824
            $lookupRows = count($lookup_vector);
825
            $l = array_keys($lookup_vector);
826
            $lookupColumns = count($lookup_vector[array_shift($l)]);
827
        }
828
829
        if ($result_vector === null) {
830
            $result_vector = $lookup_vector;
831
        }
832
        $resultRows = count($result_vector);
833
        $l = array_keys($result_vector);
834
        $l = array_shift($l);
835
        $resultColumns = count($result_vector[$l]);
836
        if ((($resultRows == 1) && ($resultColumns > 1)) || (($resultRows == 2) && ($resultColumns != 2))) {
837
            $result_vector = self::TRANSPOSE($result_vector);
838
            $resultRows = count($result_vector);
0 ignored issues
show
Unused Code introduced by
The assignment to $resultRows is dead and can be removed.
Loading history...
839
            $r = array_keys($result_vector);
840
            $resultColumns = count($result_vector[array_shift($r)]);
0 ignored issues
show
Unused Code introduced by
The assignment to $resultColumns is dead and can be removed.
Loading history...
841
        }
842
843
        if ($lookupRows == 2) {
844
            $result_vector = array_pop($lookup_vector);
845
            $lookup_vector = array_shift($lookup_vector);
846
        }
847
        if ($lookupColumns != 2) {
848
            foreach ($lookup_vector as &$value) {
849
                if (is_array($value)) {
850
                    $k = array_keys($value);
851
                    $key1 = $key2 = array_shift($k);
852
                    ++$key2;
853
                    $dataValue1 = $value[$key1];
854
                } else {
855
                    $key1 = 0;
856
                    $key2 = 1;
857
                    $dataValue1 = $value;
858
                }
859
                $dataValue2 = array_shift($result_vector);
860
                if (is_array($dataValue2)) {
861
                    $dataValue2 = array_shift($dataValue2);
862
                }
863
                $value = [$key1 => $dataValue1, $key2 => $dataValue2];
864
            }
865
            unset($value);
866
        }
867
868
        return self::VLOOKUP($lookup_value, $lookup_vector, 2);
869
    }
870
}
871