Failed Conditions
Pull Request — master (#3962)
by Owen
11:35
created

Cell::getCalculatedValue()   F

Complexity

Conditions 52
Paths > 20000

Size

Total Lines 167
Code Lines 120

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 62
CRAP Score 52.0108

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 120
c 1
b 0
f 0
dl 0
loc 167
rs 0
ccs 62
cts 63
cp 0.9841
cc 52
nc 237100
nop 1
crap 52.0108

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\Cell;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
9
use PhpOffice\PhpSpreadsheet\Collection\Cells;
10
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\RichText\RichText;
12
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
13
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
14
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
15
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
16
use PhpOffice\PhpSpreadsheet\Style\Protection;
17
use PhpOffice\PhpSpreadsheet\Style\Style;
18
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
19
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
20
use Stringable;
21
22
class Cell implements Stringable
23
{
24
    /**
25
     * Value binder to use.
26
     */
27
    private static ?IValueBinder $valueBinder = null;
28
29
    /**
30
     * Value of the cell.
31
     */
32
    private mixed $value;
33
34
    /**
35
     *    Calculated value of the cell (used for caching)
36
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
37
     *        create the original spreadsheet file.
38
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
39
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
40
     *        values used by the formula have changed since it was last calculated.
41
     *
42
     * @var mixed
43
     */
44
    private $calculatedValue;
45
46
    /**
47
     * Type of the cell data.
48
     */
49
    private string $dataType;
50
51
    /**
52
     * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet).
53
     */
54
    private ?Cells $parent;
55
56
    /**
57
     * Index to the cellXf reference for the styling of this cell.
58
     */
59
    private int $xfIndex = 0;
60
61
    /**
62
     * Attributes of the formula.
63
     *
64
     * @var null|array<string, string>
65
     */
66
    private mixed $formulaAttributes = null;
67
68
    private IgnoredErrors $ignoredErrors;
69
70
    /**
71
     * Update the cell into the cell collection.
72
     *
73 9754
     * @throws SpreadsheetException
74
     */
75 9754
    public function updateInCollection(): self
76 9754
    {
77 2
        $parent = $this->parent;
78
        if ($parent === null) {
79 9752
            throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet');
80
        }
81 9752
        $parent->update($this);
82
83
        return $this;
84 9276
    }
85
86 9276
    public function detach(): void
87
    {
88
        $this->parent = null;
89 8582
    }
90
91 8582
    public function attach(Cells $parent): void
92
    {
93
        $this->parent = $parent;
94
    }
95
96
    /**
97
     * Create a new Cell.
98
     *
99 9806
     * @throws SpreadsheetException
100
     */
101
    public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet)
102 9806
    {
103
        // Initialise cell value
104
        $this->value = $value;
105 9806
106
        // Set worksheet cache
107
        $this->parent = $worksheet->getCellCollection();
108 9806
109 9806
        // Set datatype?
110
        if ($dataType !== null) {
111
            if ($dataType == DataType::TYPE_STRING2) {
112 9806
                $dataType = DataType::TYPE_STRING;
113
            }
114
            $this->dataType = $dataType;
115
        } elseif (self::getValueBinder()->bindValue($this, $value) === false) {
116 9806
            throw new SpreadsheetException('Value could not be bound to cell.');
117
        }
118
        $this->ignoredErrors = new IgnoredErrors();
119
    }
120
121
    /**
122
     * Get cell coordinate column.
123
     *
124 557
     * @throws SpreadsheetException
125
     */
126 557
    public function getColumn(): string
127 557
    {
128 1
        $parent = $this->parent;
129
        if ($parent === null) {
130
            throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet');
131 556
        }
132
133
        return $parent->getCurrentColumn();
134
    }
135
136
    /**
137
     * Get cell coordinate row.
138
     *
139 541
     * @throws SpreadsheetException
140
     */
141 541
    public function getRow(): int
142 541
    {
143 1
        $parent = $this->parent;
144
        if ($parent === null) {
145
            throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet');
146 540
        }
147
148
        return $parent->getCurrentRow();
149
    }
150
151
    /**
152
     * Get cell coordinate.
153
     *
154 9760
     * @throws SpreadsheetException
155
     */
156 9760
    public function getCoordinate(): string
157 9760
    {
158 9760
        $parent = $this->parent;
159
        if ($parent !== null) {
160 2
            $coordinate = $parent->getCurrentCoordinate();
161
        } else {
162 9760
            $coordinate = null;
163 2
        }
164
        if ($coordinate === null) {
165
            throw new SpreadsheetException('Coordinate no longer exists');
166 9760
        }
167
168
        return $coordinate;
169
    }
170
171
    /**
172 9109
     * Get cell value.
173
     */
174 9109
    public function getValue(): mixed
175
    {
176
        return $this->value;
177 464
    }
178
179 464
    public function getValueString(): string
180
    {
181 464
        $value = $this->value;
182
183
        return ($value === '' || is_scalar($value) || $value instanceof Stringable) ? "$value" : '';
184
    }
185
186
    /**
187 94
     * Get cell value with formatting.
188
     */
189 94
    public function getFormattedValue(): string
190 94
    {
191 94
        $currentCalendar = SharedDate::getExcelCalendar();
192 94
        SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
193 94
        $formattedValue = (string) NumberFormat::toFormattedString(
194 94
            $this->getCalculatedValue(),
195 94
            (string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
196
        );
197 94
        SharedDate::setExcelCalendar($currentCalendar);
198
199
        return $formattedValue;
200 9708
    }
201
202 9708
    protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
203 9708
    {
204 9708
        $oldValue = (is_scalar($oldValue) || $oldValue instanceof Stringable) ? ((string) $oldValue) : null;
205 891
        $newValue = (is_scalar($newValue) || $newValue instanceof Stringable) ? ((string) $newValue) : null;
206
        if (StringHelper::strToLower($oldValue ?? '') === StringHelper::strToLower($newValue ?? '') || $workSheet === null) {
207
            return;
208 9685
        }
209
210 10
        foreach ($workSheet->getTableCollection() as $table) {
211 7
            /** @var Table $table */
212 7
            if ($cell->isInRange($table->getRange())) {
213 4
                $rangeRowsColumns = Coordinate::getRangeBoundaries($table->getRange());
214
                if ($cell->getRow() === (int) $rangeRowsColumns[0][1]) {
215
                    Table\Column::updateStructuredReferences($workSheet, $oldValue, $newValue);
216 7
                }
217
218
                return;
219
            }
220
        }
221
    }
222
223
    /**
224
     * Set cell value.
225
     *
226
     *    Sets the value for a cell, automatically determining the datatype using the value binder
227
     *
228
     * @param mixed $value Value
229
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
230
     *
231 9418
     * @throws SpreadsheetException
232
     */
233 9418
    public function setValue(mixed $value, ?IValueBinder $binder = null): self
234 9418
    {
235
        $binder ??= self::getValueBinder();
236
        if (!$binder->bindValue($this, $value)) {
237
            throw new SpreadsheetException('Value could not be bound to cell.');
238 9417
        }
239
240
        return $this;
241
    }
242
243
    /**
244
     * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
245
     *
246
     * @param mixed $value Value
247
     * @param string $dataType Explicit data type, see DataType::TYPE_*
248
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
249
     *             method, then it is your responsibility as an end-user developer to validate that the value and
250
     *             the datatype match.
251
     *       If you do mismatch value and datatype, then the value you enter may be changed to match the datatype
252
     *          that you specify.
253
     *
254 9713
     * @throws SpreadsheetException
255
     */
256 9713
    public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
257 9713
    {
258
        $oldValue = $this->value;
259
        $quotePrefix = false;
260
261 264
        // set the value according to data type
262 557
        switch ($dataType) {
263
            case DataType::TYPE_NULL:
264 557
                $this->value = null;
265 264
266 3
                break;
267
            case DataType::TYPE_STRING2:
268 264
                $dataType = DataType::TYPE_STRING;
269
                // no break
270 4943
            case DataType::TYPE_STRING:
271 39
                // Synonym for string
272
                if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
273
                    $quotePrefix = true;
274 238
                }
275
                // no break
276 4959
            case DataType::TYPE_INLINE:
277 2
                // Rich text
278
                if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) {
279 4957
                    throw new SpreadsheetException('Invalid unstringable value for datatype Inline/String/String2');
280
                }
281 4957
                $this->value = DataType::checkString(($value instanceof RichText) ? $value : ((string) $value));
282 237
283 7022
                break;
284 2
            case DataType::TYPE_NUMERIC:
285
                if (is_string($value) && !is_numeric($value)) {
286 7021
                    throw new SpreadsheetException('Invalid numeric value for datatype Numeric');
287
                }
288 7021
                $this->value = 0 + $value;
289 193
290 8176
                break;
291
            case DataType::TYPE_FORMULA:
292
                if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) {
293 8176
                    throw new SpreadsheetException('Invalid unstringable value for datatype Formula');
294
                }
295 8176
                $this->value = (string) $value;
296 18
297 601
                break;
298
            case DataType::TYPE_BOOL:
299 601
                $this->value = (bool) $value;
300
301 6
                break;
302 5
            case DataType::TYPE_ISO_DATE:
303
                $this->value = SharedDate::convertIsoDate($value);
304 5
                $dataType = DataType::TYPE_NUMERIC;
305
306 19
                break;
307
            case DataType::TYPE_ERROR:
308 19
                $this->value = DataType::checkErrorCode($value);
309
310 1
                break;
311
            default:
312
                throw new SpreadsheetException('Invalid datatype: ' . $dataType);
313
        }
