Passed
Push — develop ( 3028c6...9b44cf )
by Mark
29:23
created

LookupRef::TRANSPOSE()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 1
dl 0
loc 18
ccs 0
cts 12
cp 0
crap 20
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
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) {
0 ignored issues
show
introduced by
The condition is_bool($referenceStyle) is always true.
Loading history...
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<mixed,integer[]> 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
     *
208
     * @return int The number of rows in cellAddress
209
     */
210 8
    public static function ROWS($cellAddress = null)
211
    {
212 8
        if ($cellAddress === null || $cellAddress === '') {
213 2
            return 1;
214 6
        } elseif (!is_array($cellAddress)) {
215 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...
216
        }
217
218 5
        reset($cellAddress);
219 5
        $isMatrix = (is_numeric(key($cellAddress)));
220 5
        list($columns, $rows) = Calculation::getMatrixDimensions($cellAddress);
221
222 5
        if ($isMatrix) {
223 3
            return $columns;
224
        }
225
226 2
        return $rows;
227
    }
228
229
    /**
230
     * HYPERLINK.
231
     *
232
     * Excel Function:
233
     *        =HYPERLINK(linkURL,displayName)
234
     *
235
     * @category Logical Functions
236
     *
237
     * @param string $linkURL Value to check, is also the value returned when no error
238
     * @param string $displayName Value to return when testValue is an error condition
239
     * @param Cell $pCell The cell to set the hyperlink in
240
     *
241
     * @return mixed The value of $displayName (or $linkURL if $displayName was blank)
242
     */
243 1
    public static function HYPERLINK($linkURL = '', $displayName = null, Cell $pCell = null)
244
    {
245 1
        $linkURL = ($linkURL === null) ? '' : Functions::flattenSingleValue($linkURL);
0 ignored issues
show
introduced by
The condition $linkURL === null is always false.
Loading history...
246 1
        $displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
247
248 1
        if ((!is_object($pCell)) || (trim($linkURL) == '')) {
249
            return Functions::REF();
250
        }
251
252 1
        if ((is_object($displayName)) || trim($displayName) == '') {
253
            $displayName = $linkURL;
254
        }
255
256 1
        $pCell->getHyperlink()->setUrl($linkURL);
257 1
        $pCell->getHyperlink()->setTooltip($displayName);
258
259 1
        return $displayName;
260
    }
261
262
    /**
263
     * INDIRECT.
264
     *
265
     * Returns the reference specified by a text string.
266
     * References are immediately evaluated to display their contents.
267
     *
268
     * Excel Function:
269
     *        =INDIRECT(cellAddress)
270
     *
271
     * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010
272
     *
273
     * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
274
     * @param Cell $pCell The current cell (containing this formula)
275
     *
276
     * @return mixed The cells referenced by cellAddress
277
     *
278
     * @todo    Support for the optional a1 parameter introduced in Excel 2010
279
     */
280
    public static function INDIRECT($cellAddress = null, Cell $pCell = null)
281
    {
282
        $cellAddress = Functions::flattenSingleValue($cellAddress);
283
        if ($cellAddress === null || $cellAddress === '') {
284
            return Functions::REF();
285
        }
286
287
        $cellAddress1 = $cellAddress;
288
        $cellAddress2 = null;
289
        if (strpos($cellAddress, ':') !== false) {
290
            list($cellAddress1, $cellAddress2) = explode(':', $cellAddress);
291
        }
292
293
        if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) ||
294
            (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches)))) {
295
            if (!preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $cellAddress1, $matches)) {
296
                return Functions::REF();
297
            }
298
299
            if (strpos($cellAddress, '!') !== false) {
300
                list($sheetName, $cellAddress) = explode('!', $cellAddress);
301
                $sheetName = trim($sheetName, "'");
302
                $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

302
                $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...
303
            } else {
304
                $pSheet = $pCell->getWorksheet();
305
            }
