Completed
Push — develop ( 888c61...656149 )
by Adrien
35:11
created

Cell::getHyperlink()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Cell;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Collection\Cells;
7
use PhpOffice\PhpSpreadsheet\Exception;
8
use PhpOffice\PhpSpreadsheet\RichText\RichText;
9
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
10
use PhpOffice\PhpSpreadsheet\Style\Style;
11
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
12
13
class Cell
14
{
15
    /**
16
     * Default range variable constant.
17
     *
18
     * @var string
19
     */
20
    const DEFAULT_RANGE = 'A1:A1';
21
22
    /**
23
     * Value binder to use.
24
     *
25
     * @var IValueBinder
26
     */
27
    private static $valueBinder;
28
29
    /**
30
     * Value of the cell.
31
     *
32
     * @var mixed
33
     */
34
    private $value;
35
36
    /**
37
     *    Calculated value of the cell (used for caching)
38
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
39
     *        create the original spreadsheet file.
40
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
41
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
42
     *        values used by the formula have changed since it was last calculated.
43
     *
44
     * @var mixed
45
     */
46
    private $calculatedValue;
47
48
    /**
49
     * Type of the cell data.
50
     *
51
     * @var string
52
     */
53
    private $dataType;
54
55
    /**
56
     * Collection of cells.
57
     *
58
     * @var Cells
59
     */
60
    private $parent;
61
62
    /**
63
     * Index to cellXf.
64
     *
65
     * @var int
66
     */
67
    private $xfIndex = 0;
68
69
    /**
70
     * Attributes of the formula.
71
     */
72
    private $formulaAttributes;
73
74
    /**
75
     * Update the cell into the cell collection.
76
     *
77
     * @return self
78
     */
79 121
    public function updateInCollection()
80
    {
81 121
        $this->parent->update($this);
82
83 121
        return $this;
84
    }
85
86 111
    public function detach()
87
    {
88 111
        $this->parent = null;
89 111
    }
90
91 103
    public function attach(Cells $parent)
92
    {
93 103
        $this->parent = $parent;
94 103
    }
95
96
    /**
97
     * Create a new Cell.
98
     *
99
     * @param mixed $pValue
100
     * @param string $pDataType
101
     * @param Worksheet $pSheet
102
     *
103
     * @throws Exception
104
     */
105 125
    public function __construct($pValue, $pDataType, Worksheet $pSheet)
106
    {
107
        // Initialise cell value
108 125
        $this->value = $pValue;
109
110
        // Set worksheet cache
111 125
        $this->parent = $pSheet->getCellCollection();
112
113
        // Set datatype?
114 125
        if ($pDataType !== null) {
115 125
            if ($pDataType == DataType::TYPE_STRING2) {
116
                $pDataType = DataType::TYPE_STRING;
117
            }
118 125
            $this->dataType = $pDataType;
119
        } elseif (!self::getValueBinder()->bindValue($this, $pValue)) {
120
            throw new Exception('Value could not be bound to cell.');
121
        }
122 125
    }
123
124
    /**
125
     * Get cell coordinate column.
126
     *
127
     * @return string
128
     */
129 48
    public function getColumn()
130
    {
131 48
        return $this->parent->getCurrentColumn();
132
    }
133
134
    /**
135
     * Get cell coordinate row.
136
     *
137
     * @return int
138
     */
139 46
    public function getRow()
140
    {
141 46
        return $this->parent->getCurrentRow();
142
    }
143
144
    /**
145
     * Get cell coordinate.
146
     *
147
     * @return string
148
     */
149 125
    public function getCoordinate()
150
    {
151 125
        return $this->parent->getCurrentCoordinate();
152
    }
153
154
    /**
155
     * Get cell value.
156
     *
157
     * @return mixed
158
     */
159 109
    public function getValue()
160
    {
161 109
        return $this->value;
162
    }
163
164
    /**
165
     * Get cell value with formatting.
166
     *
167
     * @return string
168
     */
169 5
    public function getFormattedValue()
170
    {
171 5
        return (string) NumberFormat::toFormattedString(
172 5
            $this->getCalculatedValue(),
173 5
            $this->getStyle()
174 5
                ->getNumberFormat()->getFormatCode()
175
        );
176
    }
177
178
    /**
179
     * Set cell value.
180
     *
181
     *    Sets the value for a cell, automatically determining the datatype using the value binder
182
     *
183
     * @param mixed $pValue Value
184
     *
185
     * @throws Exception
186
     *
187
     * @return Cell
188
     */
189 79
    public function setValue($pValue)
190
    {
191 79
        if (!self::getValueBinder()->bindValue($this, $pValue)) {
192
            throw new Exception('Value could not be bound to cell.');
193
        }
194
195 79
        return $this;
196
    }
197
198
    /**
199
     * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
200
     *
201
     * @param mixed $pValue Value
202
     * @param string $pDataType Explicit data type, see DataType::TYPE_*
203
     *
204
     * @throws Exception
205
     *
206
     * @return Cell
207
     */
208 117
    public function setValueExplicit($pValue, $pDataType)
209
    {
210
        // set the value according to data type
211
        switch ($pDataType) {
212 117
            case DataType::TYPE_NULL:
213 29
                $this->value = $pValue;
214
215 29
                break;
216 117
            case DataType::TYPE_STRING2:
217
                $pDataType = DataType::TYPE_STRING;
218
                // no break
219 117
            case DataType::TYPE_STRING:
220
                // Synonym for string
221 100
            case DataType::TYPE_INLINE:
222
                // Rich text
223 101
                $this->value = DataType::checkString($pValue);
224
225 101
                break;
226 99
            case DataType::TYPE_NUMERIC:
227 93
                $this->value = (float) $pValue;
228
229 93
                break;
230 59
            case DataType::TYPE_FORMULA:
231 57
                $this->value = (string) $pValue;
232
233 57
                break;
234 25
            case DataType::TYPE_BOOL:
235 25
                $this->value = (bool) $pValue;
236
237 25
                break;
238
            case DataType::TYPE_ERROR:
239
                $this->value = DataType::checkErrorCode($pValue);
240
241
                break;
242
            default:
243
                throw new Exception('Invalid datatype: ' . $pDataType);
244
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
245
        }
246
247
        // set the datatype
248 117
        $this->dataType = $pDataType;
249
250 117
        return $this->updateInCollection();
251
    }
252
253
    /**
254
     * Get calculated cell value.
255
     *
256
     * @param bool $resetLog Whether the calculation engine logger should be reset or not
257
     *
258
     * @throws Exception
259
     *
260
     * @return mixed
261
     */
262 71
    public function getCalculatedValue($resetLog = true)
263
    {
264 71
        if ($this->dataType == DataType::TYPE_FORMULA) {
265
            try {
266 48
                $result = Calculation::getInstance(
267 48
                    $this->getWorksheet()->getParent()
268 48
                )->calculateCellValue($this, $resetLog);
269
                //    We don't yet handle array returns
270 48
                if (is_array($result)) {
271 48
                    while (is_array($result)) {
272 10
                        $result = array_pop($result);
273
                    }
274
                }
275
            } catch (Exception $ex) {
276
                if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
277
                    return $this->calculatedValue; // Fallback for calculations referencing external files.
278
                }
279
280
                throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception(
281
                    $this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage()
282
                );
283
            }
284
285 48
            if ($result === '#Not Yet Implemented') {
286
                return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
287
            }
288
289 48
            return $result;
290 71
        } elseif ($this->value instanceof RichText) {
291 2
            return $this->value->getPlainText();
292
        }
293
294 71
        return $this->value;
295
    }