314 9709
315
        // set the datatype
316 9709
        $this->dataType = $dataType;
317 9708
318 9708
        $this->updateInCollection();
319 9708
        $cellCoordinate = $this->getCoordinate();
320
        self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value);
321 9708
        $this->getWorksheet()->applyStylesFromArray($cellCoordinate, ['quotePrefix' => $quotePrefix]);
322
323
        return $this->getParent()?->get($cellCoordinate) ?? $this;
324
    }
325
326
    public const CALCULATE_DATE_TIME_ASIS = 0;
327
    public const CALCULATE_DATE_TIME_FLOAT = 1;
328
    public const CALCULATE_TIME_FLOAT = 2;
329
330 45
    private static int $calculateDateTimeType = self::CALCULATE_DATE_TIME_ASIS;
331
332 45
    public static function getCalculateDateTimeType(): int
333
    {
334
        return self::$calculateDateTimeType;
335
    }
336 45
337
    /** @throws CalculationException */
338 45
    public static function setCalculateDateTimeType(int $calculateDateTimeType): void
339 45
    {
340 1
        self::$calculateDateTimeType = match ($calculateDateTimeType) {
341 45
            self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType,
342
            default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"),
343
        };
344
    }
345
346
    /**
347 8542
     * Convert date, time, or datetime from int to float if desired.
348
     */
