Passed
Push — master ( 95c8bb...bbbfdb )
by Adrien
29:49 queued 23:07
created

LookupRef::TRANSPOSE()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.0092

Importance

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

90
        if ($cellAddress === null || trim(/** @scrutinizer ignore-type */ $cellAddress) === '') {
Loading history...
91
            return 0;
92
        }
93
94
        if (is_array($cellAddress)) {
95
            foreach ($cellAddress as $columnKey => $value) {
96
                $columnKey = preg_replace('/[^a-z]/i', '', $columnKey);
97
98
                return (int) Coordinate::columnIndexFromString($columnKey);
99
            }
100
        } else {
101
            [$sheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
102
            if (strpos($cellAddress, ':') !== false) {
103
                [$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
        [$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 1
    public static function ROW($cellAddress = null)
166
    {
167 1
        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 1
        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 1
            [$sheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
179 1
            if (strpos($cellAddress, ':') !== false) {
180
                [$startAddress, $endAddress] = explode(':', $cellAddress);
181
                $startAddress = preg_replace('/\D/', '', $startAddress);
182
                $endAddress = preg_replace('/\D/', '', $endAddress);
183
                $returnValue = [];
184
                do {
185
                    $returnValue[][] = (int) $startAddress;
186
                } while ($startAddress++ != $endAddress);
187
188
                return $returnValue;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $returnValue returns the type array<mixed,integer[]> which is incompatible with the documented return type integer.
Loading history...
189
            }
190 1
            [$cellAddress] = explode(':', $cellAddress);
191
192 1
            return (int) preg_replace('/\D/', '', $cellAddress);
193
        }
194
    }
195
196
    /**
197
     * ROWS.
198
     *
199
     * Returns the number of rows in an array or reference.
200
     *
201
     * Excel Function:
202
     *        =ROWS(cellAddress)
203
     *
204
     * @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
205
     *
206
     * @return int The number of rows in cellAddress
207
     */
208 8
    public static function ROWS($cellAddress = null)
209
    {
210 8
        if ($cellAddress === null || $cellAddress === '') {
211 2
            return 1;
212 6
        } elseif (!is_array($cellAddress)) {
213 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...
214
        }
215
216 5
        reset($cellAddress);
217 5
        $isMatrix = (is_numeric(key($cellAddress)));
218 5
        [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress);
219
220 5
        if ($isMatrix) {
221 3
            return $columns;
222
        }
223
224 2
        return $rows;
225
    }
226
227
    /**
228
     * HYPERLINK.
229
     *
230
     * Excel Function:
231
     *        =HYPERLINK(linkURL,displayName)
232
     *
233
     * @category Logical Functions
234
     *
235
     * @param string $linkURL Value to check, is also the value returned when no error
236
     * @param string $displayName Value to return when testValue is an error condition
237
     * @param Cell $pCell The cell to set the hyperlink in
238
     *
239
     * @return mixed The value of $displayName (or $linkURL if $displayName was blank)
240
     */
241 1
    public static function HYPERLINK($linkURL = '', $displayName = null, Cell $pCell = null)
242
    {
243 1
        $linkURL = ($linkURL === null) ? '' : Functions::flattenSingleValue($linkURL);
0 ignored issues
show
introduced by
The condition $linkURL === null is always false.
Loading history...
244 1
        $displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
245
246 1
        if ((!is_object($pCell)) || (trim($linkURL) == '')) {
247
            return Functions::REF();
248
        }
249
250 1
        if ((is_object($displayName)) || trim($displayName) == '') {
251
            $displayName = $linkURL;
252
        }
253
254 1
        $pCell->getHyperlink()->setUrl($linkURL);
255 1
        $pCell->getHyperlink()->setTooltip($displayName);
256
257 1
        return $displayName;
258
    }
259
260
    /**
261
     * INDIRECT.
262
     *
263
     * Returns the reference specified by a text string.
264
     * References are immediately evaluated to display their contents.
265
     *
266
     * Excel Function:
267
     *        =INDIRECT(cellAddress)
268
     *
269
     * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010
270
     *
271
     * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
272
     * @param Cell $pCell The current cell (containing this formula)
273
     *
274
     * @return mixed The cells referenced by cellAddress
275
     *
276
     * @todo    Support for the optional a1 parameter introduced in Excel 2010
277
     */
278 1
    public static function INDIRECT($cellAddress = null, Cell $pCell = null)
279
    {
280 1
        $cellAddress = Functions::flattenSingleValue($cellAddress);
281 1
        if ($cellAddress === null || $cellAddress === '') {
282
            return Functions::REF();
283
        }
284
285 1
        $cellAddress1 = $cellAddress;
286 1
        $cellAddress2 = null;
287 1
        if (strpos($cellAddress, ':') !== false) {
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

287
        if (strpos(/** @scrutinizer ignore-type */ $cellAddress, ':') !== false) {
Loading history...
288
            [$cellAddress1, $cellAddress2] = 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

288
            [$cellAddress1, $cellAddress2] = explode(':', /** @scrutinizer ignore-type */ $cellAddress);
Loading history...
289
        }
290
291 1
        if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) ||
1 ignored issue
show
Bug introduced by
It seems like $cellAddress1 can also be of type array; however, parameter $subject of preg_match() 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

291
        if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', /** @scrutinizer ignore-type */ $cellAddress1, $matches)) ||
Loading history...
292 1
            (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches)))) {
293
            if (!preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $cellAddress1, $matches)) {
294
                return Functions::REF();
295
            }
296
297
            if (strpos($cellAddress, '!') !== false) {
298
                [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
1 ignored issue
show
Bug introduced by
It seems like $cellAddress can also be of type array; however, parameter $pRange of PhpOffice\PhpSpreadsheet...et::extractSheetTitle() 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

298
                [$sheetName, $cellAddress] = Worksheet::extractSheetTitle(/** @scrutinizer ignore-type */ $cellAddress, true);
Loading history...
299
                $sheetName = trim($sheetName, "'");
300
                $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

300
                $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...
301
            } else {
302
                $pSheet = $pCell->getWorksheet();
303
            }
304
305
            return Calculation::getInstance()->extractNamedRange($cellAddress, $pSheet, false);
306
        }
307
308 1
        if (strpos($cellAddress, '!') !== false) {
309
            [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
310
            $sheetName = trim($sheetName, "'");
311
            $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
312
        } else {
313 1
            $pSheet = $pCell->getWorksheet();
314
        }
315
316 1
        return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false);
317
    }
318
319
    /**
320
     * OFFSET.
321
     *
322
     * Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells.
323
     * The reference that is returned can be a single cell or a range of cells. You can specify the number of rows and
324
     * the number of columns to be returned.
325
     *
326
     * Excel Function:
327
     *        =OFFSET(cellAddress, rows, cols, [height], [width])
328
     *
329
     * @param null|string $cellAddress The reference from which you want to base the offset. Reference must refer to a cell or
330
     *                                range of adjacent cells; otherwise, OFFSET returns the #VALUE! error value.
331
     * @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to.
332
     *                                Using 5 as the rows argument specifies that the upper-left cell in the reference is
333
     *                                five rows below reference. Rows can be positive (which means below the starting reference)
334
     *                                or negative (which means above the starting reference).
335
     * @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell of the result
336
     *                                to refer to. Using 5 as the cols argument specifies that the upper-left cell in the
337
     *                                reference is five columns to the right of reference. Cols can be positive (which means
338
     *                                to the right of the starting reference) or negative (which means to the left of the
339
     *                                starting reference).
340
     * @param mixed $height The height, in number of rows, that you want the returned reference to be. Height must be a positive number.
341
     * @param mixed $width The width, in number of columns, that you want the returned reference to be. Width must be a positive number.
342
     * @param null|Cell $pCell
343
     *
344
     * @return string A reference to a cell or range of cells
345
     */
346 1
    public static function OFFSET($cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, Cell $pCell = null)
347
    {
348 1
        $rows = Functions::flattenSingleValue($rows);
349 1
        $columns = Functions::flattenSingleValue($columns);
350 1
        $height = Functions::flattenSingleValue($height);
351 1
        $width = Functions::flattenSingleValue($width);
352 1
        if ($cellAddress === null) {
353
            return 0;
354
        }
355
356 1
        if (!is_object($pCell)) {
357
            return Functions::REF();
358
        }
359
360 1
        $sheetName = null;
361 1
        if (strpos($cellAddress, '!')) {
362
            [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
363
            $sheetName = trim($sheetName, "'");
364
        }
365 1
        if (strpos($cellAddress, ':')) {
366
            [$startCell, $endCell] = explode(':', $cellAddress);
367
        } else {
368 1
            $startCell = $endCell = $cellAddress;
369
        }
370 1
        [$startCellColumn, $startCellRow] = Coordinate::coordinateFromString($startCell);
371 1
        [$endCellColumn, $endCellRow] = Coordinate::coordinateFromString($endCell);
372
373 1
        $startCellRow += $rows;
374 1
        $startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1;
375 1
        $startCellColumn += $columns;
376
377 1
        if (($startCellRow <= 0) || ($startCellColumn < 0)) {
378
            return Functions::REF();
379
        }
380 1
        $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1;
381 1
        if (($width != null) && (!is_object($width))) {
382 1
            $endCellColumn = $startCellColumn + $width - 1;
383
        } else {
384 1
            $endCellColumn += $columns;
385
        }
386 1
        $startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
387
388 1
        if (($height != null) && (!is_object($height))) {
389 1
            $endCellRow = $startCellRow + $height - 1;
390
        } else {
391 1
            $endCellRow += $rows;
392
        }
393
394 1
        if (($endCellRow <= 0) || ($endCellColumn < 0)) {
395
            return Functions::REF();
396
        }
397 1
        $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1);
398
399 1
        $cellAddress = $startCellColumn . $startCellRow;
400 1
        if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) {
401
            $cellAddress .= ':' . $endCellColumn . $endCellRow;
402
        }
403
404 1
        if ($sheetName !== null) {
405
            $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
406
        } else {
407 1
            $pSheet = $pCell->getWorksheet();
408
        }
409
410 1
        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...
411
    }
412
413
    /**
414
     * CHOOSE.
415
     *
416
     * Uses lookup_value to return a value from the list of value arguments.
417
     * Use CHOOSE to select one of up to 254 values based on the lookup_value.
418
     *
419
     * Excel Function:
420
     *        =CHOOSE(index_num, value1, [value2], ...)
421
     *
422
     * @param mixed $index_num Specifies which value argument is selected.
423
     *                            Index_num must be a number between 1 and 254, or a formula or reference to a cell containing a number
424
     *                                between 1 and 254.
425
     * @param mixed $value1 ... Value1 is required, subsequent values are optional.
426
     *                            Between 1 to 254 value arguments from which CHOOSE selects a value or an action to perform based on
427
     *                                index_num. The arguments can be numbers, cell references, defined names, formulas, functions, or
428
     *                                text.
429
     *
430
     * @return mixed The selected value
431
     */
432
    public static function CHOOSE(...$chooseArgs)
433
    {
434
        $chosenEntry = Functions::flattenArray(array_shift($chooseArgs));
435
        $entryCount = count($chooseArgs) - 1;
436
437
        if (is_array($chosenEntry)) {
0 ignored issues
show
introduced by
The condition is_array($chosenEntry) is always true.
Loading history...
438
            $chosenEntry = array_shift($chosenEntry);
439
        }
440
        if ((is_numeric($chosenEntry)) && (!is_bool($chosenEntry))) {
441
            --$chosenEntry;
442
        } else {
443
            return Functions::VALUE();
444
        }
445
        $chosenEntry = floor($chosenEntry);
446
        if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
447
            return Functions::VALUE();
448
        }
449
450
        if (is_array($chooseArgs[$chosenEntry])) {
451
            return Functions::flattenArray($chooseArgs[$chosenEntry]);
452
        }
453
454
        return $chooseArgs[$chosenEntry];
455
    }