296
297
    /**
298
     * Set old calculated value (cached).
299
     *
300
     * @param mixed $pValue Value
301
     *
302
     * @return Cell
303
     */
304 22
    public function setCalculatedValue($pValue)
305
    {
306 22
        if ($pValue !== null) {
307 22
            $this->calculatedValue = (is_numeric($pValue)) ? (float) $pValue : $pValue;
308
        }
309
310 22
        return $this->updateInCollection();
311
    }
312
313
    /**
314
     *    Get old calculated value (cached)
315
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
316
     *        create the original spreadsheet file.
317
     *    Note that this value is not guaranteed to refelect the actual calculated value because it is
318
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
319
     *        values used by the formula have changed since it was last calculated.
320
     *
321
     * @return mixed
322
     */
323
    public function getOldCalculatedValue()
324
    {
325
        return $this->calculatedValue;
326
    }
327
328
    /**
329
     * Get cell data type.
330
     *
331
     * @return string
332
     */
333 70
    public function getDataType()
334
    {
335 70
        return $this->dataType;
336
    }
337
338
    /**
339
     * Set cell data type.
340
     *
341
     * @param string $pDataType see DataType::TYPE_*
342
     *
343
     * @return Cell
344
     */
345
    public function setDataType($pDataType)
346
    {
347
        if ($pDataType == DataType::TYPE_STRING2) {
348
            $pDataType = DataType::TYPE_STRING;
349
        }
350
        $this->dataType = $pDataType;
351
352
        return $this->updateInCollection();
353
    }