349 8542
    private function convertDateTimeInt(mixed $result): mixed
350 4465
    {
351 4
        if (is_int($result)) {
352 2
            if (self::$calculateDateTimeType === self::CALCULATE_TIME_FLOAT) {
353
                if (SharedDate::isDateTime($this, $result, false)) {
354 4461
                    $result = (float) $result;
355 4
                }
356 3
            } elseif (self::$calculateDateTimeType === self::CALCULATE_DATE_TIME_FLOAT) {
357
                if (SharedDate::isDateTime($this, $result, true)) {
358
                    $result = (float) $result;
359
                }
360
            }
361 8542
        }
362
363
        return $result;
364
    }
365
366
    /**
367 627
     * Get calculated cell value converted to string.
368
     */
369 627
    public function getCalculatedValueString(): string
370
    {
371 627
        $value = $this->getCalculatedValue();
372
        while (is_array($value)) {
373
            $value = array_shift($value);
374
        }
375
376
        return ($value === '' || is_scalar($value) || $value instanceof Stringable) ? "$value" : '';
377
    }
378
379
    /**
380
     * Get calculated cell value.
381 8781
     *
382
     * @param bool $resetLog Whether the calculation engine logger should be reset or not
383 8781
     *
384
     * @throws CalculationException
385 7834
     */
386 7834
    public function getCalculatedValue(bool $resetLog = true): mixed
