Completed
Push — develop ( 004a19...276177 )
by Adrien
21:44 queued 07:25
created

Cell::mergeRangesInCollection()   C

Complexity

Conditions 12
Paths 155

Size

Total Lines 62
Code Lines 41

Duplication

Lines 12
Ratio 19.35 %

Code Coverage

Tests 36
CRAP Score 12.0028

Importance

Changes 0
Metric Value
cc 12
eloc 41
nc 155
nop 1
dl 12
loc 62
ccs 36
cts 37
cp 0.973
crap 12.0028
rs 5.6398
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet;
4
5
use PhpOffice\PhpSpreadsheet\Collection\Cells;
6
7
/**
8
 *    Copyright (c) 2006 - 2016 PhpSpreadsheet.
9
 *
10
 *    This library is free software; you can redistribute it and/or
11
 *    modify it under the terms of the GNU Lesser General Public
12
 *    License as published by the Free Software Foundation; either
13
 *    version 2.1 of the License, or (at your option) any later version.
14
 *
15
 *    This library is distributed in the hope that it will be useful,
16
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
 *    Lesser General Public License for more details.
19
 *
20
 *    You should have received a copy of the GNU Lesser General Public
21
 *    License along with this library; if not, write to the Free Software
22
 *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23
 *
24
 *    @category    PhpSpreadsheet
25
 *
26
 *    @copyright    Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet)
27
 *    @license    http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt    LGPL
28
 */