354
355
    /**
356
     * Identify if the cell contains a formula.
357
     *
358
     * @return bool
359
     */
360
    public function isFormula()
361
    {
362
        return $this->dataType == DataType::TYPE_FORMULA;
363
    }
364
365
    /**
366
     *    Does this cell contain Data validation rules?
367
     *
368
     * @throws Exception
369
     *
370
     * @return bool
371
     */
372 3
    public function hasDataValidation()
373
    {
374 3
        if (!isset($this->parent)) {
375
            throw new Exception('Cannot check for data validation when cell is not bound to a worksheet');
376
        }
377
378 3
        return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
379
    }
380
381
    /**
382
     * Get Data validation rules.
383
     *
384
     * @throws Exception
385
     *
386
     * @return DataValidation
387
     */
388 4
    public function getDataValidation()
389
    {
390 4
        if (!isset($this->parent)) {
391
            throw new Exception('Cannot get data validation for cell that is not bound to a worksheet');
392
        }
393
394 4
        return $this->getWorksheet()->getDataValidation($this->getCoordinate());
395
    }
396
397
    /**
398
     * Set Data validation rules.
399
     *
400
     * @param DataValidation $pDataValidation
401
     *
402
     * @throws Exception
403
     *
404
     * @return Cell
405
     */
406 View Code Duplication
    public function setDataValidation(DataValidation $pDataValidation = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
407
    {
408
        if (!isset($this->parent)) {
409
            throw new Exception('Cannot set data validation for cell that is not bound to a worksheet');
410
        }
411
412
        $this->getWorksheet()->setDataValidation($this->getCoordinate(), $pDataValidation);
413
414
        return $this->updateInCollection();
415
    }
416
417
    /**
418
     * Does this cell contain valid value?
419
     *
420
     * @return bool
421
     */
422 3
    public function hasValidValue()
423
    {
424 3
        $validator = new DataValidator();
425
426 3
        return $validator->isValid($this);
427
    }
428
429
    /**
430
     * Does this cell contain a Hyperlink?
431
     *
432
     * @throws Exception
433
     *
434
     * @return bool
435
     */
436
    public function hasHyperlink()
437
    {
438
        if (!isset($this->parent)) {
439
            throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet');
440
        }
441
442
        return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
443
    }
444
445
    /**
446
     * Get Hyperlink.
447
     *
448
     * @throws Exception
449
     *
450
     * @return Hyperlink
451
     */
452 21
    public function getHyperlink()
453
    {
454 21
        if (!isset($this->parent)) {
455
            throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet');
456
        }
457
458 21
        return $this->getWorksheet()->getHyperlink($this->getCoordinate());
459
    }
460
461
    /**
462
     * Set Hyperlink.
463
     *
464
     * @param Hyperlink $pHyperlink
465
     *
466
     * @throws Exception
467
     *
468
     * @return Cell
469
     */
470 View Code Duplication
    public function setHyperlink(Hyperlink $pHyperlink = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
471
    {
472
        if (!isset($this->parent)) {
473
            throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet');
474
        }
475
476
        $this->getWorksheet()->setHyperlink($this->getCoordinate(), $pHyperlink);
477
478
        return $this->updateInCollection();
479
    }
480
481
    /**
482
     * Get cell collection.
483
     *
484
     * @return Cells
485
     */
486 64
    public function getParent()
487
    {
488 64
        return $this->parent;
489
    }
490
491
    /**
492
     * Get parent worksheet.
493
     *
494
     * @return Worksheet
495
     */
496 80
    public function getWorksheet()
497
    {
498 80
        return $this->parent->getParent();
499
    }
500
501
    /**
502
     * Is this cell in a merge range.
503
     *
504
     * @return bool
505
     */
506
    public function isInMergeRange()
507
    {
508
        return (bool) $this->getMergeRange();
509
    }
510
511
    /**
512
     * Is this cell the master (top left cell) in a merge range (that holds the actual data value).
513
     *
514
     * @return bool
515
     */
516
    public function isMergeRangeValueCell()
517
    {
518
        if ($mergeRange = $this->getMergeRange()) {
519
            $mergeRange = self::splitRange($mergeRange);
0 ignored issues
show
Documentation introduced by
$mergeRange is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
520
            list($startCell) = $mergeRange[0];
521
            if ($this->getCoordinate() === $startCell) {
522
                return true;
523
            }
524
        }
525
526
        return false;
527
    }
528
529
    /**
530
     * If this cell is in a merge range, then return the range.
531
     *
532
     * @return string
533
     */
534
    public function getMergeRange()
535
    {
536
        foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
537
            if ($this->isInRange($mergeRange)) {
0 ignored issues
show
Documentation introduced by
$mergeRange is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
538
                return $mergeRange;
539
            }
540
        }
541
542
        return false;
543
    }
544
545
    /**
546
     * Get cell style.
547
     *
548
     * @return Style
549
     */
550 6
    public function getStyle()
551
    {
552 6
        return $this->getWorksheet()->getStyle($this->getCoordinate());
553
    }
554
555
    /**
556
     * Re-bind parent.
557
     *
558
     * @param Worksheet $parent
559
     *
560
     * @return Cell
561
     */
562
    public function rebindParent(Worksheet $parent)
563
    {
564
        $this->parent = $parent->getCellCollection();
565
566
        return $this->updateInCollection();
567
    }
568
569
    /**
570
     *    Is cell in a specific range?
571
     *
572
     * @param string $pRange Cell range (e.g. A1:A1)
573
     *
574
     * @return bool
575
     */
576
    public function isInRange($pRange)
577
    {
578
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);
579
580
        // Translate properties
581
        $myColumn = self::columnIndexFromString($this->getColumn());
582
        $myRow = $this->getRow();
583
584
        // Verify if cell is in range
585
        return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn) &&