306
307
            return Calculation::getInstance()->extractNamedRange($cellAddress, $pSheet, false);
308
        }
309
310
        if (strpos($cellAddress, '!') !== false) {
311
            list($sheetName, $cellAddress) = explode('!', $cellAddress);
312
            $sheetName = trim($sheetName, "'");
313
            $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
314
        } else {
315
            $pSheet = $pCell->getWorksheet();
316
        }
317
318
        return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false);
319
    }
320
321
    /**
322
     * OFFSET.
323
     *
324
     * Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells.
325
     * The reference that is returned can be a single cell or a range of cells. You can specify the number of rows and
326
     * the number of columns to be returned.
327
     *
328
     * Excel Function:
329
     *        =OFFSET(cellAddress, rows, cols, [height], [width])
330
     *
331
     * @param null|array|string $cellAddress The reference from which you want to base the offset. Reference must refer to a cell or
332
     *                                range of adjacent cells; otherwise, OFFSET returns the #VALUE! error value.
333
     * @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to.
334
     *                                Using 5 as the rows argument specifies that the upper-left cell in the reference is
335
     *                                five rows below reference. Rows can be positive (which means below the starting reference)
336
     *                                or negative (which means above the starting reference).
337
     * @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell of the result
338
     *                                to refer to. Using 5 as the cols argument specifies that the upper-left cell in the
339
     *                                reference is five columns to the right of reference. Cols can be positive (which means
340
     *                                to the right of the starting reference) or negative (which means to the left of the
341
     *                                starting reference).
342
     * @param mixed $height The height, in number of rows, that you want the returned reference to be. Height must be a positive number.
343
     * @param mixed $width The width, in number of columns, that you want the returned reference to be. Width must be a positive number.
344
     * @param null|Cell $pCell
345
     *
346
     * @return string A reference to a cell or range of cells
347
     */
348
    public static function OFFSET($cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, Cell $pCell = null)
349
    {
350
        $rows = Functions::flattenSingleValue($rows);
351
        $columns = Functions::flattenSingleValue($columns);
352
        $height = Functions::flattenSingleValue($height);
353
        $width = Functions::flattenSingleValue($width);
354
        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...
355
            return 0;
356
        }
357
358
        if (!is_object($pCell)) {
359
            return Functions::REF();
360
        }
361
362
        $sheetName = null;
363
        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

363
        if (strpos(/** @scrutinizer ignore-type */ $cellAddress, '!')) {
Loading history...
364
            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

364
            list($sheetName, $cellAddress) = explode('!', /** @scrutinizer ignore-type */ $cellAddress);
Loading history...
365
            $sheetName = trim($sheetName, "'");
366
        }
367
        if (strpos($cellAddress, ':')) {
368
            list($startCell, $endCell) = explode(':', $cellAddress);
369
        } else {
370
            $startCell = $endCell = $cellAddress;
371
        }
372
        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

372
        list($startCellColumn, $startCellRow) = Coordinate::coordinateFromString(/** @scrutinizer ignore-type */ $startCell);
Loading history...
373
        list($endCellColumn, $endCellRow) = Coordinate::coordinateFromString($endCell);
374
375
        $startCellRow += $rows;
376
        $startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1;
377
        $startCellColumn += $columns;
378
379
        if (($startCellRow <= 0) || ($startCellColumn < 0)) {
380
            return Functions::REF();
381
        }
382
        $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1;
383
        if (($width != null) && (!is_object($width))) {
384
            $endCellColumn = $startCellColumn + $width - 1;
385
        } else {
386
            $endCellColumn += $columns;
387
        }
388
        $startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
389
390
        if (($height != null) && (!is_object($height))) {
391
            $endCellRow = $startCellRow + $height - 1;
392
        } else {
393
            $endCellRow += $rows;
394
        }
395
396
        if (($endCellRow <= 0) || ($endCellColumn < 0)) {
397
            return Functions::REF();
398
        }
