Skip to content

Commit aab1614

Browse files
authored
Add Ability to Ignore Cell Errors in Excel (PHPOffice#3508)
* Add Ability to Ignore Cell Errors in Excel Fix PHPOffice#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.
1 parent 5ef48e9 commit aab1614

File tree

6 files changed

+252
-1
lines changed

6 files changed

+252
-1
lines changed

src/PhpSpreadsheet/Cell/Cell.php

+9
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ class Cell
7171
*/
7272
private $formulaAttributes;
7373

74+
/** @var IgnoredErrors */
75+
private $ignoredErrors;
76+
7477
/**
7578
* Update the cell into the cell collection.
7679
*
@@ -119,6 +122,7 @@ public function __construct($value, ?string $dataType, Worksheet $worksheet)
119122
} elseif (self::getValueBinder()->bindValue($this, $value) === false) {
120123
throw new Exception('Value could not be bound to cell.');
121124
}
125+
$this->ignoredErrors = new IgnoredErrors();
122126
}
123127

124128
/**
@@ -796,4 +800,9 @@ public function __toString()
796800
{
797801
return (string) $this->getValue();
798802
}
803+
804+
public function getIgnoredErrors(): IgnoredErrors
805+
{
806+
return $this->ignoredErrors;
807+
}
799808
}
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Cell;
4+
5+
class IgnoredErrors
6+
{
7+
/** @var bool */
8+
private $numberStoredAsText = false;
9+
10+
/** @var bool */
11+
private $formula = false;
12+
13+
/** @var bool */
14+
private $twoDigitTextYear = false;
15+
16+
/** @var bool */
17+
private $evalError = false;
18+
19+
public function setNumberStoredAsText(bool $value): self
20+
{
21+
$this->numberStoredAsText = $value;
22+
23+
return $this;
24+
}
25+
26+
public function getNumberStoredAsText(): bool
27+
{
28+
return $this->numberStoredAsText;
29+
}
30+
31+
public function setFormula(bool $value): self
32+
{
33+
$this->formula = $value;
34+
35+
return $this;
36+
}
37+
38+
public function getFormula(): bool
39+
{
40+
return $this->formula;
41+
}
42+
43+
public function setTwoDigitTextYear(bool $value): self
44+
{
45+
$this->twoDigitTextYear = $value;
46+
47+
return $this;
48+
}
49+
50+
public function getTwoDigitTextYear(): bool
51+
{
52+
return $this->twoDigitTextYear;
53+
}
54+
55+
public function setEvalError(bool $value): self
56+
{
57+
$this->evalError = $value;
58+
59+
return $this;
60+
}
61+
62+
public function getEvalError(): bool
63+
{
64+
return $this->evalError;
65+
}
66+
}

src/PhpSpreadsheet/Reader/Xlsx.php

+50
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,12 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
956956
++$cIndex;
957957
}
958958
}
959+
if ($xmlSheetNS && $xmlSheetNS->ignoredErrors) {
960+
foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredErrorx) {
961+
$ignoredError = self::testSimpleXml($ignoredErrorx);
962+
$this->processIgnoredErrors($ignoredError, $docSheet);
963+
}
964+
}
959965

960966
if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) {
961967
$protAttr = $xmlSheetNS->sheetProtection->attributes() ?? [];
@@ -2263,4 +2269,48 @@ private static function extractPalette(?SimpleXMLElement $sxml): array
22632269

22642270
return $array;
22652271
}
2272+
2273+
private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void
2274+
{
2275+
$attributes = self::getAttributes($xml);
2276+
$sqref = (string) ($attributes['sqref'] ?? '');
2277+
$numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? '');
2278+
$formula = (string) ($attributes['formula'] ?? '');
2279+
$twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? '');
2280+
$evalError = (string) ($attributes['evalError'] ?? '');
2281+
if (!empty($sqref)) {
2282+
$explodedSqref = explode(' ', $sqref);
2283+
$pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/';
2284+
foreach ($explodedSqref as $sqref1) {
2285+
if (preg_match($pattern1, $sqref1, $matches) === 1) {
2286+
$firstRow = $matches[2];
2287+
$firstCol = $matches[1];
2288+
if (array_key_exists(3, $matches)) {
2289+
$lastCol = $matches[4];
2290+
$lastRow = $matches[5];
2291+
} else {
2292+
$lastCol = $firstCol;
2293+
$lastRow = $firstRow;
2294+
}
2295+
++$lastCol;
2296+
for ($row = $firstRow; $row <= $lastRow; ++$row) {
2297+
for ($col = $firstCol; $col !== $lastCol; ++$col) {
2298+
if ($numberStoredAsText === '1') {
2299+
$sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true);
2300+
}
2301+
if ($formula === '1') {
2302+
$sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true);
2303+
}
2304+
if ($twoDigitTextYear === '1') {
2305+
$sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true);
2306+
}
2307+
if ($evalError === '1') {
2308+
$sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true);
2309+
}
2310+
}
2311+
}
2312+
}
2313+
}
2314+
}
2315+
}
22662316
}

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