387 7834
    {
388 7834
        $title = 'unknown';
389 7834
        $oldAttributes = $this->formulaAttributes;
390 7834
        $oldAttributesT = $oldAttributes['t'] ?? '';
391 7834
        $coordinate = $this->getCoordinate();
392 7593
        $oldAttributesRef = $oldAttributes['ref'] ?? $coordinate;
393 7593
        if (!str_contains($oldAttributesRef, ':')) {
394 7593
            $oldAttributesRef .= ":$oldAttributesRef";
395
        }
396 7593
        $originalValue = $this->value;
397 7593
        $originalDataType = $this->dataType;
398 4764
        $this->formulaAttributes = [];
399
        $spill = false;
400
401 259
        if ($this->dataType === DataType::TYPE_FORMULA) {
402 259
            try {
403 259
                $currentCalendar = SharedDate::getExcelCalendar();
404 1
                SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
405 259
                $thisworksheet = $this->getWorksheet();
406 29
                $index = $thisworksheet->getParentOrThrow()->getActiveSheetIndex();
407
                $selected = $thisworksheet->getSelectedCells();
408
                $title = $thisworksheet->getTitle();
409 230
                $calculation = Calculation::getInstance($thisworksheet->getParent());
410 230
                $result = $calculation->calculateCellValue($this, $resetLog);
411 230
                $result = $this->convertDateTimeInt($result);
412 230
                $thisworksheet->setSelectedCells($selected);
413 230
                $thisworksheet->getParentOrThrow()->setActiveSheetIndex($index);
414
                if (is_array($result) && $calculation->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
415 7593
                    while (is_array($result)) {
416
                        $result = array_shift($result);
417 7593
                    }
418 11
                }
419
                // if return_as_array for formula like '=sheet!cell'
420
                if (is_array($result) && count($result) === 1) {
421 7585
                    $resultKey = array_keys($result)[0];
422 7482
                    $resultValue = $result[$resultKey];
423 20
                    if (is_int($resultKey) && is_array($resultValue) && count($resultValue) === 1) {
424
                        $resultKey2 = array_keys($resultValue)[0];
425
                        $resultValue2 = $resultValue[$resultKey2];
426 7477
                        if (is_string($resultKey2) && !is_array($resultValue2) && preg_match('/[a-zA-Z]{1,3}/', $resultKey2) === 1) {
427
                            $result = $resultValue2;
428
                        }
429
                    }
430
                }
431
                $newColumn = $this->getColumn();
432
                if (is_array($result)) {
433
                    $this->formulaAttributes['t'] = 'array';
434 403
                    $this->formulaAttributes['ref'] = $maxCoordinate = $coordinate;
435
                    $newRow = $row = $this->getRow();
436 403
                    $column = $this->getColumn();
437 403
                    foreach ($result as $resultRow) {
438
                        if (is_array($resultRow)) {
439
                            $newColumn = $column;
440 403
                            foreach ($resultRow as $resultValue) {
441
                                if ($row !== $newRow || $column !== $newColumn) {
442
                                    $maxCoordinate = $newColumn . $newRow;
443
                                    if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) {
444
                                        if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) {
445
                                            $spill = true;
446
447
                                            break;
448
                                        }
449
                                    }
450
                                }
451 15
                                ++$newColumn;
452
                            }
453 15
                            ++$newRow;
454
                        } else {
455
                            if ($row !== $newRow || $column !== $newColumn) {
456
                                $maxCoordinate = $newColumn . $newRow;
457
                                if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) {
458
                                    if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) {
459 1119
                                        $spill = true;
460
                                    }
461 1119
                                }
462
                            }
463
                            ++$newColumn;
464
                        }
465
                        if ($spill) {
466
                            break;
467
                        }
468
                    }
469 2
                    if (!$spill) {
470
                        $this->formulaAttributes['ref'] .= ":$maxCoordinate";
471 2
                    }
472
                    $thisworksheet->getCell($column . $row);
473 2
                }
474
                if (is_array($result)) {
475
                    if ($oldAttributes !== null && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
476
                        if (($oldAttributesT) === 'array') {
477
                            $thisworksheet = $this->getWorksheet();
478
                            $coordinate = $this->getCoordinate();
479 67
                            $ref = $oldAttributesRef;
480
                            if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) {
481 67
                                if (isset($matches[3])) {
482
                                    $minCol = $matches[1];
483
                                    $minRow = (int) $matches[2];
484
                                    $maxCol = $matches[4];
485
                                    ++$maxCol;
486
                                    $maxRow = (int) $matches[5];
487
                                    for ($row = $minRow; $row <= $maxRow; ++$row) {
488
                                        for ($col = $minCol; $col !== $maxCol; ++$col) {
489 17
                                            if ("$col$row" !== $coordinate) {
490
                                                $thisworksheet->getCell("$col$row")->setValue(null);
491 17
                                            }
492 1
                                        }
493
                                    }
494
                                }
495 16
                            }
496
                            $thisworksheet->getCell($coordinate);
497
                        }
498
                    }
499
                }
500
                if ($spill) {
501
                    $result = ExcelError::SPILL();
502
                }
