From ff61a854ace45314e0d5ec316e0726ec4c8ee42f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 3 Apr 2023 08:18:47 -0700 Subject: [PATCH 1/3] Add Ability to Ignore Cell Errors in Excel Fix #1141, which had been closed as stale, but which I have reopened. Excel will show cells with certain "errors" with a green triangle in the upper left. The suggestion in the issue to use quotePrefix to suppress the numberStoredAsText error is ineffective. In Excel, the user can turn this indicator off for individual cells. Cells where this is turned off can be detected at read time, and PhpSpreadsheet will now process those. In addition, the user can explicitly set the ignored error as in Excel. ```php $cell->setIgnoredErrorNumberStoredAsText(true); ``` There are a number of different errors that can be ignored in this fashion. This PR implements `numberStoredAsText` (which is likely to be by far the most useful one), `formula`, `twoDigitTextYear`, and `evalError`, all of which are demonstrated in the new test spreadsheet. There are several others for which I am not able to create good examples; I have not implemented those, but they can be easily added if needed (`calculatedColumn`, `emptyCellReference`, `formulaRange`, `listDataValidation`, and `unlockedFormula`). --- src/PhpSpreadsheet/Cell/Cell.php | 60 ++++++++++++++++++ src/PhpSpreadsheet/Reader/Xlsx.php | 49 ++++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 56 +++++++++++++++- .../Reader/Xlsx/IgnoredErrorTest.php | 58 +++++++++++++++++ tests/data/Reader/XLSX/ignoreerror.xlsx | Bin 0 -> 12243 bytes 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php create mode 100644 tests/data/Reader/XLSX/ignoreerror.xlsx diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 7fa6b12123..e16458614d 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -71,6 +71,18 @@ class Cell */ private $formulaAttributes; + /** @var bool */ + private $ignoredErrorNumberStoredAsText = false; + + /** @var bool */ + private $ignoredErrorFormula = false; + + /** @var bool */ + private $ignoredErrorTwoDigitTextYear = false; + + /** @var bool */ + private $ignoredErrorEvalError = false; + /** * Update the cell into the cell collection. * @@ -796,4 +808,52 @@ public function __toString() { return (string) $this->getValue(); } + + public function setIgnoredErrorNumberStoredAsText(bool $value): self + { + $this->ignoredErrorNumberStoredAsText = $value; + + return $this; + } + + public function getIgnoredErrorNumberStoredAsText(): bool + { + return $this->ignoredErrorNumberStoredAsText; + } + + public function setIgnoredErrorFormula(bool $value): self + { + $this->ignoredErrorFormula = $value; + + return $this; + } + + public function getIgnoredErrorFormula(): bool + { + return $this->ignoredErrorFormula; + } + + public function setIgnoredErrorTwoDigitTextYear(bool $value): self + { + $this->ignoredErrorTwoDigitTextYear = $value; + + return $this; + } + + public function getIgnoredErrorTwoDigitTextYear(): bool + { + return $this->ignoredErrorTwoDigitTextYear; + } + + public function setIgnoredErrorEvalError(bool $value): self + { + $this->ignoredErrorEvalError = $value; + + return $this; + } + + public function getIgnoredErrorEvalError(): bool + { + return $this->ignoredErrorEvalError; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 50fe8181a1..52aad763e6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -915,6 +915,11 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet ++$cIndex; } } + if ($xmlSheetNS && $xmlSheetNS->ignoredErrors) { + foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) { + $this->processIgnoredErrors($ignoredError, $docSheet); + } + } if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) { $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? []; @@ -2222,4 +2227,48 @@ private static function extractPalette(?SimpleXMLElement $sxml): array return $array; } + + private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void + { + $attributes = self::getAttributes($xml); + $sqref = (string) ($attributes['sqref'] ?? ''); + $numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? ''); + $formula = (string) ($attributes['formula'] ?? ''); + $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? ''); + $evalError = (string) ($attributes['evalError'] ?? ''); + if (!empty($sqref)) { + $explodedSqref = explode(' ', $sqref); + $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/'; + foreach ($explodedSqref as $sqref1) { + if (preg_match($pattern1, $sqref1, $matches) === 1) { + $firstRow = $matches[2]; + $firstCol = $matches[1]; + if (array_key_exists(3, $matches)) { + $lastCol = $matches[4]; + $lastRow = $matches[5]; + } else { + $lastCol = $firstCol; + $lastRow = $firstRow; + } + ++$lastCol; + for ($row = $firstRow; $row <= $lastRow; ++$row) { + for ($col = $firstCol; $col !== $lastCol; ++$col) { + if ($numberStoredAsText === '1') { + $sheet->getCell("$col$row")->setIgnoredErrorNumberStoredAsText(true); + } + if ($formula === '1') { + $sheet->getCell("$col$row")->setIgnoredErrorFormula(true); + } + if ($twoDigitTextYear === '1') { + $sheet->getCell("$col$row")->setIgnoredErrorTwoDigitTextYear(true); + } + if ($evalError === '1') { + $sheet->getCell("$col$row")->setIgnoredErrorEvalError(true); + } + } + } + } + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 53c4512457..b38abd3b44 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -18,6 +18,18 @@ class Worksheet extends WriterPart { + /** @var string */ + private $numberStoredAsText = ''; + + /** @var string */ + private $formula = ''; + + /** @var string */ + private $twoDigitTextYear = ''; + + /** @var string */ + private $evalError = ''; + /** * Write worksheet to XML format. * @@ -118,6 +130,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable // AlternateContent $this->writeAlternateContent($objWriter, $worksheet); + // IgnoredErrors + $this->writeIgnoredErrors($objWriter); + // Table $this->writeTable($objWriter, $worksheet); @@ -131,6 +146,32 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable return $objWriter->getData(); } + private function writeIgnoredError(XMLWriter $objWriter, bool &$started, string $attr, string $cells): void + { + if ($cells !== '') { + if (!$started) { + $objWriter->startElement('ignoredErrors'); + $started = true; + } + $objWriter->startElement('ignoredError'); + $objWriter->writeAttribute('sqref', substr($cells, 1)); + $objWriter->writeAttribute($attr, '1'); + $objWriter->endElement(); + } + } + + private function writeIgnoredErrors(XMLWriter $objWriter): void + { + $started = false; + $this->writeIgnoredError($objWriter, $started, 'numberStoredAsText', $this->numberStoredAsText); + $this->writeIgnoredError($objWriter, $started, 'formula', $this->formula); + $this->writeIgnoredError($objWriter, $started, 'twoDigitTextYear', $this->twoDigitTextYear); + $this->writeIgnoredError($objWriter, $started, 'evalError', $this->evalError); + if ($started) { + $objWriter->endElement(); + } + } + /** * Write SheetPr. */ @@ -1134,7 +1175,20 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w array_pop($columnsInRow); foreach ($columnsInRow as $column) { // Write cell - $this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable); + $coord = "$column$currentRow"; + if ($worksheet->getCell($coord)->getIgnoredErrorNumberStoredAsText()) { + $this->numberStoredAsText .= " $coord"; + } + if ($worksheet->getCell($coord)->getIgnoredErrorFormula()) { + $this->formula .= " $coord"; + } + if ($worksheet->getCell($coord)->getIgnoredErrorTwoDigitTextYear()) { + $this->twoDigitTextYear .= " $coord"; + } + if ($worksheet->getCell($coord)->getIgnoredErrorEvalError()) { + $this->evalError .= " $coord"; + } + $this->writeCell($objWriter, $worksheet, $coord, $aFlippedStringTable); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php new file mode 100644 index 0000000000..ad116f8f3a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php @@ -0,0 +1,58 @@ +load(self::FILENAME); + $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); + $originalSpreadsheet->disconnectWorksheets(); + $sheet = $spreadsheet->getActiveSheet(); + self::assertFalse($sheet->getCell('A1')->getIgnoredErrorNumberStoredAsText()); + self::assertTrue($sheet->getCell('A2')->getIgnoredErrorNumberStoredAsText()); + self::assertFalse($sheet->getCell('H2')->getIgnoredErrorNumberStoredAsText()); + self::assertTrue($sheet->getCell('H3')->getIgnoredErrorNumberStoredAsText()); + self::assertFalse($sheet->getCell('I2')->getIgnoredErrorNumberStoredAsText()); + self::assertTrue($sheet->getCell('I3')->getIgnoredErrorNumberStoredAsText()); + + self::assertFalse($sheet->getCell('H3')->getIgnoredErrorFormula()); + self::assertFalse($sheet->getCell('D2')->getIgnoredErrorFormula()); + self::assertTrue($sheet->getCell('D3')->getIgnoredErrorFormula()); + + self::assertFalse($sheet->getCell('A11')->getIgnoredErrorTwoDigitTextYear()); + self::assertTrue($sheet->getCell('A12')->getIgnoredErrorTwoDigitTextYear()); + + self::assertFalse($sheet->getCell('C12')->getIgnoredErrorEvalError()); + self::assertTrue($sheet->getCell('C11')->getIgnoredErrorEvalError()); + + $spreadsheet->disconnectWorksheets(); + } + + public function testSetIgnoredError(): void + { + $originalSpreadsheet = new Spreadsheet(); + $originalSheet = $originalSpreadsheet->getActiveSheet(); + $originalSheet->getCell('A1')->setValueExplicit('0', DataType::TYPE_STRING); + $originalSheet->getCell('A2')->setValueExplicit('1', DataType::TYPE_STRING); + $originalSheet->getStyle('A1:A2')->setQuotePrefix(true); + $originalSheet->getCell('A2')->setIgnoredErrorNumberStoredAsText(true); + $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); + $originalSpreadsheet->disconnectWorksheets(); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('0', $sheet->getCell('A1')->getValue()); + self::assertSame('1', $sheet->getCell('A2')->getValue()); + self::assertFalse($sheet->getCell('A1')->getIgnoredErrorNumberStoredAsText()); + self::assertTrue($sheet->getCell('A2')->getIgnoredErrorNumberStoredAsText()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/ignoreerror.xlsx b/tests/data/Reader/XLSX/ignoreerror.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b29d9b5d76a323c70e7c49d7a87cef5f7e3878b4 GIT binary patch literal 12243 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 literal 0 HcmV?d00001 From c3f2a0fe92c29f756e257e684d9b8b5bddf03f35 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 3 Apr 2023 08:46:17 -0700 Subject: [PATCH 2/3] Scrutinizer A new change, a new Scrutinizer false positive. --- src/PhpSpreadsheet/Reader/Xlsx.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 52aad763e6..bbb1ea97fe 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -916,7 +916,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } } if ($xmlSheetNS && $xmlSheetNS->ignoredErrors) { - foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) { + foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredErrorx) { + $ignoredError = self::testSimpleXml($ignoredErrorx); $this->processIgnoredErrors($ignoredError, $docSheet); } } From 8ceee534a8994feeb23191113312def6e32d8178 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 4 Apr 2023 20:39:07 -0700 Subject: [PATCH 3/3] Move Ignored Errors to Own Class In response to comments from @MarkBaker, implement ignoredError as a new class. This simplifies Cell by requiring only 1 new method, rather than 8+. This requires a slightly more complicated syntax. ```php $cell->getIgnoredErrors()->setNumberScoredAsText(true); ``` Mark had also suggested that there might be a pre-existing regexp for processing the cells/cellranges when reading the sqref attribute. Those in Calculation are too complicated (read "non-performant") for this piece of code; the one in Coordinates is slightly less complicated than Calculation, but still more complicated than the one I'm using, and doesn't handle ranges. --- src/PhpSpreadsheet/Cell/Cell.php | 61 ++-------------- src/PhpSpreadsheet/Cell/IgnoredErrors.php | 66 ++++++++++++++++++ src/PhpSpreadsheet/Reader/Xlsx.php | 8 +-- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 12 ++-- .../Reader/Xlsx/IgnoredErrorTest.php | 42 ++++++----- tests/data/Reader/XLSX/ignoreerror.xlsx | Bin 12243 -> 12728 bytes 6 files changed, 109 insertions(+), 80 deletions(-) create mode 100644 src/PhpSpreadsheet/Cell/IgnoredErrors.php diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index e16458614d..80314e3e5e 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -71,17 +71,8 @@ class Cell */ private $formulaAttributes; - /** @var bool */ - private $ignoredErrorNumberStoredAsText = false; - - /** @var bool */ - private $ignoredErrorFormula = false; - - /** @var bool */ - private $ignoredErrorTwoDigitTextYear = false; - - /** @var bool */ - private $ignoredErrorEvalError = false; + /** @var IgnoredErrors */ + private $ignoredErrors; /** * Update the cell into the cell collection. @@ -131,6 +122,7 @@ public function __construct($value, ?string $dataType, Worksheet $worksheet) } elseif (self::getValueBinder()->bindValue($this, $value) === false) { throw new Exception('Value could not be bound to cell.'); } + $this->ignoredErrors = new IgnoredErrors(); } /** @@ -809,51 +801,8 @@ public function __toString() return (string) $this->getValue(); } - public function setIgnoredErrorNumberStoredAsText(bool $value): self - { - $this->ignoredErrorNumberStoredAsText = $value; - - return $this; - } - - public function getIgnoredErrorNumberStoredAsText(): bool - { - return $this->ignoredErrorNumberStoredAsText; - } - - public function setIgnoredErrorFormula(bool $value): self - { - $this->ignoredErrorFormula = $value; - - return $this; - } - - public function getIgnoredErrorFormula(): bool - { - return $this->ignoredErrorFormula; - } - - public function setIgnoredErrorTwoDigitTextYear(bool $value): self - { - $this->ignoredErrorTwoDigitTextYear = $value; - - return $this; - } - - public function getIgnoredErrorTwoDigitTextYear(): bool - { - return $this->ignoredErrorTwoDigitTextYear; - } - - public function setIgnoredErrorEvalError(bool $value): self - { - $this->ignoredErrorEvalError = $value; - - return $this; - } - - public function getIgnoredErrorEvalError(): bool + public function getIgnoredErrors(): IgnoredErrors { - return $this->ignoredErrorEvalError; + return $this->ignoredErrors; } } diff --git a/src/PhpSpreadsheet/Cell/IgnoredErrors.php b/src/PhpSpreadsheet/Cell/IgnoredErrors.php new file mode 100644 index 0000000000..ee4b515620 --- /dev/null +++ b/src/PhpSpreadsheet/Cell/IgnoredErrors.php @@ -0,0 +1,66 @@ +numberStoredAsText = $value; + + return $this; + } + + public function getNumberStoredAsText(): bool + { + return $this->numberStoredAsText; + } + + public function setFormula(bool $value): self + { + $this->formula = $value; + + return $this; + } + + public function getFormula(): bool + { + return $this->formula; + } + + public function setTwoDigitTextYear(bool $value): self + { + $this->twoDigitTextYear = $value; + + return $this; + } + + public function getTwoDigitTextYear(): bool + { + return $this->twoDigitTextYear; + } + + public function setEvalError(bool $value): self + { + $this->evalError = $value; + + return $this; + } + + public function getEvalError(): bool + { + return $this->evalError; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index bbb1ea97fe..7e72090fc8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -2255,16 +2255,16 @@ private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): for ($row = $firstRow; $row <= $lastRow; ++$row) { for ($col = $firstCol; $col !== $lastCol; ++$col) { if ($numberStoredAsText === '1') { - $sheet->getCell("$col$row")->setIgnoredErrorNumberStoredAsText(true); + $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true); } if ($formula === '1') { - $sheet->getCell("$col$row")->setIgnoredErrorFormula(true); + $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true); } if ($twoDigitTextYear === '1') { - $sheet->getCell("$col$row")->setIgnoredErrorTwoDigitTextYear(true); + $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true); } if ($evalError === '1') { - $sheet->getCell("$col$row")->setIgnoredErrorEvalError(true); + $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true); } } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index b38abd3b44..5e453b3d86 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -40,6 +40,10 @@ class Worksheet extends WriterPart */ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false) { + $this->numberStoredAsText = ''; + $this->formula = ''; + $this->twoDigitTextYear = ''; + $this->evalError = ''; // Create XML writer $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { @@ -1176,16 +1180,16 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w foreach ($columnsInRow as $column) { // Write cell $coord = "$column$currentRow"; - if ($worksheet->getCell($coord)->getIgnoredErrorNumberStoredAsText()) { + if ($worksheet->getCell($coord)->getIgnoredErrors()->getNumberStoredAsText()) { $this->numberStoredAsText .= " $coord"; } - if ($worksheet->getCell($coord)->getIgnoredErrorFormula()) { + if ($worksheet->getCell($coord)->getIgnoredErrors()->getFormula()) { $this->formula .= " $coord"; } - if ($worksheet->getCell($coord)->getIgnoredErrorTwoDigitTextYear()) { + if ($worksheet->getCell($coord)->getIgnoredErrors()->getTwoDigitTextYear()) { $this->twoDigitTextYear .= " $coord"; } - if ($worksheet->getCell($coord)->getIgnoredErrorEvalError()) { + if ($worksheet->getCell($coord)->getIgnoredErrors()->getEvalError()) { $this->evalError .= " $coord"; } $this->writeCell($objWriter, $worksheet, $coord, $aFlippedStringTable); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php index ad116f8f3a..bb476d6d69 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php @@ -18,22 +18,32 @@ public function testIgnoredError(): void $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); $originalSpreadsheet->disconnectWorksheets(); $sheet = $spreadsheet->getActiveSheet(); - self::assertFalse($sheet->getCell('A1')->getIgnoredErrorNumberStoredAsText()); - self::assertTrue($sheet->getCell('A2')->getIgnoredErrorNumberStoredAsText()); - self::assertFalse($sheet->getCell('H2')->getIgnoredErrorNumberStoredAsText()); - self::assertTrue($sheet->getCell('H3')->getIgnoredErrorNumberStoredAsText()); - self::assertFalse($sheet->getCell('I2')->getIgnoredErrorNumberStoredAsText()); - self::assertTrue($sheet->getCell('I3')->getIgnoredErrorNumberStoredAsText()); + self::assertFalse($sheet->getCell('A1')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertTrue($sheet->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertFalse($sheet->getCell('A3')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertTrue($sheet->getCell('A4')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertFalse($sheet->getCell('H2')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertTrue($sheet->getCell('H3')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertFalse($sheet->getCell('I2')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertTrue($sheet->getCell('I3')->getIgnoredErrors()->getNumberStoredAsText()); - self::assertFalse($sheet->getCell('H3')->getIgnoredErrorFormula()); - self::assertFalse($sheet->getCell('D2')->getIgnoredErrorFormula()); - self::assertTrue($sheet->getCell('D3')->getIgnoredErrorFormula()); + self::assertFalse($sheet->getCell('H3')->getIgnoredErrors()->getFormula()); + self::assertFalse($sheet->getCell('D2')->getIgnoredErrors()->getFormula()); + self::assertTrue($sheet->getCell('D3')->getIgnoredErrors()->getFormula()); - self::assertFalse($sheet->getCell('A11')->getIgnoredErrorTwoDigitTextYear()); - self::assertTrue($sheet->getCell('A12')->getIgnoredErrorTwoDigitTextYear()); + self::assertFalse($sheet->getCell('A11')->getIgnoredErrors()->getTwoDigitTextYear()); + self::assertTrue($sheet->getCell('A12')->getIgnoredErrors()->getTwoDigitTextYear()); - self::assertFalse($sheet->getCell('C12')->getIgnoredErrorEvalError()); - self::assertTrue($sheet->getCell('C11')->getIgnoredErrorEvalError()); + self::assertFalse($sheet->getCell('C12')->getIgnoredErrors()->getEvalError()); + self::assertTrue($sheet->getCell('C11')->getIgnoredErrors()->getEvalError()); + + $sheetLast = $spreadsheet->getSheetByNameOrThrow('Last'); + self::assertFalse($sheetLast->getCell('D2')->getIgnoredErrors()->getFormula()); + self::assertFalse($sheetLast->getCell('D3')->getIgnoredErrors()->getFormula(), 'prior sheet ignoredErrors shouldn\'t bleed'); + self::assertFalse($sheetLast->getCell('A1')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertFalse($sheetLast->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertTrue($sheetLast->getCell('A3')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertFalse($sheetLast->getCell('A4')->getIgnoredErrors()->getNumberStoredAsText(), 'prior sheet numberStoredAsText shouldn\'t bleed'); $spreadsheet->disconnectWorksheets(); } @@ -45,14 +55,14 @@ public function testSetIgnoredError(): void $originalSheet->getCell('A1')->setValueExplicit('0', DataType::TYPE_STRING); $originalSheet->getCell('A2')->setValueExplicit('1', DataType::TYPE_STRING); $originalSheet->getStyle('A1:A2')->setQuotePrefix(true); - $originalSheet->getCell('A2')->setIgnoredErrorNumberStoredAsText(true); + $originalSheet->getCell('A2')->getIgnoredErrors()->setNumberStoredAsText(true); $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); $originalSpreadsheet->disconnectWorksheets(); $sheet = $spreadsheet->getActiveSheet(); self::assertSame('0', $sheet->getCell('A1')->getValue()); self::assertSame('1', $sheet->getCell('A2')->getValue()); - self::assertFalse($sheet->getCell('A1')->getIgnoredErrorNumberStoredAsText()); - self::assertTrue($sheet->getCell('A2')->getIgnoredErrorNumberStoredAsText()); + self::assertFalse($sheet->getCell('A1')->getIgnoredErrors()->getNumberStoredAsText()); + self::assertTrue($sheet->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText()); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/data/Reader/XLSX/ignoreerror.xlsx b/tests/data/Reader/XLSX/ignoreerror.xlsx index b29d9b5d76a323c70e7c49d7a87cef5f7e3878b4..2034bfc93287173f2a6f9a23d2da7841dac226a5 100644 GIT binary patch delta 3178 zcmZ9Oc{CJW8^_0(Nk+!bq_NHngREI=>`PQcG?=oLe%8q@6^0>OLm1RFXfb5Vl6^PX zvS-O&lr5nlWeu->=RK$Qy!VfL?s@J#pFh5z^PKw(IaJ>)**gXfePLuAm&_te$WrG# z2pv3NX}uF?8ivgEZ!4(0QkMT_eA5h<=i3Y_2=i-K&dc9toRbhUr4C7rHlRyq*Vncw z6qla)MZBHcWvgq^3J!*IN4E|TY^|OQODev{_ zx!)GdVL88kUFoWDd5XslJ}nm@1e=~3ld8^*jm);8tm4d*t5Df*iY~Eo|pXy-onXv$Zo3@ zJ4a#Uo-Xz~Q+Eis zq|j=LA31fZ{fyW3UoN@Hw}LCW6(6wiO?2_vi}`XmSG7?xJ@!(yejT4e`K5bPtDSITgYZSrgi0c$Ne-`=b zAik9FJ4h~@V1XFHQ$xzFQ4xvYz_|(CpFBjaqQ^dioxTn@@G4F z!&*?@^-yv6_ifl_XnxoSRA9%~oDjZnocrYck-6a6k5M3v3R&;* z@kaAX>2O7(h9u`?%>l42(eQzzRA%D=cXlNiXX#9{NETEjiap8UX@MjOwO6W`-87Dt ze#b>W_yKykUr11Cn6Z?=u|6)ZwZ7W^`3869S-tCvHcaqy{kv>iGi)Simt zKoTQYQnjShI8}E#H&1?tSKly|KwGouh4C6boiPsR}4F)L#i z(tWrkUt}3P|0MP4(5GxOW0qZBrtJ+6{FzVfl_9|xXO4e{=Ghh|vuBvZ80id{ zDW)9PFMa#If?d+=d>^dC;tQ?V`RaBX3!@@-T-!8u<@w^(x-E~(@k^iXDe#|bY5eUa zI7RJ{X6(H>>|H-);4p9|W2o+oP*!nS+sdVBs`I+{v&?09&s%l%Y3(Qd5l#G^&sw+^ zv?ctly;U}ul~h>DsE`d1BnB+{`UDsNkm4e9vY#X<$pzs()03|~qAGAcn3nmaZ4=7E z9RBe%#1=np(0lE}WZlgu$2I}?b81)j8A371c2e>K7i5${wlV5b!!#4KJHV^cM zz6hr;+^$!bA z87_?#hpm|~0qeonhjj{Weejie?{Kf&m74@ac3Wy{16@KSQuDe~i%*TOBsK29PD?JQ zQ@sBrQa%n{EVAs1TkJ-MbV@v&>lVNG?Tbi8zhJByrdk|25!XnYm+nPV``ZEgY2IrL zckb<77BnZ5IWpV%-QvW=>ig=quR%$np0LF0pqgmGlzi9tj|cr|A_`Jc@OE$arDP?X zXTLY;7_QOsbDd@|xMm61wcrzO7M+{YyTn)DOnT{$kxJYXDYN3b&kVV>xkUWU3;f87mrCr}l5kIjT+-({K8gdL|bJs?R?p7#~yPka~hJB|&0QR!#-;SM7lO-Xta zYXo#E*-qd%plCl!IRSOdP^c9*;Uxo<)#Bez#{J&4jXuMz-tXT+8!%5gqyD2lXje&F zu+C2?jKlnWk$A>dh)Le85PCs;D59N07_n)?e43CAXaJ9pRQGH?J5Ed*<`zP zPIU*V+Thf(_-5W-nQx~~dSpl1&g`vQhNKgg=3_J0rquJTc*s9ZZaKK};5`3^(F~Ns z4$MChSC5>(XY}=k*1}>{Sk|5bG)nz8X`G$M%SOV89uCq1AsmA=njtiU+-8HrnkQ|5 zLF%?o@P}o^=4t$Uo$k%SLxMtwmuO{1M-%FyucxQ6(RjK8-G_usOOVj0J!Ap^4i5o; zwv^P#+Yt&Uq8z~Um4j#(ehV7vv#$BY44%!1j+_5n^i~}wD zZh||>{#q)0rh^Z>gmX;Sk!0C+0`K77+vV3)S7i%zv3F$J$KEHq z^aUyqo{uG*nk0DcOpgMj%Pff5ie3IFvJf`1C`{*8ql|O;KjZ|dJW-n>aG|Q-M4Xpy zqsvMS7ztG|I@x?Z&;~w5)R`Af?CJF!6bPf2Gdow*Fc;p{Q70EayNt=}Pg5U;-2$+U zJNW=W;nEn#G0&0mahg0J!j0vfI~xnEa`M|u+I_%90-G`1oum|&y;7nMAnK$FCthm% zL!2Y&=q{wBK@~K9APP4*v1y4mN5JDUM?g_fcs@q#JSqoy%HMSIys7W%gHzonI49bq9l&LBm7J^*-P8C;TT?cj-2V3xkg`GJnqo&EJN_ zYt>CW$@AFJ>W0>DfS;e1R)qvjGkbtAK#vf~W}eL%k^Y%jq6-AdTW>zof#bW%s-O)y zi+Sb4S~%-GF8L2r(vt#pM=J-UrjBzd6_$vUs(C3V z-*>C~@GW?atFK06aVoL*&Krmp-NvE-jD;YUnsf`|h20Iq*VlGJ ziot8teR)RScbyu$cws1Jjc)8Pn~S-8o6!Z99#qeWTvsLyDVcV*@x{6nuWo2BOg`U# zfQj7Z@R{HypEM~t-->wtZmn|>-B@NXVgKJ@olHM{g^5Pyl|9e2MZPX81OGcP006L~ z^#3jb2RWTXklY~4`M*EYs4Nz^0VB)HX#;&l$=-4rKrDh>ASVI;|4#V}vPF_t<;3BC ltpWgCe{YXYJt(q_ye9Ahn(Qu*0dmQZbLCOT1mym_`40~#)&u|m delta 2663 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