Cell   F
last analyzed

Complexity

Total Complexity 180

Size/Duplication

Total Lines 984
Duplicated Lines 0 %

Test Coverage

Coverage 94.12%

Importance

Changes 0
Metric Value
wmc 180
eloc 377
c 0
b 0
f 0
dl 0
loc 984
ccs 384
cts 408
cp 0.9412
rs 2

52 Methods

Rating   Name   Duplication   Size   Complexity  
A detach() 0 3 1
A attach() 0 3 1
A getValue() 0 3 1
A getCoordinate() 0 13 3
A getRow() 0 8 2
A getColumn() 0 8 2
A getFormattedValue() 0 11 1
A updateInCollection() 0 9 2
A convertDateTimeInt() 0 15 6
A setCalculateDateTimeType() 0 5 1
A getCalculateDateTimeType() 0 3 1
A __construct() 0 21 4
A getValueString() 0 3 1
A updateIfCellIsTableHeader() 0 17 6
A setValue() 0 9 2
A getCalculatedValueString() 0 8 2
A getDataType() 0 3 1
A getStyle() 0 3 1
A setDataValidation() 0 9 2
A getOldCalculatedValue() 0 3 1
A compareCells() 0 11 4
A rebindParent() 0 5 1
A getAppliedStyle() 0 13 3
A __clone() 0 8 4
A getDataValidation() 0 7 2
A setValueBinder() 0 3 1
A getParent() 0 3 1
A __toString() 0 5 1
A isInRange() 0 11 4
A isFormula() 0 3 2
A isHiddenOnFormulaBar() 0 12 3
A setFormulaAttributes() 0 5 1
A isInMergeRange() 0 3 1
A getWorksheetOrNull() 0 10 2
A isLocked() 0 9 2
F setValueExplicit() 0 84 23
A getValueBinder() 0 7 2
A getHyperlink() 0 7 2
A getWorksheet() 0 14 3
A setHyperlink() 0 9 2
A getXfIndex() 0 3 1
F getCalculatedValue() 0 179 56
A isMergeRangeValueCell() 0 10 2
A getIgnoredErrors() 0 3 1
A hasValidValue() 0 5 1
A hasHyperlink() 0 7 2
A getFormulaAttributes() 0 3 1
A hasDataValidation() 0 7 2
A setXfIndex() 0 5 1
A setDataType() 0 5 1
A setCalculatedValue() 0 7 4
A getMergeRange() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Cell often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cell, and based on these observations, apply Extract Interface, too.

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