503 28
                if (is_array($result)) {
504
                    $newRow = $row = $this->getRow();
505 28
                    $newColumn = $column = $this->getColumn();
506 1
                    foreach ($result as $resultRow) {
507
                        if (is_array($resultRow)) {
508
                            $newColumn = $column;
509 27
                            foreach ($resultRow as $resultValue) {
510
                                if ($row !== $newRow || $column !== $newColumn) {
511
                                    $thisworksheet->getCell($newColumn . $newRow)->setValue($resultValue);
512
                                }
513
                                ++$newColumn;
514
                            }
515
                            ++$newRow;
516
                        } else {
517 4
                            if ($row !== $newRow || $column !== $newColumn) {
518
                                $thisworksheet->getCell($newColumn . $newRow)->setValue($resultRow);
519 4
                            }
520 1
                            ++$newColumn;
521
                        }
522
                    }
523 3
                    $thisworksheet->getCell($column . $row);
524
                    $this->value = $originalValue;
525 3
                    $this->dataType = $originalDataType;
526
                }
527
            } catch (SpreadsheetException $ex) {
528
                SharedDate::setExcelCalendar($currentCalendar);
529
                if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
530
                    return $this->calculatedValue; // Fallback for calculations referencing external files.
531 7
                } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
532
                    return ExcelError::NAME();
533 7
                }
534
535 7
                throw new CalculationException(
536
                    $title . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
537
                    $ex->getCode(),
538
                    $ex
539
                );
540
            }
541
            SharedDate::setExcelCalendar($currentCalendar);
542
543 1
            if ($result === Functions::NOT_YET_IMPLEMENTED) {
544
                return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
545 1
            }
546 1
547
            return $result;
548
        } elseif ($this->value instanceof RichText) {
549
            return $this->value->getPlainText();
550
        }
551
552
        return $this->convertDateTimeInt($this->value);
553
    }
554
555
    /**
556
     * Set old calculated value (cached).
557 82
     *
558
     * @param mixed $originalValue Value
559 82
     */
560 1
    public function setCalculatedValue(mixed $originalValue, bool $tryNumeric = true): self
561
    {
562
        if ($originalValue !== null) {
563 81
            $this->calculatedValue = ($tryNumeric && is_numeric($originalValue)) ? (0 + $originalValue) : $originalValue;
564
        }
565
566
        return $this->updateInCollection();
567
    }
568
569
    /**
570
     *    Get old calculated value (cached)
571 1
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
572
     *        create the original spreadsheet file.
573 1
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
574 1
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
575
     *        values used by the formula have changed since it was last calculated.
576
     */
577
    public function getOldCalculatedValue(): mixed
578
    {
579
        return $this->calculatedValue;
580
    }
581
582
    /**
583
     * Get cell data type.
584
     */
585 9710
    public function getDataType(): string
586
    {
587 9710
        return $this->dataType;
588
    }
589
590
    /**
591
     * Set cell data type.
592
     *
593
     * @param string $dataType see DataType::TYPE_*
594
     */
595 9722
    public function setDataType(string $dataType): self
596
    {
597 9722
        $this->setValueExplicit($this->value, $dataType);
598 9722
599 9722
        return $this;
600
    }
601 1
602
    /**
603
     * Identify if the cell contains a formula.
604 9722
     */
605 1
    public function isFormula(): bool
606
    {
607
        return $this->dataType === DataType::TYPE_FORMULA && $this->getStyle()->getQuotePrefix() === false;
608 9722
    }
609
610
    /**
611 9
     *    Does this cell contain Data validation rules?
612
     *
613 9
     * @throws SpreadsheetException
614 9
     */
615 9
    public function hasDataValidation(): bool
616
    {
617
        if (!isset($this->parent)) {
618
            throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet');
619
        }
620 9
621
        return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
622
    }
623
624
    /**
625
     * Get Data validation rules.
626 7
     *
627
     * @throws SpreadsheetException
628 7
     */
629
    public function getDataValidation(): DataValidation
630
    {
631
        if (!isset($this->parent)) {
632
            throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet');
633
        }
634 51
635
        return $this->getWorksheet()->getDataValidation($this->getCoordinate());
636 51
    }
637 21
638 21
    /**
639
     * Set Data validation rules.
640 21
     *
641
     * @throws SpreadsheetException
642
     */
643 32
    public function setDataValidation(?DataValidation $dataValidation = null): self