+59-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818

1919
class Worksheet extends WriterPart
2020
{
21+
/** @var string */
22+
private $numberStoredAsText = '';
23+
24+
/** @var string */
25+
private $formula = '';
26+
27+
/** @var string */
28+
private $twoDigitTextYear = '';
29+
30+
/** @var string */
31+
private $evalError = '';
32+
2133
/**
2234
* Write worksheet to XML format.
2335
*
@@ -28,6 +40,10 @@ class Worksheet extends WriterPart
2840
*/
2941
public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false)
3042
{
43+
$this->numberStoredAsText = '';
44+
$this->formula = '';
45+
$this->twoDigitTextYear = '';
46+
$this->evalError = '';
3147
// Create XML writer
3248
$objWriter = null;
3349
if ($this->getParentWriter()->getUseDiskCaching()) {
@@ -118,6 +134,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable
118134
// AlternateContent
119135
$this->writeAlternateContent($objWriter, $worksheet);
120136

137+
// IgnoredErrors
138+
$this->writeIgnoredErrors($objWriter);
139+
121140
// Table
122141
$this->writeTable($objWriter, $worksheet);
123142

@@ -131,6 +150,32 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable
131150
return $objWriter->getData();
132151
}
133152

153+
private function writeIgnoredError(XMLWriter $objWriter, bool &$started, string $attr, string $cells): void
154+
{
155+
if ($cells !== '') {
156+
if (!$started) {
157+
$objWriter->startElement('ignoredErrors');
158+
$started = true;
159+
}
160+
$objWriter->startElement('ignoredError');
161+
$objWriter->writeAttribute('sqref', substr($cells, 1));
162+
$objWriter->writeAttribute($attr, '1');
163+
$objWriter->endElement();
164+
}
165+
}
166+
167+
private function writeIgnoredErrors(XMLWriter $objWriter): void
168+
{
169+
$started = false;
170+
$this->writeIgnoredError($objWriter, $started, 'numberStoredAsText', $this->numberStoredAsText);
171+
$this->writeIgnoredError($objWriter, $started, 'formula', $this->formula);
172+
$this->writeIgnoredError($objWriter, $started, 'twoDigitTextYear', $this->twoDigitTextYear);
173+
$this->writeIgnoredError($objWriter, $started, 'evalError', $this->evalError);
174+
if ($started) {
175+
$objWriter->endElement();
176+
}
177+
}
178+
134179
/**
135180
* Write SheetPr.
136181
*/
@@ -1134,7 +1179,20 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
11341179
array_pop($columnsInRow);
11351180
foreach ($columnsInRow as $column) {
11361181
// Write cell
1137-
$this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable);
1182+
$coord = "$column$currentRow";
1183+
if ($worksheet->getCell($coord)->getIgnoredErrors()->getNumberStoredAsText()) {
1184+
$this->numberStoredAsText .= " $coord";
1185+
}
1186+
if ($worksheet->getCell($coord)->getIgnoredErrors()->getFormula()) {
1187+
$this->formula .= " $coord";
1188+
}
1189+
if ($worksheet->getCell($coord)->getIgnoredErrors()->getTwoDigitTextYear()) {
1190+
$this->twoDigitTextYear .= " $coord";
1191+
}
1192+
if ($worksheet->getCell($coord)->getIgnoredErrors()->getEvalError()) {
1193+
$this->evalError .= " $coord";
1194+
}
1195+
$this->writeCell($objWriter, $worksheet, $coord, $aFlippedStringTable);
11381196
}
11391197
}
11401198

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
4+
5+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
6+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class IgnoredErrorTest extends AbstractFunctional
11+
{
12+
private const FILENAME = 'tests/data/Reader/XLSX/ignoreerror.xlsx';
13+
14+
public function testIgnoredError(): void
15+
{
16+
$reader = new Xlsx();
17+
$originalSpreadsheet = $reader->load(self::FILENAME);
18+
$spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx');
19+
$originalSpreadsheet->disconnectWorksheets();
20+
$sheet = $spreadsheet->getActiveSheet();
21+
self::assertFalse($sheet->getCell('A1')->getIgnoredErrors()->getNumberStoredAsText());
22+
self::assertTrue($sheet->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText());
23+
self::assertFalse($sheet->getCell('A3')->getIgnoredErrors()->getNumberStoredAsText());
24+
self::assertTrue($sheet->getCell('A4')->getIgnoredErrors()->getNumberStoredAsText());
25+
self::assertFalse($sheet->getCell('H2')->getIgnoredErrors()->getNumberStoredAsText());
26+
self::assertTrue($sheet->getCell('H3')->getIgnoredErrors()->getNumberStoredAsText());
27+
self::assertFalse($sheet->getCell('I2')->getIgnoredErrors()->getNumberStoredAsText());
28+
self::assertTrue($sheet->getCell('I3')->getIgnoredErrors()->getNumberStoredAsText());
29+
30+
self::assertFalse($sheet->getCell('H3')->getIgnoredErrors()->getFormula());
31+
self::assertFalse($sheet->getCell('D2')->getIgnoredErrors()->getFormula());
32+
self::assertTrue($sheet->getCell('D3')->getIgnoredErrors()->getFormula());
33+
34+
self::assertFalse($sheet->getCell('A11')->getIgnoredErrors()->getTwoDigitTextYear());
35+
self::assertTrue($sheet->getCell('A12')->getIgnoredErrors()->getTwoDigitTextYear());
36+
37+
self::assertFalse($sheet->getCell('C12')->getIgnoredErrors()->getEvalError());
38+
self::assertTrue($sheet->getCell('C11')->getIgnoredErrors()->getEvalError());
39+
40+
$sheetLast = $spreadsheet->getSheetByNameOrThrow('Last');
41+
self::assertFalse($sheetLast->getCell('D2')->getIgnoredErrors()->getFormula());
42+
self::assertFalse($sheetLast->getCell('D3')->getIgnoredErrors()->getFormula(), 'prior sheet ignoredErrors shouldn\'t bleed');
43+
self::assertFalse($sheetLast->getCell('A1')->getIgnoredErrors()->getNumberStoredAsText());
44+
self::assertFalse($sheetLast->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText());
45+
self::assertTrue($sheetLast->getCell('A3')->getIgnoredErrors()->getNumberStoredAsText());
46+
self::assertFalse($sheetLast->getCell('A4')->getIgnoredErrors()->getNumberStoredAsText(), 'prior sheet numberStoredAsText shouldn\'t bleed');
47+
48+
$spreadsheet->disconnectWorksheets();
49+
}
50+
51+
public function testSetIgnoredError(): void
52+
{
53+
$originalSpreadsheet = new Spreadsheet();
54+
$originalSheet = $originalSpreadsheet->getActiveSheet();
55+
$originalSheet->getCell('A1')->setValueExplicit('0', DataType::TYPE_STRING);
56+
$originalSheet->getCell('A2')->setValueExplicit('1', DataType::TYPE_STRING);
57+
$originalSheet->getStyle('A1:A2')->setQuotePrefix(true);
58+
$originalSheet->getCell('A2')->getIgnoredErrors()->setNumberStoredAsText(true);
59+
$spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx');
60+
$originalSpreadsheet->disconnectWorksheets();
61+
$sheet = $spreadsheet->getActiveSheet();
62+
self::assertSame('0', $sheet->getCell('A1')->getValue());
63+
self::assertSame('1', $sheet->getCell('A2')->getValue());
64+
self::assertFalse($sheet->getCell('A1')->getIgnoredErrors()->getNumberStoredAsText());
65+
self::assertTrue($sheet->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText());
66+
$spreadsheet->disconnectWorksheets();
67+
}
68+
}
12.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)