399
        $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1);
400
401
        $cellAddress = $startCellColumn . $startCellRow;
402
        if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) {
403
            $cellAddress .= ':' . $endCellColumn . $endCellRow;
404
        }
405
406
        if ($sheetName !== null) {
407
            $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName);
408
        } else {
409
            $pSheet = $pCell->getWorksheet();
410
        }
411
412
        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...
413
    }
414
415
    /**
416
     * CHOOSE.
417
     *
418
     * Uses lookup_value to return a value from the list of value arguments.
419
     * Use CHOOSE to select one of up to 254 values based on the lookup_value.
420
     *
421
     * Excel Function:
422
     *        =CHOOSE(index_num, value1, [value2], ...)
423
     *
424
     * @param mixed $index_num Specifies which value argument is selected.
425
     *                            Index_num must be a number between 1 and 254, or a formula or reference to a cell containing a number
426
     *                                between 1 and 254.
427
     * @param mixed $value1... Value1 is required, subsequent values are optional.
428
     *                            Between 1 to 254 value arguments from which CHOOSE selects a value or an action to perform based on
429
     *                                index_num. The arguments can be numbers, cell references, defined names, formulas, functions, or
430
     *                                text.
431
     *
432
     * @return mixed The selected value
433
     */
434
    public static function CHOOSE(...$chooseArgs)
435
    {
436
        $chosenEntry = Functions::flattenArray(array_shift($chooseArgs));
437
        $entryCount = count($chooseArgs) - 1;
438
439
        if (is_array($chosenEntry)) {
0 ignored issues
show
introduced by
The condition is_array($chosenEntry) is always true.
Loading history...
440
            $chosenEntry = array_shift($chosenEntry);
441
        }
442
        if ((is_numeric($chosenEntry)) && (!is_bool($chosenEntry))) {
443
            --$chosenEntry;
444
        } else {
445
            return Functions::VALUE();
446
        }
447
        $chosenEntry = floor($chosenEntry);
448
        if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
449
            return Functions::VALUE();
450
        }
451
452
        if (is_array($chooseArgs[$chosenEntry])) {
453
            return Functions::flattenArray($chooseArgs[$chosenEntry]);
454
        }
455
456
        return $chooseArgs[$chosenEntry];
457
    }
458
459
    /**
460
     * MATCH.
461
     *
462
     * The MATCH function searches for a specified item in a range of cells
463
     *
464
     * Excel Function:
465
     *        =MATCH(lookup_value, lookup_array, [match_type])
466
     *
467
     * @param mixed $lookupValue The value that you want to match in lookup_array
468
     * @param mixed $lookupArray The range of cells being searched
469
     * @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.
470
     *
471
     * @return int The relative position of the found item
472
     */
473 16
    public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
474
    {
475 16
        $lookupArray = Functions::flattenArray($lookupArray);
476 16
        $lookupValue = Functions::flattenSingleValue($lookupValue);
477 16
        $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
478
479
        // MATCH is not case sensitive
480 16
        $lookupValue = strtolower($lookupValue);
481
482
        // Lookup_value type has to be number, text, or logical values
483 16
        if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
0 ignored issues
show
introduced by
The condition is_string($lookupValue) is always true.
Loading history...
484
            return Functions::NA();
485
        }
486
487
        // 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...
488 16
        if (($matchType !== 0) && ($matchType !== -1) && ($matchType !== 1)) {
489
            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...
490
        }
491
492
        // Lookup_array should not be empty
493 16
        $lookupArraySize = count($lookupArray);
494 16
        if ($lookupArraySize <= 0) {
495
            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...
496
        }
497
498
        // Lookup_array should contain only number, text, or logical values, or empty (null) cells