644
    {
645
        if (!isset($this->parent)) {
646
            throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet');
647
        }
648
649
        $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation);
650
651 54
        return $this->updateInCollection();
652
    }
653 54
654 21
    /**
655 21
     * Does this cell contain valid value?
656
     */
657
    public function hasValidValue(): bool
658
    {
659 35
        $validator = new DataValidator();
660
661
        return $validator->isValid($this);
662
    }
663
664
    /**
665 8542
     * Does this cell contain a Hyperlink?
666
     *
667 8542
     * @throws SpreadsheetException
668
     */
669
    public function hasHyperlink(): bool
670
    {
671
        if (!isset($this->parent)) {
672
            throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet');
673 6
        }
674
675 6
        return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
676 2
    }
677
678 4
    /**
679 4
     * Get Hyperlink.
680
     *
681
     * @throws SpreadsheetException
682
     */
683 4
    public function getHyperlink(): Hyperlink
684
    {
685 4
        if (!isset($this->parent)) {
686
            throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
687
        }
688
689
        return $this->getWorksheet()->getHyperlink($this->getCoordinate());
690
    }
691
692
    /**
693
     * Set Hyperlink.
694
     *
695
     * @throws SpreadsheetException
696
     */
697
    public function setHyperlink(?Hyperlink $hyperlink = null): self
698
    {
699
        if (!isset($this->parent)) {
700
            throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
701
        }
702
703 259
        $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
704
705 259
        return $this->updateInCollection();
706
    }
707
708 259
    /**
709 259
     * Get cell collection.
710
     */
711
    public function getParent(): ?Cells
712 259
    {
713 259
        return $this->parent;
714
    }
715
716
    /**
717
     * Get parent worksheet.
718
     *
719
     * @throws SpreadsheetException
720
     */
721
    public function getWorksheet(): Worksheet
722
    {
723
        $parent = $this->parent;
724
        if ($parent !== null) {
725
            $worksheet = $parent->getParent();
726
        } else {
727
            $worksheet = null;
728
        }
729
730
        if ($worksheet === null) {
731
            throw new SpreadsheetException('Worksheet no longer exists');
732
        }
733
734
        return $worksheet;
735
    }
736
737
    public function getWorksheetOrNull(): ?Worksheet
738
    {
739
        $parent = $this->parent;
740 9419
        if ($parent !== null) {
741
            $worksheet = $parent->getParent();
742 9419
        } else {
743 238
            $worksheet = null;
744
        }
745
746 9419
        return $worksheet;
747
    }
748
749
    /**
750
     * Is this cell in a merge range.
751
     */
752 156
    public function isInMergeRange(): bool
753
    {
754 156
        return (bool) $this->getMergeRange();
755
    }
756
757
    /**
758
     * Is this cell the master (top left cell) in a merge range (that holds the actual data value).
759
     */
760 11
    public function isMergeRangeValueCell(): bool
761
    {
762 11
        if ($mergeRange = $this->getMergeRange()) {
763 11
            $mergeRange = Coordinate::splitRange($mergeRange);
764 11
            [$startCell] = $mergeRange[0];
765 11
766
            return $this->getCoordinate() === $startCell;
767 11
        }
768
769
        return false;
770
    }
771
772
    /**
773
     * If this cell is in a merge range, then return the range.
774
     *
775 9752
     * @return false|string
776
     */
777 9752
    public function getMergeRange()
778
    {
779
        foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
780
            if ($this->isInRange($mergeRange)) {
781
                return $mergeRange;
782
            }
783 9750
        }
784
785 9750
        return false;
786
    }
787 9750
788
    /**
789
     * Get cell style.
790
     */
791
    public function getStyle(): Style
792
    {
793
        return $this->getWorksheet()->getStyle($this->getCoordinate());
794
    }
795 2
796
    /**
797 2
     * Get cell style.
798
     */
799 2
    public function getAppliedStyle(): Style
800
    {
801
        if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) {
802
            return $this->getStyle();
803
        }
804
        $range = $this->getWorksheet()->getConditionalRange($this->getCoordinate());
805 90
        if ($range === null) {
806
            return $this->getStyle();
807 90
        }
808
809
        $matcher = new CellStyleAssessor($this, $range);
810
811
        return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate()));
812
    }
813 2
814
    /**
815 2
     * Re-bind parent.
816
     */
817 2
    public function rebindParent(Worksheet $parent): self