586
                ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
587
    }
588
589
    /**
590
     * Coordinate from string.
591
     *
592
     * @param string $pCoordinateString eg: 'A1'
593
     *
594
     * @throws Exception
595
     *
596
     * @return string[] Array containing column and row (indexes 0 and 1)
597
     */
598 174
    public static function coordinateFromString($pCoordinateString)
599
    {
600 174
        if (preg_match("/^([$]?[A-Z]{1,3})([$]?\d{1,7})$/", $pCoordinateString, $matches)) {
601 171
            return [$matches[1], $matches[2]];
602 4
        } elseif ((strpos($pCoordinateString, ':') !== false) || (strpos($pCoordinateString, ',') !== false)) {
603 1
            throw new Exception('Cell coordinate string can not be a range of cells');
604 3
        } elseif ($pCoordinateString == '') {
605 1
            throw new Exception('Cell coordinate can not be zero-length string');
606
        }
607
608 2
        throw new Exception('Invalid cell coordinate ' . $pCoordinateString);
609
    }
610
611
    /**
612
     * Make string row, column or cell coordinate absolute.
613
     *
614
     * @param string $pCoordinateString e.g. 'A' or '1' or 'A1'
615
     *                    Note that this value can be a row or column reference as well as a cell reference
616
     *
617
     * @throws Exception
618
     *
619
     * @return string Absolute coordinate        e.g. '$A' or '$1' or '$A$1'
620
     */
621 21
    public static function absoluteReference($pCoordinateString)
622
    {
623 21
        if (strpos($pCoordinateString, ':') === false && strpos($pCoordinateString, ',') === false) {
624
            // Split out any worksheet name from the reference
625 20
            $worksheet = '';
626 20
            $cellAddress = explode('!', $pCoordinateString);
627 20
            if (count($cellAddress) > 1) {
628 8
                list($worksheet, $pCoordinateString) = $cellAddress;
629
            }
630 20
            if ($worksheet > '') {
631 8
                $worksheet .= '!';
632
            }
633
634
            // Create absolute coordinate
635 20
            if (ctype_digit($pCoordinateString)) {
636 2
                return $worksheet . '$' . $pCoordinateString;
637 18
            } elseif (ctype_alpha($pCoordinateString)) {
638 2
                return $worksheet . '$' . strtoupper($pCoordinateString);
639
            }
640
641 16
            return $worksheet . self::absoluteCoordinate($pCoordinateString);
642
        }
643
644 1
        throw new Exception('Cell coordinate string can not be a range of cells');
645
    }