499 16
        foreach ($lookupArray as $i => $lookupArrayValue) {
500
            //    check the type of the value
501 16
            if ((!is_numeric($lookupArrayValue)) && (!is_string($lookupArrayValue)) &&
502 16
                (!is_bool($lookupArrayValue)) && ($lookupArrayValue !== null)
503
            ) {
504
                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...
505
            }
506
            // Convert strings to lowercase for case-insensitive testing
507 16
            if (is_string($lookupArrayValue)) {
508
                $lookupArray[$i] = strtolower($lookupArrayValue);
509
            }
510 16
            if (($lookupArrayValue === null) && (($matchType == 1) || ($matchType == -1))) {
511 16
                $lookupArray = array_slice($lookupArray, 0, $i - 1);
512
            }
513
        }
514
515 16
        if ($matchType == 1) {
516
            // If match_type is 1 the list has to be processed from last to first
517
518 4
            $lookupArray = array_reverse($lookupArray);
519 4
            $keySet = array_reverse(array_keys($lookupArray));
520
        }
521
522
        // **
523
        // find the match
524
        // **
525
526 16
        if ($matchType == 0 || $matchType == 1) {
527 9
            foreach ($lookupArray as $i => $lookupArrayValue) {
528 9
                if (($matchType == 0) && ($lookupArrayValue == $lookupValue)) {
529
                    //    exact match
530 4
                    return ++$i;
531 7
                } elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) {
532 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...
533
534
                    // The current value is the (first) match
535 7
                    return $i + 1;
536
                }
537
            }
538
        } else {
539
            // matchType = -1
540
541
            // "Special" case: since the array it's supposed to be ordered in descending order, the
542
            // Excel algorithm gives up immediately if the first element is smaller than the searched value
543 7
            if ($lookupArray[0] < $lookupValue) {
544 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...
545
            }
546
547 6
            $maxValueKey = null;
548
549
            // The basic algorithm is:
550
            // Iterate and keep the highest match until the next element is smaller than the searched value.
551
            // Return immediately if perfect match is found
552 6
            foreach ($lookupArray as $i => $lookupArrayValue) {
553 6
                if ($lookupArrayValue == $lookupValue) {
554
                    // Another "special" case. If a perfect match is found,
555
                    // the algorithm gives up immediately
556 2
                    return $i + 1;
557 5
                } elseif ($lookupArrayValue >= $lookupValue) {
558 5
                    $maxValueKey = $i + 1;
559
                }
560
            }
561
562 4
            if ($maxValueKey !== null) {
563 4
                return $maxValueKey;
564
            }
565
        }
566
567
        // Unsuccessful in finding a match, return #N/A error value
568 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...
569
    }
570
571
    /**
572
     * INDEX.
573
     *
574
     * Uses an index to choose a value from a reference or array
575
     *
576
     * Excel Function:
577
     *        =INDEX(range_array, row_num, [column_num])
578
     *
579
     * @param mixed $arrayValues A range of cells or an array constant
580
     * @param mixed $rowNum The row in array from which to return a value. If row_num is omitted, column_num is required.
581
     * @param mixed $columnNum The column in array from which to return a value. If column_num is omitted, row_num is required.
582
     *
583
     * @return mixed the value of a specified cell or array of cells
584
     */
585 9
    public static function INDEX($arrayValues, $rowNum = 0, $columnNum = 0)