818
    {
819
        $this->parent = $parent->getCellCollection();
820 323
821
        return $this->updateInCollection();
822 323
    }
823
824
    /**
825 3
     *    Is cell in a specific range?
826
     *
827 3
     * @param string $range Cell range (e.g. A1:A1)
828 3
     */
829 1
    public function isInRange(string $range): bool
830
    {
831 3
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
832
833 3
        // Translate properties
834
        $myColumn = Coordinate::columnIndexFromString($this->getColumn());
835
        $myRow = $this->getRow();
836 2
837
        // Verify if cell is in range
838 2
        return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn)
839 2
            && ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
840
    }
841 2
842 2
    /**
843 2
     * Compare 2 cells.
844
     *
845 2
     * @param Cell $a Cell a
846
     * @param Cell $b Cell b
847 2
     *
848
     * @return int Result of comparison (always -1 or 1, never zero!)
849
     */
850
    public static function compareCells(self $a, self $b): int
851
    {
852
        if ($a->getRow() < $b->getRow()) {
853
            return -1;
854
        } elseif ($a->getRow() > $b->getRow()) {
855
            return 1;
856
        } elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) {
857
            return -1;
858
        }
859
860
        return 1;
861
    }
862
863
    /**
864
     * Get value binder to use.
865
     */
866
    public static function getValueBinder(): IValueBinder
867
    {
868
        if (self::$valueBinder === null) {
869
            self::$valueBinder = new DefaultValueBinder();
870
        }
871
872
        return self::$valueBinder;
873
    }
874
875
    /**
876
     * Set value binder to use.
877
     */
878
    public static function setValueBinder(IValueBinder $binder): void
879
    {
880
        self::$valueBinder = $binder;
881
    }
882
883
    /**
884
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
885
     */
886
    public function __clone()
887
    {
888
        $vars = get_object_vars($this);
889
        foreach ($vars as $propertyName => $propertyValue) {
890
            if ((is_object($propertyValue)) && ($propertyName !== 'parent')) {
891
                $this->$propertyName = clone $propertyValue;
892
            } else {
893
                $this->$propertyName = $propertyValue;
894
            }
895
        }
896
    }
897
898
    /**
899
     * Get index to cellXf.
900
     */
901
    public function getXfIndex(): int
902
    {
903
        return $this->xfIndex;
904
    }
905
906
    /**
907
     * Set index to cellXf.
908
     */
909
    public function setXfIndex(int $indexValue): self
910
    {
911
        $this->xfIndex = $indexValue;
912
913
        return $this->updateInCollection();
914
    }
915
916
    /**
917
     * Set the formula attributes.
918
     *
919
     * @param $attributes null|array<string, string>
920
     *
921
     * @return $this
922
     */
923
    public function setFormulaAttributes(mixed $attributes): self
924
    {
925
        $this->formulaAttributes = $attributes;
926
927
        return $this;
928
    }
929
930
    /**
931
     * Get the formula attributes.
932
     *
933
     * @return null|array<string, string>
934
     */
935
    public function getFormulaAttributes(): mixed
936
    {
937
        return $this->formulaAttributes;
938
    }
939
940
    /**
941
     * Convert to string.
942
     */
943
    public function __toString(): string
944
    {
945
        $retVal = $this->value;
946
947
        return ($retVal === null || is_scalar($retVal) || $retVal instanceof Stringable) ? ((string) $retVal) : '';
948
    }
949
950
    public function getIgnoredErrors(): IgnoredErrors
951
    {
952
        return $this->ignoredErrors;
953
    }
954
955
    public function isLocked(): bool
956
    {
957
        $protected = $this->parent?->getParent()?->getProtection()?->getSheet();
958
        if ($protected !== true) {
959
            return false;
960
        }
961
        $locked = $this->getStyle()->getProtection()->getLocked();
962
963
        return $locked !== Protection::PROTECTION_UNPROTECTED;
964
    }
965
966
    public function isHiddenOnFormulaBar(): bool
967
    {
968
        if ($this->getDataType() !== DataType::TYPE_FORMULA) {
969
            return false;
970
        }
971
        $protected = $this->parent?->getParent()?->getProtection()?->getSheet();
972
        if ($protected !== true) {
973
            return false;
974
        }
975
        $hidden = $this->getStyle()->getProtection()->getHidden();
976
977
        return $hidden !== Protection::PROTECTION_UNPROTECTED;
978
    }
979
}
980