29
class Cell
30
{
31
    /**
32
     * Default range variable constant.
33
     *
34
     * @var string
35
     */
36
    const DEFAULT_RANGE = 'A1:A1';
37
38
    /**
39
     * Value binder to use.
40
     *
41
     * @var Cell\IValueBinder
42
     */
43
    private static $valueBinder;
44
45
    /**
46
     * Value of the cell.
47
     *
48
     * @var mixed
49
     */
50
    private $value;
51
52
    /**
53
     *    Calculated value of the cell (used for caching)
54
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
55
     *        create the original spreadsheet file.
56
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
57
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
58
     *        values used by the formula have changed since it was last calculated.
59
     *
60
     * @var mixed
61
     */
62
    private $calculatedValue;
63
64
    /**
65
     * Type of the cell data.
66
     *
67
     * @var string
68
     */
69
    private $dataType;
70
71
    /**
72
     * Collection of cells.
73
     *
74
     * @var Cells
75
     */
76
    private $parent;
77
78
    /**
79
     * Index to cellXf.
80
     *
81
     * @var int
82
     */
83
    private $xfIndex = 0;
84
85
    /**
86
     * Attributes of the formula.
87
     */
88
    private $formulaAttributes;
89
90
    /**
91
     * Update the cell into the cell collection.
92
     *
93
     * @return self
94
     */
95 83
    public function updateInCollection()
96
    {
97 83
        $this->parent->update($this);
98
99 83
        return $this;
100
    }
101
102 74
    public function detach()
103
    {
104 74
        $this->parent = null;
105 74
    }
106
107 71
    public function attach(Cells $parent)
108
    {
109 71
        $this->parent = $parent;
110 71
    }
111
112
    /**
113
     * Create a new Cell.
114
     *
115
     * @param mixed $pValue
116
     * @param string $pDataType
117
     * @param Worksheet $pSheet
118
     *
119
     * @throws Exception
120
     */
121 85
    public function __construct($pValue, $pDataType, Worksheet $pSheet)
122
    {
123
        // Initialise cell value
124 85
        $this->value = $pValue;
125
126
        // Set worksheet cache
127 85
        $this->parent = $pSheet->getCellCollection();
128
129
        // Set datatype?
130 85
        if ($pDataType !== null) {
131 85
            if ($pDataType == Cell\DataType::TYPE_STRING2) {
132
                $pDataType = Cell\DataType::TYPE_STRING;
133
            }
134 85
            $this->dataType = $pDataType;
135
        } elseif (!self::getValueBinder()->bindValue($this, $pValue)) {
136
            throw new Exception('Value could not be bound to cell.');
137
        }
138 85
    }
139
140
    /**
141
     * Get cell coordinate column.
142
     *
143
     * @return string
144
     */
145 48
    public function getColumn()
146
    {
147 48
        return $this->parent->getCurrentColumn();
148
    }
149
150
    /**
151
     * Get cell coordinate row.
152
     *
153
     * @return int
154
     */
155 46
    public function getRow()
156
    {
157 46
        return $this->parent->getCurrentRow();
158
    }
159
160
    /**
161
     * Get cell coordinate.
162
     *
163
     * @return string
164
     */
165 85
    public function getCoordinate()
166
    {
167 85
        return $this->parent->getCurrentCoordinate();
168
    }
169
170
    /**
171
     * Get cell value.
172
     *
173
     * @return mixed
174
     */
175 79
    public function getValue()
176
    {
177 79
        return $this->value;
178
    }
179
180
    /**
181
     * Get cell value with formatting.
182
     *
183
     * @return string
184
     */
185 1
    public function getFormattedValue()
186
    {
187 1
        return (string) Style\NumberFormat::toFormattedString(
188 1
            $this->getCalculatedValue(),
189 1
            $this->getStyle()
190 1
                ->getNumberFormat()->getFormatCode()
191
        );
192
    }
193
194
    /**
195
     * Set cell value.
196
     *
197
     *    Sets the value for a cell, automatically determining the datatype using the value binder
198
     *
199
     * @param mixed $pValue Value
200
     *
201
     * @throws Exception
202
     *
203
     * @return Cell
204
     */
205 59
    public function setValue($pValue)
206
    {
207 59
        if (!self::getValueBinder()->bindValue($this, $pValue)) {
208
            throw new Exception('Value could not be bound to cell.');
209
        }
210
211 59
        return $this;
212
    }
213
214
    /**
215
     * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
216
     *
217
     * @param mixed $pValue Value
218
     * @param string $pDataType Explicit data type, see Cell\DataType::TYPE_*
219
     *
220
     * @throws Exception
221
     *
222
     * @return Cell
223
     */
224 80
    public function setValueExplicit($pValue, $pDataType)
225
    {
226
        // set the value according to data type
227
        switch ($pDataType) {
228 80
            case Cell\DataType::TYPE_NULL:
229 18
                $this->value = $pValue;
230 18
                break;
231 80
            case Cell\DataType::TYPE_STRING2:
232
                $pDataType = Cell\DataType::TYPE_STRING;
233
                // no break
234 80
            case Cell\DataType::TYPE_STRING:
235
                // Synonym for string
236 68
            case Cell\DataType::TYPE_INLINE:
237
                // Rich text
238 68
                $this->value = Cell\DataType::checkString($pValue);
239 68
                break;
240 67
            case Cell\DataType::TYPE_NUMERIC:
241 63
                $this->value = (float) $pValue;
242 63
                break;
243 33
            case Cell\DataType::TYPE_FORMULA:
244 31
                $this->value = (string) $pValue;
245 31
                break;
246 14
            case Cell\DataType::TYPE_BOOL:
247 14
                $this->value = (bool) $pValue;
248 14
                break;
249
            case Cell\DataType::TYPE_ERROR:
250
                $this->value = Cell\DataType::checkErrorCode($pValue);
251
                break;
252
            default:
253
                throw new Exception('Invalid datatype: ' . $pDataType);
254
                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...
255
        }
256
257
        // set the datatype
258 80
        $this->dataType = $pDataType;
259
260 80
        return $this->updateInCollection();
261
    }
262
263
    /**
264
     * Get calculated cell value.
265
     *
266
     * @param bool $resetLog Whether the calculation engine logger should be reset or not
267
     *
268
     * @throws Exception
269
     *
270
     * @return mixed
271
     */
272 44
    public function getCalculatedValue($resetLog = true)
273
    {
274 44
        if ($this->dataType == Cell\DataType::TYPE_FORMULA) {
275
            try {
276 26
                $result = Calculation::getInstance(
277 26
                    $this->getWorksheet()->getParent()
278 26
                )->calculateCellValue($this, $resetLog);
279
                //    We don't yet handle array returns
280 26
                if (is_array($result)) {
281 26
                    while (is_array($result)) {
282 6
                        $result = array_pop($result);
283
                    }
284
                }
285
            } catch (Exception $ex) {
286
                if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
287
                    return $this->calculatedValue; // Fallback for calculations referencing external files.
288
                }
289
                $result = '#N/A';
0 ignored issues
show
Unused Code introduced by
$result 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...
290
                throw new Calculation\Exception(
291
                    $this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage()
292
                );
293
            }
294
295 26
            if ($result === '#Not Yet Implemented') {
296
                return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
297
            }
298
299 26
            return $result;
300 44
        } elseif ($this->value instanceof RichText) {
301 2
            return $this->value->getPlainText();
302
        }
303
304 44
        return $this->value;
305
    }