586
    {
587 9
        $rowNum = Functions::flattenSingleValue($rowNum);
588 9
        $columnNum = Functions::flattenSingleValue($columnNum);
589
590 9
        if (($rowNum < 0) || ($columnNum < 0)) {
591 2
            return Functions::VALUE();
592
        }
593
594 7
        if (!is_array($arrayValues) || ($rowNum > count($arrayValues))) {
595 1
            return Functions::REF();
596
        }
597
598 6
        $rowKeys = array_keys($arrayValues);
599 6
        $columnKeys = @array_keys($arrayValues[$rowKeys[0]]);
600
601 6
        if ($columnNum > count($columnKeys)) {
602 1
            return Functions::VALUE();
603 5
        } elseif ($columnNum == 0) {
604 3
            if ($rowNum == 0) {
605 1
                return $arrayValues;
606
            }
607 2
            $rowNum = $rowKeys[--$rowNum];
608 2
            $returnArray = [];
609 2
            foreach ($arrayValues as $arrayColumn) {
610 2
                if (is_array($arrayColumn)) {
611 2
                    if (isset($arrayColumn[$rowNum])) {
612
                        $returnArray[] = $arrayColumn[$rowNum];
613
                    } else {
614 2
                        return [$rowNum => $arrayValues[$rowNum]];
615
                    }
616
                } else {
617
                    return $arrayValues[$rowNum];
618
                }
619
            }
620
621
            return $returnArray;
622
        }
623 2
        $columnNum = $columnKeys[--$columnNum];
624 2
        if ($rowNum > count($rowKeys)) {
625
            return Functions::VALUE();
626 2
        } elseif ($rowNum == 0) {
627
            return $arrayValues[$columnNum];
628
        }
629 2
        $rowNum = $rowKeys[--$rowNum];
630
631 2
        return $arrayValues[$rowNum][$columnNum];
632
    }
633
634
    /**
635
     * TRANSPOSE.
636
     *
637
     * @param array $matrixData A matrix of values
638
     *
639
     * @return array
640
     *
641
     * Unlike the Excel TRANSPOSE function, which will only work on a single row or column, this function will transpose a full matrix
642
     */
643
    public static function TRANSPOSE($matrixData)
644
    {
645
        $returnMatrix = [];
646
        if (!is_array($matrixData)) {
0 ignored issues
show
introduced by
The condition is_array($matrixData) is always true.
Loading history...
647
            $matrixData = [[$matrixData]];
648
        }
649
650
        $column = 0;
651
        foreach ($matrixData as $matrixRow) {
652
            $row = 0;
653
            foreach ($matrixRow as $matrixCell) {
654
                $returnMatrix[$row][$column] = $matrixCell;
655
                ++$row;
656
            }
657
            ++$column;
658
        }
659
660
        return $returnMatrix;
661
    }
662
663 2
    private static function vlookupSort($a, $b)
664
    {
665 2
        reset($a);
666 2
        $firstColumn = key($a);
667 2
        if (($aLower = strtolower($a[$firstColumn])) == ($bLower = strtolower($b[$firstColumn]))) {
668
            return 0;
669
        }
670
671 2
        return ($aLower < $bLower) ? -1 : 1;
672
    }
673
674
    /**
675
     * VLOOKUP
676
     * 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.
677
     *
678
     * @param mixed $lookup_value The value that you want to match in lookup_array
679
     * @param mixed $lookup_array The range of cells being searched
680
     * @param mixed $index_number The column number in table_array from which the matching value must be returned. The first column is 1.
681
     * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
682
     *
683
     * @return mixed The value of the found cell
684
     */
685 5
    public static function VLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
686
    {
687 5
        $lookup_value = Functions::flattenSingleValue($lookup_value);
688 5
        $index_number = Functions::flattenSingleValue($index_number);
689 5
        $not_exact_match = Functions::flattenSingleValue($not_exact_match);
690
691
        // index_number must be greater than or equal to 1
692 5
        if ($index_number < 1) {
693
            return Functions::VALUE();
694
        }
695
696
        // index_number must be less than or equal to the number of columns in lookup_array
697 5
        if ((!is_array($lookup_array)) || (empty($lookup_array))) {
698
            return Functions::REF();
699
        }
700 5
        $f = array_keys($lookup_array);
701 5
        $firstRow = array_pop($f);
702 5
        if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array[$firstRow]))) {
703
            return Functions::REF();
704
        }
705 5
        $columnKeys = array_keys($lookup_array[$firstRow]);
706 5
        $returnColumn = $columnKeys[--$index_number];
707 5
        $firstColumn = array_shift($columnKeys);