456
457
    /**
458
     * MATCH.
459
     *
460
     * The MATCH function searches for a specified item in a range of cells
461
     *
462
     * Excel Function:
463
     *        =MATCH(lookup_value, lookup_array, [match_type])
464
     *
465
     * @param mixed $lookupValue The value that you want to match in lookup_array
466
     * @param mixed $lookupArray The range of cells being searched
467
     * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
468
     *                         If match_type is 1 or -1, the list has to be ordered.
469
     *
470
     * @return int|string The relative position of the found item
471
     */
472 40
    public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
473
    {
474 40
        $lookupArray = Functions::flattenArray($lookupArray);
475 40
        $lookupValue = Functions::flattenSingleValue($lookupValue);
476 40
        $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
477
478
        // MATCH is not case sensitive, so we convert lookup value to be lower cased in case it's string type.
479 40
        if (is_string($lookupValue)) {
480 13
            $lookupValue = StringHelper::strToLower($lookupValue);
481
        }
482
483
        // Lookup_value type has to be number, text, or logical values
484 40
        if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
485
            return Functions::NA();
486
        }
487
488
        // Match_type is 0, 1 or -1
489 40
        if (($matchType !== 0) && ($matchType !== -1) && ($matchType !== 1)) {
490
            return Functions::NA();
491
        }