306
307
    /**
308
     * Set old calculated value (cached).
309
     *
310
     * @param mixed $pValue Value
311
     *
312
     * @return Cell
313
     */
314 12
    public function setCalculatedValue($pValue)
315
    {
316 12
        if ($pValue !== null) {
317 12
            $this->calculatedValue = (is_numeric($pValue)) ? (float) $pValue : $pValue;
318
        }
319
320 12
        return $this->updateInCollection();
321
    }
322
323
    /**
324
     *    Get old calculated value (cached)
325
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
326
     *        create the original spreadsheet file.
327
     *    Note that this value is not guaranteed to refelect the actual calculated value because it is
328
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
329
     *        values used by the formula have changed since it was last calculated.
330
     *
331
     * @return mixed
332
     */
333
    public function getOldCalculatedValue()
334
    {
335
        return $this->calculatedValue;
336
    }
337
338
    /**
339
     * Get cell data type.
340
     *
341
     * @return string
342
     */
343 66
    public function getDataType()
344
    {
345 66
        return $this->dataType;
346
    }
347
348
    /**
349
     * Set cell data type.
350
     *
351
     * @param string $pDataType see Cell\DataType::TYPE_*
352
     *
353
     * @return Cell
354
     */
355
    public function setDataType($pDataType)
356
    {
357
        if ($pDataType == Cell\DataType::TYPE_STRING2) {
358
            $pDataType = Cell\DataType::TYPE_STRING;
359
        }
360
        $this->dataType = $pDataType;
361
362
        return $this->updateInCollection();
363
    }
364
365
    /**
366
     * Identify if the cell contains a formula.
367
     *
368
     * @return bool
369
     */
370
    public function isFormula()
371
    {
372
        return $this->dataType == Cell\DataType::TYPE_FORMULA;
373
    }
374
375
    /**
376
     *    Does this cell contain Data validation rules?
377
     *
378
     * @throws Exception
379
     *
380
     * @return bool
381
     */
382
    public function hasDataValidation()
383
    {
384
        if (!isset($this->parent)) {
385
            throw new Exception('Cannot check for data validation when cell is not bound to a worksheet');
386
        }
387
388
        return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
389
    }
390
391
    /**
392
     * Get Data validation rules.
393
     *
394
     * @throws Exception
395
     *
396
     * @return Cell\DataValidation
397
     */
398 2
    public function getDataValidation()
399
    {
400 2
        if (!isset($this->parent)) {
401
            throw new Exception('Cannot get data validation for cell that is not bound to a worksheet');
402
        }
403
404 2
        return $this->getWorksheet()->getDataValidation($this->getCoordinate());
405
    }
406
407
    /**
408
     * Set Data validation rules.
409
     *
410
     * @param Cell\DataValidation $pDataValidation
411
     *
412
     * @throws Exception
413
     *
414
     * @return Cell
415
     */