708
709 5
        if (!$not_exact_match) {
710 2
            uasort($lookup_array, ['self', 'vlookupSort']);
711
        }
712
713 5
        $rowNumber = $rowValue = false;
714 5
        foreach ($lookup_array as $rowKey => $rowData) {
715 5
            if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn]) && ($rowData[$firstColumn] > $lookup_value)) ||
716 5
                (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]) && (strtolower($rowData[$firstColumn]) > strtolower($lookup_value)))) {
717 4
                break;
718
            }
719
            // remember the last key, but only if datatypes match
720 5
            if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn])) ||
721 5
                (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]))) {
722 4
                $rowNumber = $rowKey;
723 5
                $rowValue = $rowData[$firstColumn];
724
            }
725
        }
726
727 5
        if ($rowNumber !== false) {
0 ignored issues
show
introduced by
The condition $rowNumber !== false is always false.
Loading history...
728 4
            if ((!$not_exact_match) && ($rowValue != $lookup_value)) {
729
                //    if an exact match is required, we have what we need to return an appropriate response
730 2
                return Functions::NA();
731
            }
732
            //    otherwise return the appropriate value
733 2
            return $lookup_array[$rowNumber][$returnColumn];
734
        }
735
736 1
        return Functions::NA();
737
    }
738
739
    /**
740
     * HLOOKUP
741
     * 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.
742
     *
743
     * @param mixed $lookup_value The value that you want to match in lookup_array
744
     * @param mixed $lookup_array The range of cells being searched
745
     * @param mixed $index_number The row number in table_array from which the matching value must be returned. The first row is 1.
746
     * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value
747
     *
748
     * @return mixed The value of the found cell
749
     */
750 9
    public static function HLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true)
751
    {
752 9
        $lookup_value = Functions::flattenSingleValue($lookup_value);
753 9
        $index_number = Functions::flattenSingleValue($index_number);
754 9
        $not_exact_match = Functions::flattenSingleValue($not_exact_match);
755
756
        // index_number must be greater than or equal to 1
757 9
        if ($index_number < 1) {
758
            return Functions::VALUE();
759
        }
760
761
        // index_number must be less than or equal to the number of columns in lookup_array
762 9
        if ((!is_array($lookup_array)) || (empty($lookup_array))) {
763
            return Functions::REF();
764
        }
765 9
        $f = array_keys($lookup_array);
766 9
        $firstRow = array_pop($f);
767 9
        if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) {
768
            return Functions::REF();
769
        }
770 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...
771 9
        $firstkey = $f[0] - 1;
772 9
        $returnColumn = $firstkey + $index_number;
773 9
        $firstColumn = array_shift($f);
774
775 9
        if (!$not_exact_match) {
776 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...
777
        }
778 9
        $rowNumber = $rowValue = false;
779 9
        foreach ($lookup_array[$firstColumn] as $rowKey => $rowData) {
780 9
            if ((is_numeric($lookup_value) && is_numeric($rowData) && ($rowData > $lookup_value)) ||
781 9
                (!is_numeric($lookup_value) && !is_numeric($rowData) && (strtolower($rowData) > strtolower($lookup_value)))) {
782 7
                break;
783
            }
784 9
            $rowNumber = $rowKey;
785 9
            $rowValue = $rowData;
786
        }
787
788 9
        if ($rowNumber !== false) {
789 9
            if ((!$not_exact_match) && ($rowValue != $lookup_value)) {
790
                //  if an exact match is required, we have what we need to return an appropriate response
791 1
                return Functions::NA();
792
            }
793
            //  otherwise return the appropriate value
794 8
            return $lookup_array[$returnColumn][$rowNumber];
795
        }
796
797
        return Functions::NA();
798
    }