646
647
    /**
648
     * Make string coordinate absolute.
649
     *
650
     * @param string $pCoordinateString e.g. 'A1'
651
     *
652
     * @throws Exception
653
     *
654
     * @return string Absolute coordinate        e.g. '$A$1'
655
     */
656 32
    public static function absoluteCoordinate($pCoordinateString)
657
    {
658 32
        if (strpos($pCoordinateString, ':') === false && strpos($pCoordinateString, ',') === false) {
659
            // Split out any worksheet name from the coordinate
660 31
            $worksheet = '';
661 31
            $cellAddress = explode('!', $pCoordinateString);
662 31
            if (count($cellAddress) > 1) {
663 6
                list($worksheet, $pCoordinateString) = $cellAddress;
664
            }
665 31
            if ($worksheet > '') {
666 6
                $worksheet .= '!';
667
            }
668
669
            // Create absolute coordinate
670 31
            list($column, $row) = self::coordinateFromString($pCoordinateString);
671 31
            $column = ltrim($column, '$');
672 31
            $row = ltrim($row, '$');
673
674 31
            return $worksheet . '$' . $column . '$' . $row;
675
        }
676
677 1
        throw new Exception('Cell coordinate string can not be a range of cells');
678
    }
679
680
    /**
681
     * Split range into coordinate strings.
682
     *
683
     * @param string $pRange e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4'
684
     *
685
     * @return array Array containg one or more arrays containing one or two coordinate strings
686
     *                                e.g. array('B4','D9') or array(array('B4','D9'),array('H2','O11'))
687
     *                                        or array('B4')
688
     */
689 104
    public static function splitRange($pRange)
690
    {
691
        // Ensure $pRange is a valid range
692 104
        if (empty($pRange)) {
693
            $pRange = self::DEFAULT_RANGE;
694
        }
695
696 104
        $exploded = explode(',', $pRange);
697 104
        $counter = count($exploded);
698 104
        for ($i = 0; $i < $counter; ++$i) {
699 104
            $exploded[$i] = explode(':', $exploded[$i]);
700
        }
701
702 104
        return $exploded;
703
    }
704
705
    /**
706
     * Build range from coordinate strings.
707
     *
708
     * @param array $pRange Array containg one or more arrays containing one or two coordinate strings
709
     *
710
     * @throws Exception
711
     *
712
     * @return string String representation of $pRange
713
     */
714 19
    public static function buildRange(array $pRange)
715
    {
716
        // Verify range
717 19
        if (empty($pRange) || !is_array($pRange[0])) {
718
            throw new Exception('Range does not contain any information');
719
        }
720
721
        // Build range
722 19
        $imploded = [];
0 ignored issues
show
Unused Code introduced by
$imploded is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
723 19
        $counter = count($pRange);
724 19
        for ($i = 0; $i < $counter; ++$i) {
725 19
            $pRange[$i] = implode(':', $pRange[$i]);
726
        }
727 19
        $imploded = implode(',', $pRange);
728
729 19
        return $imploded;
730
    }
731
732
    /**
733
     * Calculate range boundaries.
734
     *
735
     * @param string $pRange Cell range (e.g. A1:A1)
736
     *
737
     * @return array Range coordinates array(Start Cell, End Cell)
738
     *                    where Start Cell and End Cell are arrays (Column Number, Row Number)
739
     */
740 65
    public static function rangeBoundaries($pRange)
741
    {
742
        // Ensure $pRange is a valid range
743 65
        if (empty($pRange)) {
744
            $pRange = self::DEFAULT_RANGE;
745
        }
746
747
        // Uppercase coordinate
748 65
        $pRange = strtoupper($pRange);
749
750
        // Extract range
751 65 View Code Duplication
        if (strpos($pRange, ':') === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
752 2
            $rangeA = $rangeB = $pRange;
753
        } else {
754 63
            list($rangeA, $rangeB) = explode(':', $pRange);
755
        }
756
757
        // Calculate range outer borders
758 65
        $rangeStart = self::coordinateFromString($rangeA);
759 65
        $rangeEnd = self::coordinateFromString($rangeB);
760
761
        // Translate column into index
762 65
        $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
763 65
        $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);