416 View Code Duplication
    public function setDataValidation(Cell\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...
417
    {
418
        if (!isset($this->parent)) {
419
            throw new Exception('Cannot set data validation for cell that is not bound to a worksheet');
420
        }
421
422
        $this->getWorksheet()->setDataValidation($this->getCoordinate(), $pDataValidation);
423
424
        return $this->updateInCollection();
425
    }
426
427
    /**
428
     *    Does this cell contain a Hyperlink?
429
     *
430
     * @throws Exception
431
     *
432
     * @return bool
433
     */
434
    public function hasHyperlink()
435
    {
436
        if (!isset($this->parent)) {
437
            throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet');
438
        }
439
440
        return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
441
    }
442
443
    /**
444
     * Get Hyperlink.
445
     *
446
     * @throws Exception
447
     *
448
     * @return Cell\Hyperlink
449
     */
450 19
    public function getHyperlink()
451
    {
452 19
        if (!isset($this->parent)) {
453
            throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet');
454
        }
455
456 19
        return $this->getWorksheet()->getHyperlink($this->getCoordinate());
457
    }
458
459
    /**
460
     * Set Hyperlink.
461
     *
462
     * @param Cell\Hyperlink $pHyperlink
463
     *
464
     * @throws Exception
465
     *
466
     * @return Cell
467
     */
468 View Code Duplication
    public function setHyperlink(Cell\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...
469
    {
470
        if (!isset($this->parent)) {
471
            throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet');
472
        }
473
474
        $this->getWorksheet()->setHyperlink($this->getCoordinate(), $pHyperlink);
475
476
        return $this->updateInCollection();
477
    }
478
479
    /**
480
     * Get cell collection.
481
     *
482
     * @return Cells
483
     */
484 41
    public function getParent()
485
    {
486 41
        return $this->parent;
487
    }
488
489
    /**
490
     * Get parent worksheet.
491
     *
492
     * @return Worksheet
493
     */
494 54
    public function getWorksheet()
495
    {
496 54
        return $this->parent->getParent();
497
    }
498
499
    /**
500
     * Is this cell in a merge range.
501
     *
502
     * @return bool
503
     */
504
    public function isInMergeRange()
505
    {
506
        return (bool) $this->getMergeRange();
507
    }
508
509
    /**
510
     * Is this cell the master (top left cell) in a merge range (that holds the actual data value).
511
     *
512
     * @return bool
513
     */
514
    public function isMergeRangeValueCell()
515
    {
516
        if ($mergeRange = $this->getMergeRange()) {
517
            $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...
518
            list($startCell) = $mergeRange[0];
519
            if ($this->getCoordinate() === $startCell) {
520
                return true;
521
            }
522
        }
523
524
        return false;
525
    }
526
527
    /**
528
     * If this cell is in a merge range, then return the range.
529
     *
530
     * @return string
531
     */
532
    public function getMergeRange()
533
    {
534
        foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
535
            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...
536
                return $mergeRange;
537
            }
538
        }
539
540
        return false;
541
    }
542
543
    /**
544
     * Get cell style.
545
     *
546
     * @return Style
547
     */
548 2
    public function getStyle()
549
    {
550 2
        return $this->getWorksheet()->getStyle($this->getCoordinate());
551
    }
552
553
    /**
554
     * Re-bind parent.
555
     *
556
     * @param Worksheet $parent
557
     *
558
     * @return Cell
559
     */
560
    public function rebindParent(Worksheet $parent)
561
    {
562
        $this->parent = $parent->getCellCollection();
563
564
        return $this->updateInCollection();
565
    }
566
567
    /**
568
     *    Is cell in a specific range?
569
     *
570
     * @param string $pRange Cell range (e.g. A1:A1)
571
     *
572
     * @return bool
573
     */
574
    public function isInRange($pRange)
575
    {
576
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);
577
578
        // Translate properties
579
        $myColumn = self::columnIndexFromString($this->getColumn());
580
        $myRow = $this->getRow();
581
582
        // Verify if cell is in range
583
        return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn) &&
584
                ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
585
    }
586
587
    /**
588
     * Coordinate from string.
589
     *
590
     * @param string $pCoordinateString eg: 'A1'
591
     *
592
     * @throws Exception
593
     *
594
     * @return string[] Array containing column and row (indexes 0 and 1)
595
     */
596 134
    public static function coordinateFromString($pCoordinateString)
597
    {
598 134
        if (preg_match("/^([$]?[A-Z]{1,3})([$]?\d{1,7})$/", $pCoordinateString, $matches)) {
599 131
            return [$matches[1], $matches[2]];
600 3
        } elseif ((strpos($pCoordinateString, ':') !== false) || (strpos($pCoordinateString, ',') !== false)) {
601 1
            throw new Exception('Cell coordinate string can not be a range of cells');
602 2
        } elseif ($pCoordinateString == '') {
603 1
            throw new Exception('Cell coordinate can not be zero-length string');
604
        }
605
606 1
        throw new Exception('Invalid cell coordinate ' . $pCoordinateString);
607
    }
608
609
    /**
610
     * Make string row, column or cell coordinate absolute.
611
     *
612
     * @param string $pCoordinateString e.g. 'A' or '1' or 'A1'
613
     *                    Note that this value can be a row or column reference as well as a cell reference
614
     *
615
     * @throws Exception
616
     *
617
     * @return string Absolute coordinate        e.g. '$A' or '$1' or '$A$1'
618
     */
619 21
    public static function absoluteReference($pCoordinateString)
