From aab16147e4f49cee1507677d77efa6333f0db022 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 8 Apr 2023 21:58:07 -0700 Subject: [PATCH] Add Ability to Ignore Cell Errors in Excel (#3508) * 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`). * Scrutinizer A new change, a new Scrutinizer false positive. * 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 | 9 +++ src/PhpSpreadsheet/Cell/IgnoredErrors.php | 66 +++++++++++++++++ src/PhpSpreadsheet/Reader/Xlsx.php | 50 +++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 60 +++++++++++++++- .../Reader/Xlsx/IgnoredErrorTest.php | 68 ++++++++++++++++++ tests/data/Reader/XLSX/ignoreerror.xlsx | Bin 0 -> 12728 bytes 6 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/PhpSpreadsheet/Cell/IgnoredErrors.php 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 e721673be9..e9e41d7c7b 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -71,6 +71,9 @@ class Cell */ private $formulaAttributes; + /** @var IgnoredErrors */ + private $ignoredErrors; + /** * Update the cell into the cell collection. * @@ -119,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(); } /** @@ -796,4 +800,9 @@ public function __toString() { return (string) $this->getValue(); } + + public function getIgnoredErrors(): IgnoredErrors + { + 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 3a76b05b51..310fef74f8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -956,6 +956,12 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet ++$cIndex; } } + if ($xmlSheetNS && $xmlSheetNS->ignoredErrors) { + foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredErrorx) { + $ignoredError = self::testSimpleXml($ignoredErrorx); + $this->processIgnoredErrors($ignoredError, $docSheet); + } + } if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) { $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? []; @@ -2263,4 +2269,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")->getIgnoredErrors()->setNumberStoredAsText(true); + } + if ($formula === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true); + } + if ($twoDigitTextYear === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true); + } + if ($evalError === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true); + } + } + } + } + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 53c4512457..5e453b3d86 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. * @@ -28,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()) { @@ -118,6 +134,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable // AlternateContent $this->writeAlternateContent($objWriter, $worksheet); + // IgnoredErrors + $this->writeIgnoredErrors($objWriter); + // Table $this->writeTable($objWriter, $worksheet); @@ -131,6 +150,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 +1179,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)->getIgnoredErrors()->getNumberStoredAsText()) { + $this->numberStoredAsText .= " $coord"; + } + if ($worksheet->getCell($coord)->getIgnoredErrors()->getFormula()) { + $this->formula .= " $coord"; + } + if ($worksheet->getCell($coord)->getIgnoredErrors()->getTwoDigitTextYear()) { + $this->twoDigitTextYear .= " $coord"; + } + 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 new file mode 100644 index 0000000000..bb476d6d69 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php @@ -0,0 +1,68 @@ +load(self::FILENAME); + $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); + $originalSpreadsheet->disconnectWorksheets(); + $sheet = $spreadsheet->getActiveSheet(); + 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')->getIgnoredErrors()->getFormula()); + self::assertFalse($sheet->getCell('D2')->getIgnoredErrors()->getFormula()); + self::assertTrue($sheet->getCell('D3')->getIgnoredErrors()->getFormula()); + + self::assertFalse($sheet->getCell('A11')->getIgnoredErrors()->getTwoDigitTextYear()); + self::assertTrue($sheet->getCell('A12')->getIgnoredErrors()->getTwoDigitTextYear()); + + 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(); + } + + 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')->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')->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 new file mode 100644 index 0000000000000000000000000000000000000000..2034bfc93287173f2a6f9a23d2da7841dac226a5 GIT binary patch literal 12728 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 literal 0 HcmV?d00001