799
800
    /**
801
     * LOOKUP
802
     * The LOOKUP function searches for value either from a one-row or one-column range or from an array.
803
     *
804
     * @param mixed $lookup_value The value that you want to match in lookup_array
805
     * @param mixed $lookup_vector The range of cells being searched
806
     * @param null|mixed $result_vector The column from which the matching value must be returned
807
     *
808
     * @return mixed The value of the found cell
809
     */
810
    public static function LOOKUP($lookup_value, $lookup_vector, $result_vector = null)
811
    {
812
        $lookup_value = Functions::flattenSingleValue($lookup_value);
813
814
        if (!is_array($lookup_vector)) {
815
            return Functions::NA();
816
        }
817
        $lookupRows = count($lookup_vector);
818
        $l = array_keys($lookup_vector);
819
        $l = array_shift($l);
820
        $lookupColumns = count($lookup_vector[$l]);
821
        if ((($lookupRows == 1) && ($lookupColumns > 1)) || (($lookupRows == 2) && ($lookupColumns != 2))) {
822
            $lookup_vector = self::TRANSPOSE($lookup_vector);
823
            $lookupRows = count($lookup_vector);
824
            $l = array_keys($lookup_vector);
825
            $lookupColumns = count($lookup_vector[array_shift($l)]);
826
        }
827
828
        if ($result_vector === null) {
829
            $result_vector = $lookup_vector;
830
        }
831
        $resultRows = count($result_vector);
832
        $l = array_keys($result_vector);
833
        $l = array_shift($l);
834
        $resultColumns = count($result_vector[$l]);
835
        if ((($resultRows == 1) && ($resultColumns > 1)) || (($resultRows == 2) && ($resultColumns != 2))) {
836
            $result_vector = self::TRANSPOSE($result_vector);
837
            $resultRows = count($result_vector);
0 ignored issues
show
Unused Code introduced by
The assignment to $resultRows is dead and can be removed.
Loading history...
838
            $r = array_keys($result_vector);
839
            $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...
840
        }
841
842
        if ($lookupRows == 2) {
843
            $result_vector = array_pop($lookup_vector);
844
            $lookup_vector = array_shift($lookup_vector);
845
        }
846
        if ($lookupColumns != 2) {
847
            foreach ($lookup_vector as &$value) {
848
                if (is_array($value)) {
849
                    $k = array_keys($value);
850
                    $key1 = $key2 = array_shift($k);
851
                    ++$key2;
852
                    $dataValue1 = $value[$key1];
853
                } else {
854
                    $key1 = 0;
855
                    $key2 = 1;
856
                    $dataValue1 = $value;
857
                }
858
                $dataValue2 = array_shift($result_vector);
859
                if (is_array($dataValue2)) {
860
                    $dataValue2 = array_shift($dataValue2);
861
                }
862
                $value = [$key1 => $dataValue1, $key2 => $dataValue2];
863
            }
864
            unset($value);
865
        }
866
867
        return self::VLOOKUP($lookup_value, $lookup_vector, 2);
868
    }
869
870
    /**
871
     * FORMULATEXT.
872
     *
873
     * @param mixed $cellReference The cell to check
874
     * @param Cell $pCell The current cell (containing this formula)
875
     *
876
     * @return string
877
     */
878 6
    public static function FORMULATEXT($cellReference = '', Cell $pCell = null)
879
    {
880 6
        if ($pCell === null) {
881
            return Functions::REF();
882
        }
883
884 6
        preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches);
885
886 6
        $cellReference = $matches[6] . $matches[7];
887 6
        $worksheetName = trim($matches[3], "'");
888 6
        $worksheet = (!empty($worksheetName))
889 2
            ? $pCell->getWorksheet()->getParent()->getSheetByName($worksheetName)
890 6
            : $pCell->getWorksheet();
891
892 6
        if (!$worksheet->getCell($cellReference)->isFormula()) {
893 2
            return Functions::NA();
894
        }
895
896 4
        return $worksheet->getCell($cellReference)->getValue();
897
    }
898
}
899