620
    {
621 21
        if (strpos($pCoordinateString, ':') === false && strpos($pCoordinateString, ',') === false) {
622
            // Split out any worksheet name from the reference
623 20
            $worksheet = '';
624 20
            $cellAddress = explode('!', $pCoordinateString);
625 20
            if (count($cellAddress) > 1) {
626 8
                list($worksheet, $pCoordinateString) = $cellAddress;
627
            }
628 20
            if ($worksheet > '') {
629 8
                $worksheet .= '!';
630
            }
631
632
            // Create absolute coordinate
633 20
            if (ctype_digit($pCoordinateString)) {
634 2
                return $worksheet . '$' . $pCoordinateString;
635 18
            } elseif (ctype_alpha($pCoordinateString)) {
636 2
                return $worksheet . '$' . strtoupper($pCoordinateString);
637
            }
638
639 16
            return $worksheet . self::absoluteCoordinate($pCoordinateString);
640
        }
641
642 1
        throw new Exception('Cell coordinate string can not be a range of cells');
643
    }
644
645
    /**
646
     * Make string coordinate absolute.
647
     *
648
     * @param string $pCoordinateString e.g. 'A1'
649
     *
650
     * @throws Exception
651
     *
652
     * @return string Absolute coordinate        e.g. '$A$1'
653
     */
654 32
    public static function absoluteCoordinate($pCoordinateString)
655
    {
656 32
        if (strpos($pCoordinateString, ':') === false && strpos($pCoordinateString, ',') === false) {
657
            // Split out any worksheet name from the coordinate
658 31
            $worksheet = '';
659 31
            $cellAddress = explode('!', $pCoordinateString);
660 31
            if (count($cellAddress) > 1) {
661 6
                list($worksheet, $pCoordinateString) = $cellAddress;
662
            }
663 31
            if ($worksheet > '') {
664 6
                $worksheet .= '!';
665
            }
666
667
            // Create absolute coordinate
668 31
            list($column, $row) = self::coordinateFromString($pCoordinateString);
669 31
            $column = ltrim($column, '$');
670 31
            $row = ltrim($row, '$');
671
672 31
            return $worksheet . '$' . $column . '$' . $row;
673
        }
674
675 1
        throw new Exception('Cell coordinate string can not be a range of cells');
676
    }
677
678
    /**
679
     * Split range into coordinate strings.
680
     *
681
     * @param string $pRange e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4'
682
     *
683
     * @return array Array containg one or more arrays containing one or two coordinate strings
684
     *                                e.g. array('B4','D9') or array(array('B4','D9'),array('H2','O11'))
685
     *                                        or array('B4')
686
     */
687 73
    public static function splitRange($pRange)
688
    {
689
        // Ensure $pRange is a valid range
690 73
        if (empty($pRange)) {
691
            $pRange = self::DEFAULT_RANGE;
692
        }
693
694 73
        $exploded = explode(',', $pRange);
695 73
        $counter = count($exploded);
696 73
        for ($i = 0; $i < $counter; ++$i) {
697 73
            $exploded[$i] = explode(':', $exploded[$i]);
698
        }
699
700 73
        return $exploded;
701
    }
702
703
    /**
704
     * Build range from coordinate strings.
705
     *
706
     * @param array $pRange Array containg one or more arrays containing one or two coordinate strings
707
     *
708
     * @throws Exception
709
     *
710
     * @return string String representation of $pRange
711
     */
712 19
    public static function buildRange(array $pRange)
713
    {
714
        // Verify range
715 19
        if (empty($pRange) || !is_array($pRange[0])) {
716
            throw new Exception('Range does not contain any information');
717
        }
718
719
        // Build range
720 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...
721 19
        $counter = count($pRange);
722 19
        for ($i = 0; $i < $counter; ++$i) {
723 19
            $pRange[$i] = implode(':', $pRange[$i]);
724
        }
725 19
        $imploded = implode(',', $pRange);
726
727 19
        return $imploded;
728
    }
729
730
    /**
731
     * Calculate range boundaries.
732
     *
733
     * @param string $pRange Cell range (e.g. A1:A1)
734
     *
735
     * @return array Range coordinates array(Start Cell, End Cell)
736
     *                    where Start Cell and End Cell are arrays (Column Number, Row Number)
737
     */
738 42
    public static function rangeBoundaries($pRange)