492
493
        // Lookup_array should not be empty
494 40
        $lookupArraySize = count($lookupArray);
495 40
        if ($lookupArraySize <= 0) {
496
            return Functions::NA();
497
        }
498
499
        // Lookup_array should contain only number, text, or logical values, or empty (null) cells
500 40
        foreach ($lookupArray as $i => $lookupArrayValue) {
501
            //    check the type of the value
502 40
            if ((!is_numeric($lookupArrayValue)) && (!is_string($lookupArrayValue)) &&
503 40
                (!is_bool($lookupArrayValue)) && ($lookupArrayValue !== null)
504
            ) {
505
                return Functions::NA();
506
            }
507
            // Convert strings to lowercase for case-insensitive testing
508 40
            if (is_string($lookupArrayValue)) {
509 17
                $lookupArray[$i] = StringHelper::strToLower($lookupArrayValue);
510
            }
511 40
            if (($lookupArrayValue === null) && (($matchType == 1) || ($matchType == -1))) {
512
                $lookupArray = array_slice($lookupArray, 0, $i - 1);
513
            }
514
        }
515
516 40
        if ($matchType == 1) {
517
            // If match_type is 1 the list has to be processed from last to first
518
519 6
            $lookupArray = array_reverse($lookupArray);
520 6
            $keySet = array_reverse(array_keys($lookupArray));
521
        }