764
765 65
        return [$rangeStart, $rangeEnd];
766
    }
767
768
    /**
769
     * Calculate range dimension.
770
     *
771
     * @param string $pRange Cell range (e.g. A1:A1)
772
     *
773
     * @return array Range dimension (width, height)
774
     */
775 16
    public static function rangeDimension($pRange)
776
    {
777
        // Calculate range outer borders
778 16
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);
779
780 16
        return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
781
    }
782
783
    /**
784
     * Calculate range boundaries.
785
     *
786
     * @param string $pRange Cell range (e.g. A1:A1)
787
     *
788
     * @return array Range coordinates array(Start Cell, End Cell)
789
     *                    where Start Cell and End Cell are arrays (Column ID, Row Number)
790
     */
791 13
    public static function getRangeBoundaries($pRange)
792
    {
793
        // Ensure $pRange is a valid range
794 13
        if (empty($pRange)) {
795
            $pRange = self::DEFAULT_RANGE;
796
        }
797
798
        // Uppercase coordinate
799 13
        $pRange = strtoupper($pRange);
800
801
        // Extract range
802 13 View Code Duplication
        if (strpos($pRange, ':') === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
803 1
            $rangeA = $rangeB = $pRange;
804
        } else {
805 12
            list($rangeA, $rangeB) = explode(':', $pRange);
806
        }
807
808 13
        return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
809
    }
810
811
    /**
812
     * Column index from string.
813
     *
814
     * @param string $pString eg 'A'
815
     *
816
     * @return int Column index (base 1 !!!)
817
     */
818 166
    public static function columnIndexFromString($pString)
819
    {
820
        //    Using a lookup cache adds a slight memory overhead, but boosts speed
821
        //    caching using a static within the method is faster than a class static,
822
        //        though it's additional memory overhead
823 166
        static $indexCache = [];
824
825 166
        if (isset($indexCache[$pString])) {
826 151
            return $indexCache[$pString];
827
        }
828
        //    It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord()
829
        //        and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant
830
        //        memory overhead either
831 118
        static $columnLookup = [
832
            'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13,
833
            'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
834
            'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
835
            'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
836
        ];
837
838
        //    We also use the language construct isset() rather than the more costly strlen() function to match the length of $pString
839
        //        for improved performance
840 118
        if (isset($pString[0])) {
841 116
            if (!isset($pString[1])) {
842 105
                $indexCache[$pString] = $columnLookup[$pString];
843
844 105
                return $indexCache[$pString];
845 12
            } elseif (!isset($pString[2])) {
846 9
                $indexCache[$pString] = $columnLookup[$pString[0]] * 26 + $columnLookup[$pString[1]];
847
848 9
                return $indexCache[$pString];
849 3
            } elseif (!isset($pString[3])) {
850 2
                $indexCache[$pString] = $columnLookup[$pString[0]] * 676 + $columnLookup[$pString[1]] * 26 + $columnLookup[$pString[2]];
851
852 2
                return $indexCache[$pString];
853
            }
854
        }
855
856 3
        throw new Exception('Column string index can not be ' . ((isset($pString[0])) ? 'longer than 3 characters' : 'empty'));
857
    }
858
859
    /**
860
     * String from columnindex.
861
     *
862
     * @param int $columnIndex Column index (A = 0)
863
     *
864
     * @return string
865
     */
866 133
    public static function stringFromColumnIndex($columnIndex)
867
    {
868 133
        static $indexCache = [];
869
870 133
        if (!isset($indexCache[$columnIndex])) {
871 114
            $indexValue = $columnIndex + 1;
872 114
            $base26 = null;
873
            do {
874 114
                $characterValue = ($indexValue % 26) ?: 26;
875 114
                $indexValue = ($indexValue - $characterValue) / 26;
876 114
                $base26 = chr($characterValue + 64) . ($base26 ?: '');
877 114
            } while ($indexValue > 0);
878 114
            $indexCache[$columnIndex] = $base26;
879
        }
880
881 133
        return $indexCache[$columnIndex];
882
    }