739
    {
740
        // Ensure $pRange is a valid range
741 42
        if (empty($pRange)) {
742
            $pRange = self::DEFAULT_RANGE;
743
        }
744
745
        // Uppercase coordinate
746 42
        $pRange = strtoupper($pRange);
747
748
        // Extract range
749 42 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...
750 2
            $rangeA = $rangeB = $pRange;
751
        } else {
752 40
            list($rangeA, $rangeB) = explode(':', $pRange);
753
        }
754
755
        // Calculate range outer borders
756 42
        $rangeStart = self::coordinateFromString($rangeA);
757 42
        $rangeEnd = self::coordinateFromString($rangeB);
758
759
        // Translate column into index
760 42
        $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
761 42
        $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);
762
763 42
        return [$rangeStart, $rangeEnd];
764
    }
765
766
    /**
767
     * Calculate range dimension.
768
     *
769
     * @param string $pRange Cell range (e.g. A1:A1)
770
     *
771
     * @return array Range dimension (width, height)
772
     */
773 15
    public static function rangeDimension($pRange)
774
    {
775
        // Calculate range outer borders
776 15
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);
777
778 15
        return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
779
    }
780
781
    /**
782
     * Calculate range boundaries.
783
     *
784
     * @param string $pRange Cell range (e.g. A1:A1)
785
     *
786
     * @return array Range coordinates array(Start Cell, End Cell)
787
     *                    where Start Cell and End Cell are arrays (Column ID, Row Number)
788
     */
789 4
    public static function getRangeBoundaries($pRange)
790
    {
791
        // Ensure $pRange is a valid range
792 4
        if (empty($pRange)) {
793
            $pRange = self::DEFAULT_RANGE;
794
        }
795
796
        // Uppercase coordinate
797 4
        $pRange = strtoupper($pRange);
798
799
        // Extract range
800 4 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...
801 1
            $rangeA = $rangeB = $pRange;
802
        } else {
803 3
            list($rangeA, $rangeB) = explode(':', $pRange);
804
        }
805
806 4
        return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
807
    }
808
809
    /**
810
     * Column index from string.
811
     *
812
     * @param string $pString eg 'A'
813
     *
814
     * @return int Column index (base 1 !!!)
815
     */
816 126
    public static function columnIndexFromString($pString)
817
    {
818
        //    Using a lookup cache adds a slight memory overhead, but boosts speed
819
        //    caching using a static within the method is faster than a class static,
820
        //        though it's additional memory overhead
821 126
        static $_indexCache = [];
822
823 126
        if (isset($_indexCache[$pString])) {
824 111
            return $_indexCache[$pString];
825
        }
826
        //    It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord()
827
        //        and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant
828
        //        memory overhead either
829 83
        static $_columnLookup = [
830
            '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,
831
            '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,
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
        ];
835
836
        //    We also use the language construct isset() rather than the more costly strlen() function to match the length of $pString
837
        //        for improved performance
838 83
        if (isset($pString[0])) {
839 81
            if (!isset($pString[1])) {
840 70
                $_indexCache[$pString] = $_columnLookup[$pString];
841
842 70
                return $_indexCache[$pString];
843 12
            } elseif (!isset($pString[2])) {
844 9
                $_indexCache[$pString] = $_columnLookup[$pString[0]] * 26 + $_columnLookup[$pString[1]];
845
846 9
                return $_indexCache[$pString];
847 3
            } elseif (!isset($pString[3])) {
848 2
                $_indexCache[$pString] = $_columnLookup[$pString[0]] * 676 + $_columnLookup[$pString[1]] * 26 + $_columnLookup[$pString[2]];
849
850 2
                return $_indexCache[$pString];
851
            }
852
        }
853 3
        throw new Exception('Column string index can not be ' . ((isset($pString[0])) ? 'longer than 3 characters' : 'empty'));
854
    }
855
856
    /**
857
     * String from columnindex.
858
     *
859
     * @param int $pColumnIndex Column index (A = 0)
860
     *
861
     * @return string
862
     */
863 96
    public static function stringFromColumnIndex($pColumnIndex)
