Failed Conditions
Pull Request — master (#3962)
by Owen
17:41 queued 07:20
created

Cell::getCalculatedValue()   F

Complexity

Conditions 52
Paths > 20000

Size

Total Lines 167
Code Lines 120

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 112
CRAP Score 52.5497

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 120
dl 0
loc 167
ccs 112
cts 119
cp 0.9412
rs 0
c 1
b 0
f 0
cc 52
nc 237100
nop 1
crap 52.5497

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