522
523
        // **
524
        // find the match
525
        // **
526
527 40
        if ($matchType === 0 || $matchType === 1) {
528 25
            foreach ($lookupArray as $i => $lookupArrayValue) {
529 25
                $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
530 25
                $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
531 25
                $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
532 25
                $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
533
534 25
                if ($matchType === 0) {
535 19
                    if ($typeMatch && is_string($lookupValue) && (bool) preg_match('/([\?\*])/', $lookupValue)) {
536 10
                        $splitString = $lookupValue;
537
                        $chars = array_map(function ($i) use ($splitString) {
538 10
                            return mb_substr($splitString, $i, 1);
539 10
                        }, range(0, mb_strlen($splitString) - 1));
540
541 10
                        $length = count($chars);
542 10
                        $pattern = '/^';
543 10
                        for ($j = 0; $j < $length; ++$j) {
544 10
                            if ($chars[$j] === '~') {
545 1
                                if (isset($chars[$j + 1])) {
546 1
                                    if ($chars[$j + 1] === '*') {
547 1
                                        $pattern .= preg_quote($chars[$j + 1], '/');
548 1
                                        ++$j;
549
                                    } elseif ($chars[$j + 1] === '?') {
550
                                        $pattern .= preg_quote($chars[$j + 1], '/');
551 1
                                        ++$j;
552
                                    }
553
                                } else {
554 1
                                    $pattern .= preg_quote($chars[$j], '/');
555
                                }
556 10
                            } elseif ($chars[$j] === '*') {
557 7
                                $pattern .= '.*';
558 8
                            } elseif ($chars[$j] === '?') {
559 3
                                $pattern .= '.{1}';
560
                            } else {
561 6
                                $pattern .= preg_quote($chars[$j], '/');
562
                            }
563
                        }
564
565 10
                        $pattern .= '$/';
566 10
                        if ((bool) preg_match($pattern, $lookupArrayValue)) {
567
                            // exact match
568 10
                            return $i + 1;
569
                        }
570 11
                    } elseif ($exactMatch) {
571
                        // exact match
572 17
                        return $i + 1;
573
                    }
574 6
                } elseif (($matchType === 1) && $typeMatch && ($lookupArrayValue <= $lookupValue)) {
575 5
                    $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...
576
577
                    // The current value is the (first) match
578 5
                    return $i + 1;
579
                }
580
            }
581
        } else {
582 15
            $maxValueKey = null;
583
584
            // The basic algorithm is:
585
            // Iterate and keep the highest match until the next element is smaller than the searched value.
586
            // Return immediately if perfect match is found
587 15
            foreach ($lookupArray as $i => $lookupArrayValue) {
588 15
                $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
589 15
                $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
590 15
                $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
591 15
                $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
592
593 15
                if ($exactMatch) {
594
                    // Another "special" case. If a perfect match is found,
595
                    // the algorithm gives up immediately
596 4
                    return $i + 1;
597 13
                } elseif ($typeMatch & $lookupArrayValue >= $lookupValue) {
0 ignored issues
show
Bug introduced by
Are you sure you want to use the bitwise & or did you mean &&?
Loading history...
598 6
                    $maxValueKey = $i + 1;
599 11
                } elseif ($typeMatch & $lookupArrayValue < $lookupValue) {
0 ignored issues
show
Bug introduced by
Are you sure you want to use the bitwise & or did you mean &&?
Loading history...
600
                    //Excel algorithm gives up immediately if the first element is smaller than the searched value
601 6
                    break;
602
                }
603
            }
604
605 11
            if ($maxValueKey !== null) {
606 5
                return $maxValueKey;
607
            }
608
        }