864
    {
865
        //    Using a lookup cache adds a slight memory overhead, but boosts speed
866
        //    caching using a static within the method is faster than a class static,
867
        //        though it's additional memory overhead
868 96
        static $_indexCache = [];
869
870 96
        if (!isset($_indexCache[$pColumnIndex])) {
871
            // Determine column string
872 79
            if ($pColumnIndex < 26) {
873 69
                $_indexCache[$pColumnIndex] = chr(65 + $pColumnIndex);
874 12
            } elseif ($pColumnIndex < 702) {
875 9
                $_indexCache[$pColumnIndex] = chr(64 + ($pColumnIndex / 26)) .
876 9
                                                chr(65 + $pColumnIndex % 26);
877
            } else {
878 3
                $_indexCache[$pColumnIndex] = chr(64 + (($pColumnIndex - 26) / 676)) .
879 3
                                                chr(65 + ((($pColumnIndex - 26) % 676) / 26)) .
880 3
                                                chr(65 + $pColumnIndex % 26);
881
            }
882
        }
883
884 96
        return $_indexCache[$pColumnIndex];
885
    }
886
887
    /**
888
     * Extract all cell references in range.
889
     *
890
     * @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25)
891
     *
892
     * @return array Array containing single cell references
893
     */
894 47
    public static function extractAllCellReferencesInRange($pRange)
895
    {
896
        // Returnvalue
897 47
        $returnValue = [];
898
899
        // Explode spaces
900 47
        $cellBlocks = explode(' ', str_replace('$', '', strtoupper($pRange)));
901 47
        foreach ($cellBlocks as $cellBlock) {
902
            // Single cell?
903 47
            if (strpos($cellBlock, ':') === false && strpos($cellBlock, ',') === false) {
904 38
                $returnValue[] = $cellBlock;
905 38
                continue;
906
            }
907
908
            // Range...
909 45
            $ranges = self::splitRange($cellBlock);
910 45
            foreach ($ranges as $range) {
911
                // Single cell?
912 45
                if (!isset($range[1])) {
913
                    $returnValue[] = $range[0];
914
                    continue;
915
                }
916
917
                // Range...
918 45
                list($rangeStart, $rangeEnd) = $range;
919 45
                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...
920 45
                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...
921 45
                ++$endCol;
922
923
                // Current data
924 45
                $currentCol = $startCol;
925 45
                $currentRow = $startRow;
926
927
                // Loop cells
928 45
                while ($currentCol != $endCol) {
929 45
                    while ($currentRow <= $endRow) {
930 45
                        $returnValue[] = $currentCol . $currentRow;
931 45
                        ++$currentRow;
932
                    }
933 45
                    ++$currentCol;
934 45
                    $currentRow = $startRow;
935
                }
936
            }
937
        }
938
939
        //    Sort the result by column and row
940 47
        $sortKeys = [];
941 47
        foreach (array_unique($returnValue) as $coord) {
942 47
            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...
943 47
            $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
944
        }
945 47
        ksort($sortKeys);
946
947
        // Return value
948 47
        return array_values($sortKeys);
949
    }
950
951
    /**
952
     * Convert an associative array of single cell coordinates to values to an associative array
953
     * of cell ranges to values.  Only adjacent cell coordinates with the same
954
     * value will be merged.  If the value is an object, it must implement the method getHashCode().
955
     *
956
     * For example, this function converts:
957
     *
958
     *    [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ]
959
     *
960
     * to:
961
     *
962
     *    [ 'A1:A3' => 'x', 'A4' => 'y' ]
963
     *
964
     * @param array $pCoordCollection associative array mapping coordinates to values
965
     *
966
     * @return array associative array mapping coordinate ranges to valuea
967
     */
968 6
    public static function mergeRangesInCollection(array $pCoordCollection)
969
    {
970 6
        $hashedValues = [];
971
972 6
        foreach ($pCoordCollection as $coord => $value) {
973 6
            list($column, $row) = self::coordinateFromString($coord);
974 6
            $row = (int) (ltrim($row, '$'));
975 6
            $hashCode = $column . '-' . (is_object($value) ? $value->getHashCode() : $value);
976
977 6
            if (!isset($hashedValues[$hashCode])) {
978 6
                $hashedValues[$hashCode] = (object) [
979 6
                    'value' => $value,
980 6
                    'col' => $column,
981 6
                    'rows' => [$row],
982
                ];
983
            } else {
984 6
                $hashedValues[$hashCode]->rows[] = $row;
985
            }
986
        }
987
988 6
        $mergedCoordCollection = [];
989 6
        ksort($hashedValues);
990
991 6
        foreach ($hashedValues as $hashedValue) {
992 6
            sort($hashedValue->rows);
993 6
            $rowStart = null;
994 6
            $rowEnd = null;
995 6
            $ranges = [];
996
997 6
            foreach ($hashedValue->rows as $row) {
998 6
                if ($rowStart === null) {
999 6
                    $rowStart = $row;
1000 6
                    $rowEnd = $row;
1001 3
                } elseif ($rowEnd === $row - 1) {
1002 3
                    $rowEnd = $row;
1003
                } else {
1004 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...
1005
                        $ranges[] = $hashedValue->col . $rowStart;
1006
                    } else {
1007 1
                        $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
1008
                    }
1009
1010 1
                    $rowStart = $row;
1011 6
                    $rowEnd = $row;
1012
                }
1013
            }
1014
1015 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...
1016 6
                if ($rowStart == $rowEnd) {
1017 5
                    $ranges[] = $hashedValue->col . $rowStart;
1018
                } else {
1019 2
                    $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
1020
                }
1021
            }