883
884
    /**
885
     * Extract all cell references in range.
886
     *
887
     * @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25)
888
     *
889
     * @return array Array containing single cell references
890
     */
891 76
    public static function extractAllCellReferencesInRange($pRange)
892
    {
893
        // Returnvalue
894 76
        $returnValue = [];
895
896
        // Explode spaces
897 76
        $cellBlocks = explode(' ', str_replace('$', '', strtoupper($pRange)));
898 76
        foreach ($cellBlocks as $cellBlock) {
899
            // Single cell?
900 76
            if (strpos($cellBlock, ':') === false && strpos($cellBlock, ',') === false) {
901 61
                $returnValue[] = $cellBlock;
902
903 61
                continue;
904
            }
905
906
            // Range...
907 68
            $ranges = self::splitRange($cellBlock);
908 68
            foreach ($ranges as $range) {
909
                // Single cell?
910 68
                if (!isset($range[1])) {
911
                    $returnValue[] = $range[0];
912
913
                    continue;
914
                }
915
916
                // Range...
917 68
                list($rangeStart, $rangeEnd) = $range;
918 68
                sscanf($rangeStart, '%[A-Z]%d', $startCol, $startRow);
0 ignored issues
show
Bug introduced by
The variable $startRow does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
919 68
                sscanf($rangeEnd, '%[A-Z]%d', $endCol, $endRow);
0 ignored issues
show
Bug introduced by
The variable $endRow does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
920 68
                ++$endCol;
921
922
                // Current data
923 68
                $currentCol = $startCol;
924 68
                $currentRow = $startRow;
925
926
                // Loop cells
927 68
                while ($currentCol != $endCol) {
928 68
                    while ($currentRow <= $endRow) {
929 68
                        $returnValue[] = $currentCol . $currentRow;
930 68
                        ++$currentRow;
931
                    }
932 68
                    ++$currentCol;
933 68
                    $currentRow = $startRow;
934
                }
935
            }
936
        }
937
938
        //    Sort the result by column and row
939 76
        $sortKeys = [];
940 76
        foreach (array_unique($returnValue) as $coord) {
941 76
            sscanf($coord, '%[A-Z]%d', $column, $row);
0 ignored issues
show
Bug introduced by
The variable $row does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
942 76
            $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
943
        }
944 76
        ksort($sortKeys);
945
946
        // Return value
947 76
        return array_values($sortKeys);
948
    }
949
950
    /**
951
     * Convert an associative array of single cell coordinates to values to an associative array
952
     * of cell ranges to values.  Only adjacent cell coordinates with the same
953
     * value will be merged.  If the value is an object, it must implement the method getHashCode().
954
     *
955
     * For example, this function converts:
956
     *
957
     *    [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ]
958
     *
959
     * to:
960
     *
961
     *    [ 'A1:A3' => 'x', 'A4' => 'y' ]
962
     *
963
     * @param array $pCoordCollection associative array mapping coordinates to values
964
     *
965
     * @return array associative array mapping coordinate ranges to valuea
966
     */
967 6
    public static function mergeRangesInCollection(array $pCoordCollection)
968
    {
969 6
        $hashedValues = [];
970
971 6
        foreach ($pCoordCollection as $coord => $value) {
972 6
            list($column, $row) = self::coordinateFromString($coord);
973 6
            $row = (int) (ltrim($row, '$'));
974 6
            $hashCode = $column . '-' . (is_object($value) ? $value->getHashCode() : $value);
975
976 6
            if (!isset($hashedValues[$hashCode])) {
977 6
                $hashedValues[$hashCode] = (object) [
978 6
                    'value' => $value,
979 6
                    'col' => $column,
980 6
                    'rows' => [$row],
981
                ];
982
            } else {
983 6
                $hashedValues[$hashCode]->rows[] = $row;
984
            }
985
        }
986
987 6
        $mergedCoordCollection = [];
988 6
        ksort($hashedValues);
989
990 6
        foreach ($hashedValues as $hashedValue) {
991 6
            sort($hashedValue->rows);
992 6
            $rowStart = null;
993 6
            $rowEnd = null;
994 6
            $ranges = [];
995
996 6
            foreach ($hashedValue->rows as $row) {
997 6
                if ($rowStart === null) {
998 6
                    $rowStart = $row;
999 6
                    $rowEnd = $row;
1000 3
                } elseif ($rowEnd === $row - 1) {
1001 3
                    $rowEnd = $row;
1002
                } else {
1003 1 View Code Duplication
                    if ($rowStart == $rowEnd) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1004
                        $ranges[] = $hashedValue->col . $rowStart;
1005
                    } else {
1006 1
                        $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
1007
                    }
1008
1009 1
                    $rowStart = $row;
1010 6
                    $rowEnd = $row;
1011
                }
1012
            }
1013
1014 6 View Code Duplication
            if ($rowStart !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1015 6
                if ($rowStart == $rowEnd) {
1016 5
                    $ranges[] = $hashedValue->col . $rowStart;
1017
                } else {
1018 2
                    $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
1019
                }
1020
            }
1021
1022 6
            foreach ($ranges as $range) {
1023 6
                $mergedCoordCollection[$range] = $hashedValue->value;
1024
            }
1025
        }