609
610
        // Unsuccessful in finding a match, return #N/A error value
611 11
        return Functions::NA();
612
    }
613
614
    /**
615
     * INDEX.
616
     *
617
     * Uses an index to choose a value from a reference or array
618
     *
619
     * Excel Function:
620
     *        =INDEX(range_array, row_num, [column_num])
621
     *
622
     * @param mixed $arrayValues A range of cells or an array constant
623
     * @param mixed $rowNum The row in array from which to return a value. If row_num is omitted, column_num is required.
624
     * @param mixed $columnNum The column in array from which to return a value. If column_num is omitted, row_num is required.
625
     *
626
     * @return mixed the value of a specified cell or array of cells
627
     */
628 9
    public static function INDEX($arrayValues, $rowNum = 0, $columnNum = 0)
629
    {
630 9
        $rowNum = Functions::flattenSingleValue($rowNum);
631 9
        $columnNum = Functions::flattenSingleValue($columnNum);
632
633 9
        if (($rowNum < 0) || ($columnNum < 0)) {
634 2
            return Functions::VALUE();
635
        }
636
637 7
        if (!is_array($arrayValues) || ($rowNum > count($arrayValues))) {
638 1
            return Functions::REF();
639
        }
640
641 6
        $rowKeys = array_keys($arrayValues);
642 6
        $columnKeys = @array_keys($arrayValues[$rowKeys[0]]);
643
644 6
        if ($columnNum > count($columnKeys)) {
645 1
            return Functions::VALUE();
646 5
        } elseif ($columnNum == 0) {
647 3
            if ($rowNum == 0) {
648 1
                return $arrayValues;
649
            }
650 2
            $rowNum = $rowKeys[--$rowNum];
651 2
            $returnArray = [];
652 2
            foreach ($arrayValues as $arrayColumn) {
653 2
                if (is_array($arrayColumn)) {
654 2
                    if (isset($arrayColumn[$rowNum])) {
655
                        $returnArray[] = $arrayColumn[$rowNum];
656
                    } else {
657 2
                        return [$rowNum => $arrayValues[$rowNum]];
658
                    }
659
                } else {
660
                    return $arrayValues[$rowNum];
661
                }
662
            }
663
664
            return $returnArray;
665
        }
666 2
        $columnNum = $columnKeys[--$columnNum];
667 2
        if ($rowNum > count($rowKeys)) {
668
            return Functions::VALUE();
669 2
        } elseif ($rowNum == 0) {
670
            return $arrayValues[$columnNum];
671
        }
672 2
        $rowNum = $rowKeys[--$rowNum];
673
674 2
        return $arrayValues[$rowNum][$columnNum];
675
    }