1022
1023 6
            foreach ($ranges as $range) {
1024 6
                $mergedCoordCollection[$range] = $hashedValue->value;
1025
            }
1026
        }
1027
1028 6
        return $mergedCoordCollection;
1029
    }
1030
1031
    /**
1032
     * Compare 2 cells.
1033
     *
1034
     * @param Cell $a Cell a
1035
     * @param Cell $b Cell b
1036
     *
1037
     * @return int Result of comparison (always -1 or 1, never zero!)
1038
     */
1039
    public static function compareCells(Cell $a, Cell $b)
1040
    {
1041
        if ($a->getRow() < $b->getRow()) {
1042
            return -1;
1043
        } elseif ($a->getRow() > $b->getRow()) {
1044
            return 1;
1045
        } elseif (self::columnIndexFromString($a->getColumn()) < self::columnIndexFromString($b->getColumn())) {
1046
            return -1;
1047
        }
1048
1049
        return 1;
1050
    }
1051
1052
    /**
1053
     * Get value binder to use.
1054
     *
1055
     * @return Cell\IValueBinder
1056
     */
1057 59
    public static function getValueBinder()
1058
    {
1059 59
        if (self::$valueBinder === null) {
1060 54
            self::$valueBinder = new Cell\DefaultValueBinder();
1061
        }
1062
1063 59
        return self::$valueBinder;
1064
    }
1065
1066
    /**
1067
     * Set value binder to use.
1068
     *
1069
     * @param Cell\IValueBinder $binder
1070
     *
1071
     * @throws Exception
1072
     */
1073 1
    public static function setValueBinder(Cell\IValueBinder $binder)
1074
    {
1075 1
        self::$valueBinder = $binder;
1076 1
    }
1077
1078
    /**
1079
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1080
     */
1081 2
    public function __clone()
1082
    {
1083 2
        $vars = get_object_vars($this);
1084 2
        foreach ($vars as $key => $value) {
1085 2
            if ((is_object($value)) && ($key != 'parent')) {
1086
                $this->$key = clone $value;
1087
            } else {
1088 2
                $this->$key = $value;
1089
            }
1090
        }
1091 2
    }
1092
1093
    /**
1094
     * Get index to cellXf.
1095
     *
1096
     * @return int
1097
     */
1098 72
    public function getXfIndex()
1099
    {
1100 72
        return $this->xfIndex;
1101
    }
1102
1103
    /**
1104
     * Set index to cellXf.
1105
     *
1106
     * @param int $pValue
1107
     *
1108
     * @return Cell
1109
     */
1110 70
    public function setXfIndex($pValue)
1111
    {
1112 70
        $this->xfIndex = $pValue;
1113
1114 70
        return $this->updateInCollection();
1115
    }
1116
1117
    /**
1118
     * Set the formula attributes.
1119
     *
1120
     * @param mixed $pAttributes
1121
     */
1122
    public function setFormulaAttributes($pAttributes)
1123
    {
1124
        $this->formulaAttributes = $pAttributes;
1125
1126
        return $this;
1127
    }
1128
1129
    /**
1130
     * Get the formula attributes.
1131
     */
1132 18
    public function getFormulaAttributes()
1133
    {
1134 18
        return $this->formulaAttributes;
1135
    }
1136
1137
    /**
1138
     * Convert to string.
1139
     *
1140
     * @return string
1141
     */
1142
    public function __toString()
1143
    {
1144
        return (string) $this->getValue();
1145
    }
1146
}
1147