1026
1027 6
        return $mergedCoordCollection;
1028
    }
1029
1030
    /**
1031
     * Compare 2 cells.
1032
     *
1033
     * @param Cell $a Cell a
1034
     * @param Cell $b Cell b
1035
     *
1036
     * @return int Result of comparison (always -1 or 1, never zero!)
1037
     */
1038
    public static function compareCells(Cell $a, Cell $b)
1039
    {
1040
        if ($a->getRow() < $b->getRow()) {
1041
            return -1;
1042
        } elseif ($a->getRow() > $b->getRow()) {
1043
            return 1;
1044
        } elseif (self::columnIndexFromString($a->getColumn()) < self::columnIndexFromString($b->getColumn())) {
1045
            return -1;
1046
        }
1047
1048
        return 1;
1049
    }
1050
1051
    /**
1052
     * Get value binder to use.
1053
     *
1054
     * @return IValueBinder
1055
     */
1056 79
    public static function getValueBinder()
1057
    {
1058 79
        if (self::$valueBinder === null) {
1059 71
            self::$valueBinder = new DefaultValueBinder();
1060
        }
1061
1062 79
        return self::$valueBinder;
1063
    }
1064
1065
    /**
1066
     * Set value binder to use.
1067
     *
1068
     * @param IValueBinder $binder
1069
     *
1070
     * @throws Exception
1071
     */
1072 2
    public static function setValueBinder(IValueBinder $binder)
1073
    {
1074 2
        self::$valueBinder = $binder;
1075 2
    }
1076
1077
    /**
1078
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1079
     */
1080 2 View Code Duplication
    public function __clone()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1081
    {
1082 2
        $vars = get_object_vars($this);
1083 2
        foreach ($vars as $key => $value) {
1084 2
            if ((is_object($value)) && ($key != 'parent')) {
1085
                $this->$key = clone $value;
1086
            } else {
1087 2
                $this->$key = $value;
1088
            }
1089
        }
1090 2
    }
1091
1092
    /**
1093
     * Get index to cellXf.
1094
     *
1095
     * @return int
1096
     */
1097 99
    public function getXfIndex()
1098
    {
1099 99
        return $this->xfIndex;
1100
    }
1101
1102
    /**
1103
     * Set index to cellXf.
1104
     *
1105
     * @param int $pValue
1106
     *
1107
     * @return Cell
1108
     */
1109 92
    public function setXfIndex($pValue)
1110
    {
1111 92
        $this->xfIndex = $pValue;
1112
1113 92
        return $this->updateInCollection();
1114
    }
1115
1116
    /**
1117
     * Set the formula attributes.
1118
     *
1119
     * @param mixed $pAttributes
1120
     */
1121
    public function setFormulaAttributes($pAttributes)
1122
    {
1123
        $this->formulaAttributes = $pAttributes;
1124
1125
        return $this;
1126
    }
1127
1128
    /**
1129
     * Get the formula attributes.
1130
     */
1131 18
    public function getFormulaAttributes()
1132
    {
1133 18
        return $this->formulaAttributes;
1134
    }
1135
1136
    /**
1137
     * Convert to string.
1138
     *
1139
     * @return string
1140
     */
1141
    public function __toString()
1142
    {
1143
        return (string) $this->getValue();
1144
    }
1145
}
1146