676
677
    /**
678
     * TRANSPOSE.
679
     *
680
     * @param array $matrixData A matrix of values
681
     *
682
     * @return array
683
     *
684
     * Unlike the Excel TRANSPOSE function, which will only work on a single row or column, this function will transpose a full matrix
685
     */
686 1
    public static function TRANSPOSE($matrixData)
687
    {
688 1
        $returnMatrix = [];
689 1
        if (!is_array($matrixData)) {
0 ignored issues
show
introduced by
The condition is_array($matrixData) is always true.
Loading history...
690
            $matrixData = [[$matrixData]];
691
        }
692
693 1
        $column = 0;
694 1
        foreach ($matrixData as $matrixRow) {
695 1
            $row = 0;
696 1
            foreach ($matrixRow as $matrixCell) {
697 1
                $returnMatrix[$row][$column] = $matrixCell;
698 1
                ++$row;
699
            }
700 1
            ++$column;
701
        }
702
703 1
        return $returnMatrix;
704
    }
705
706 3
    private static function vlookupSort($a, $b)
707
    {
708 3
        reset($a);
709 3
        $firstColumn = key($a);
710 3
        $aLower = StringHelper::strToLower($a[$firstColumn]);
711 3
        $bLower = StringHelper::strToLower($b[$firstColumn]);
712 3
        if ($aLower == $bLower) {
713 1
            return 0;
714
        }
715
716 3
        return ($aLower < $bLower) ? -1 : 1;
717
    }
718
719
    /**
720
     * VLOOKUP
721
     * 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.
722
     *
723
     * @param mixed $lookup_value The value that you want to match in lookup_array
724
     * @param mixed $lookup_array The range of cells being searched
725
     * @param mixed $index_number The column number in table_array from which the matching value must be returned. The first column is 1.
726
     * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
727
     *
728
     * @return mixed The value of the found cell
729
     */
730 15
    public static function VLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
731
    {
732 15
        $lookup_value = Functions::flattenSingleValue($lookup_value);
733 15
        $index_number = Functions::flattenSingleValue($index_number);
734 15
        $not_exact_match = Functions::flattenSingleValue($not_exact_match);
735
736
        // index_number must be greater than or equal to 1
737 15
        if ($index_number < 1) {
738
            return Functions::VALUE();
739
        }
740
741
        // index_number must be less than or equal to the number of columns in lookup_array
742 15
        if ((!is_array($lookup_array)) || (empty($lookup_array))) {
743
            return Functions::REF();
744
        }
745 15
        $f = array_keys($lookup_array);
746 15
        $firstRow = array_pop($f);
747 15
        if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array[$firstRow]))) {
748
            return Functions::REF();
749
        }
750 15
        $columnKeys = array_keys($lookup_array[$firstRow]);
751 15
        $returnColumn = $columnKeys[--$index_number];
752 15
        $firstColumn = array_shift($columnKeys);
753
754 15
        if (!$not_exact_match) {
755 3
            uasort($lookup_array, ['self', 'vlookupSort']);
756
        }
757
758 15
        $lookupLower = StringHelper::strToLower($lookup_value);
759 15
        $rowNumber = $rowValue = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $rowValue is dead and can be removed.
Loading history...
760 15
        foreach ($lookup_array as $rowKey => $rowData) {
761 15
            $firstLower = StringHelper::strToLower($rowData[$firstColumn]);
762
763
            // break if we have passed possible keys
764 15
            if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn]) && ($rowData[$firstColumn] > $lookup_value)) ||
765 15
                (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]) && ($firstLower > $lookupLower))) {
766 11
                break;
767
            }
768
            // remember the last key, but only if datatypes match
769 12
            if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn])) ||
770 12
                (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]))) {
771 11
                if ($not_exact_match) {
772 8
                    $rowNumber = $rowKey;
773
774 8
                    continue;
775 3
                } elseif (($firstLower == $lookupLower)
776
                    // Spreadsheets software returns first exact match,
777
                    // we have sorted and we might have broken key orders
778
                    // we want the first one (by its initial index)
779 3
                    && (($rowNumber == false) || ($rowKey < $rowNumber))
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
780
                ) {
781 1
                    $rowNumber = $rowKey;
782
                }
783
            }
784
        }
785
786 15
        if ($rowNumber !== false) {
787
            // return the appropriate value
788 9
            return $lookup_array[$rowNumber][$returnColumn];
789
        }
790
791 6
        return Functions::NA();
792
    }
793
794
    /**
795
     * HLOOKUP
796
     * 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.
797
     *
798
     * @param mixed $lookup_value The value that you want to match in lookup_array
799
     * @param mixed $lookup_array The range of cells being searched
800
     * @param mixed $index_number The row number in table_array from which the matching value must be returned. The first row is 1.
801
     * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
802
     *
803
     * @return mixed The value of the found cell
804
     */
805 11
    public static function HLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
806
    {
807 11
        $lookup_value = Functions::flattenSingleValue($lookup_value);
808 11
        $index_number = Functions::flattenSingleValue($index_number);
809 11
        $not_exact_match = Functions::flattenSingleValue($not_exact_match);
810
811
        // index_number must be greater than or equal to 1
812 11
        if ($index_number < 1) {
813
            return Functions::VALUE();
814
        }
815
816
        // index_number must be less than or equal to the number of columns in lookup_array
817 11
        if ((!is_array($lookup_array)) || (empty($lookup_array))) {
818
            return Functions::REF();
819
        }
820 11
        $f = array_keys($lookup_array);
821 11
        $firstRow = array_pop($f);
822 11
        if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) {
823
            return Functions::REF();
824
        }
825
826 11
        $firstkey = $f[0] - 1;
827 11
        $returnColumn = $firstkey + $index_number;
828 11
        $firstColumn = array_shift($f);
829 11
        $rowNumber = null;
830 11
        foreach ($lookup_array[$firstColumn] as $rowKey => $rowData) {
831
            // break if we have passed possible keys
832 11
            $bothNumeric = is_numeric($lookup_value) && is_numeric($rowData);
833 11
            $bothNotNumeric = !is_numeric($lookup_value) && !is_numeric($rowData);
834 11
            $lookupLower = StringHelper::strToLower($lookup_value);
835 11
            $rowDataLower = StringHelper::strToLower($rowData);
836
837 11
            if ($not_exact_match && (
838 5
                ($bothNumeric && $rowData > $lookup_value) ||
839 11
                ($bothNotNumeric && $rowDataLower > $lookupLower)
840
                )) {
841 3
                break;
842
            }
843
844
            // Remember the last key, but only if datatypes match (as in VLOOKUP)
845 11
            if ($bothNumeric || $bothNotNumeric) {
846 11
                if ($not_exact_match) {
847 5
                    $rowNumber = $rowKey;
848
849 5
                    continue;
850 6
                } elseif ($rowDataLower === $lookupLower
851 6
                    && ($rowNumber === null || $rowKey < $rowNumber)
852
                ) {
853 5
                    $rowNumber = $rowKey;
854
                }
855
            }
856
        }
857
858 11
        if ($rowNumber !== null) {
859
            //  otherwise return the appropriate value
860 10
            return $lookup_array[$returnColumn][$rowNumber];
861
        }
862
863 1
        return Functions::NA();
864
    }
865
866
    /**
867
     * LOOKUP
868
     * The LOOKUP function searches for value either from a one-row or one-column range or from an array.
869
     *
870
     * @param mixed $lookup_value The value that you want to match in lookup_array
871
     * @param mixed $lookup_vector The range of cells being searched
872
     * @param null|mixed $result_vector The column from which the matching value must be returned
873
     *
874
     * @return mixed The value of the found cell
875
     */
876 8
    public static function LOOKUP($lookup_value, $lookup_vector, $result_vector = null)
877
    {
878 8
        $lookup_value = Functions::flattenSingleValue($lookup_value);
879
880 8
        if (!is_array($lookup_vector)) {
881
            return Functions::NA();
882
        }
883 8
        $hasResultVector = isset($result_vector);
884 8
        $lookupRows = count($lookup_vector);
885 8
        $l = array_keys($lookup_vector);
886 8
        $l = array_shift($l);
887 8
        $lookupColumns = count($lookup_vector[$l]);
888
        // we correctly orient our results
889 8
        if (($lookupRows === 1 && $lookupColumns > 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) {
890 1
            $lookup_vector = self::TRANSPOSE($lookup_vector);
891 1
            $lookupRows = count($lookup_vector);
892 1
            $l = array_keys($lookup_vector);
893 1
            $lookupColumns = count($lookup_vector[array_shift($l)]);
894
        }
895
896 8
        if ($result_vector === null) {
897 2
            $result_vector = $lookup_vector;
898
        }
899 8
        $resultRows = count($result_vector);
900 8
        $l = array_keys($result_vector);
901 8
        $l = array_shift($l);
902 8
        $resultColumns = count($result_vector[$l]);
903
        // we correctly orient our results
904 8
        if ($resultRows === 1 && $resultColumns > 1) {
905
            $result_vector = self::TRANSPOSE($result_vector);
906
            $resultRows = count($result_vector);
0 ignored issues
show
Unused Code introduced by
The assignment to $resultRows is dead and can be removed.
Loading history...
907
            $r = array_keys($result_vector);
908
            $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...
909
        }
910
911 8
        if ($lookupRows === 2 && !$hasResultVector) {
912
            $result_vector = array_pop($lookup_vector);
913
            $lookup_vector = array_shift($lookup_vector);
914
        }
915
916 8
        if ($lookupColumns !== 2) {
917 6
            foreach ($lookup_vector as &$value) {
918 6
                if (is_array($value)) {
919 6
                    $k = array_keys($value);
920 6
                    $key1 = $key2 = array_shift($k);
921 6
                    ++$key2;
922 6
                    $dataValue1 = $value[$key1];
923
                } else {
924
                    $key1 = 0;
925
                    $key2 = 1;
926
                    $dataValue1 = $value;
927
                }
928 6
                $dataValue2 = array_shift($result_vector);
929 6
                if (is_array($dataValue2)) {
930 6
                    $dataValue2 = array_shift($dataValue2);
931
                }
932 6
                $value = [$key1 => $dataValue1, $key2 => $dataValue2];
933
            }
934 6
            unset($value);
935
        }
936
937 8
        return self::VLOOKUP($lookup_value, $lookup_vector, 2);
938
    }
939
940
    /**
941
     * FORMULATEXT.
942
     *
943
     * @param mixed $cellReference The cell to check
944
     * @param Cell $pCell The current cell (containing this formula)
945
     *
946
     * @return string
947
     */
948 9
    public static function FORMULATEXT($cellReference = '', Cell $pCell = null)
949
    {
950 9
        if ($pCell === null) {
951
            return Functions::REF();
952
        }
953
954 9
        preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches);
955
956 9
        $cellReference = $matches[6] . $matches[7];
957 9
        $worksheetName = trim($matches[3], "'");
958 9
        $worksheet = (!empty($worksheetName))
959 2
            ? $pCell->getWorksheet()->getParent()->getSheetByName($worksheetName)
960 9
            : $pCell->getWorksheet();
961
962 9
        if (!$worksheet->getCell($cellReference)->isFormula()) {
1 ignored issue
show
Bug introduced by
The method getCell() 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

962
        if (!$worksheet->/** @scrutinizer ignore-call */ getCell($cellReference)->isFormula()) {

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...
963 2
            return Functions::NA();
964
        }
965
966 7
        return $worksheet->getCell($cellReference)->getValue();
967
    }
968
}
969