Skip to content

Commit 06737c1

Browse files
authored
Merge pull request #4073 from oleibman/issue1310
Change Style Without Affecting Current Cell/Sheet, and Invalid Formulas
2 parents 1b68270 + 2a7cbab commit 06737c1

File tree

10 files changed

+114
-11
lines changed

10 files changed

+114
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1010
### Added
1111

1212
- Xlsx Reader Optionally Ignore Rows With No Cells. [Issue #3982](https://github.com/PHPOffice/PhpSpreadsheet/issues/3982) [PR #4035](https://github.com/PHPOffice/PhpSpreadsheet/pull/4035)
13+
- Means to change style without affecting current cell/sheet. [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073)
1314
- Option for CSV output file to have varying numbers of columns for each row. [Issue #1415](https://github.com/PHPOffice/PhpSpreadsheet/issues/1415) [PR #4076](https://github.com/PHPOffice/PhpSpreadsheet/pull/4076)
1415

1516
### Changed
@@ -38,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3839
- Problem rendering line chart with missing plot label. [PR #4074](https://github.com/PHPOffice/PhpSpreadsheet/pull/4074)
3940
- More RTL in Xlsx/Html Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4065](https://github.com/PHPOffice/PhpSpreadsheet/pull/4065)
4041
- Empty String in sharedStrings. [Issue #4063](https://github.com/PHPOffice/PhpSpreadsheet/issues/4063) [PR #4064](https://github.com/PHPOffice/PhpSpreadsheet/pull/4064)
42+
- Treat invalid formulas as strings. [Issue #1310](https://github.com/PHPOffice/PhpSpreadsheet/issues/1310) [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073)
4143

4244
## 2024-05-11 - 2.1.0
4345

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4065,7 +4065,7 @@ private function internalParseFormula(string $formula, ?Cell $cell = null): bool
40654065
$opCharacter = $formula[$index]; // Get the first character of the value at the current index position
40664066

40674067
// Check for two-character operators (e.g. >=, <=, <>)
4068-
if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
4068+
if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && isset($formula[$index + 1], self::$comparisonOperators[$formula[$index + 1]])) {
40694069
$opCharacter .= $formula[++$index];
40704070
}
40714071
// Find out if we're currently at the beginning of a number, variable, cell/row/column reference,

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ public function setValue(mixed $value, ?IValueBinder $binder = null): self
255255
public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
256256
{
257257
$oldValue = $this->value;
258+
$quotePrefix = false;
258259

259260
// set the value according to data type
260261
switch ($dataType) {
@@ -267,6 +268,10 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE
267268
// no break
268269
case DataType::TYPE_STRING:
269270
// Synonym for string
271+
if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
272+
$quotePrefix = true;
273+
}
274+
// no break
270275
case DataType::TYPE_INLINE:
271276
// Rich text
272277
if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) {
@@ -312,6 +317,7 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE
312317
$this->updateInCollection();
313318
$cellCoordinate = $this->getCoordinate();
314319
self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value);
320+
$this->getWorksheet()->applyStylesFromArray($cellCoordinate, ['quotePrefix' => $quotePrefix]);
315321

316322
return $this->getParent()?->get($cellCoordinate) ?? $this;
317323
}

src/PhpSpreadsheet/Cell/DefaultValueBinder.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PhpOffice\PhpSpreadsheet\Cell;
44

55
use DateTimeInterface;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7+
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
68
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
79
use PhpOffice\PhpSpreadsheet\RichText\RichText;
810
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
@@ -68,6 +70,23 @@ public static function dataTypeForValue(mixed $value): string
6870
throw new SpreadsheetException("unusable type $gettype");
6971
}
7072
if (strlen($value) > 1 && $value[0] === '=') {
73+
$calculation = new Calculation();
74+
$calculation->disableBranchPruning();
75+
76+
try {
77+
if (empty($calculation->parseFormula($value))) {
78+
return DataType::TYPE_STRING;
79+
}
80+
} catch (CalculationException $e) {
81+
$message = $e->getMessage();
82+
if (
83+
$message === 'Formula Error: An unexpected error occurred'
84+
|| str_contains($message, 'has no operands')
85+
) {
86+
return DataType::TYPE_STRING;
87+
}
88+
}
89+
7190
return DataType::TYPE_FORMULA;
7291
}
7392
if (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {

src/PhpSpreadsheet/Cell/StringValueBinder.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
99
use Stringable;
1010

11-
class StringValueBinder implements IValueBinder
11+
class StringValueBinder extends DefaultValueBinder implements IValueBinder
1212
{
1313
protected bool $convertNull = true;
1414

@@ -87,12 +87,9 @@ public function bindValue(Cell $cell, mixed $value): bool
8787
$cell->setValueExplicit($value, DataType::TYPE_BOOL);
8888
} elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) {
8989
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
90-
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) {
90+
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) {
9191
$cell->setValueExplicit($value, DataType::TYPE_FORMULA);
9292
} else {
93-
if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
94-
$cell->getStyle()->setQuotePrefix(true);
95-
}
9693
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING);
9794
}
9895

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3673,4 +3673,19 @@ public function copyCells(string $fromCell, string $toCells, bool $copyStyle = t
36733673
}
36743674
}
36753675
}
3676+
3677+
public function applyStylesFromArray(string $coordinate, array $styleArray): bool
3678+
{
3679+
$spreadsheet = $this->parent;
3680+
if ($spreadsheet === null) {
3681+
return false;
3682+
}
3683+
$activeSheetIndex = $spreadsheet->getActiveSheetIndex();
3684+
$originalSelected = $this->selectedCells;
3685+
$this->getStyle($coordinate)->applyFromArray($styleArray);
3686+
$this->selectedCells = $originalSelected;
3687+
$spreadsheet->setActiveSheetIndex($activeSheetIndex);
3688+
3689+
return true;
3690+
}
36763691
}

tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine;
66

7+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
78
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
89
use PhpOffice\PhpSpreadsheet\NamedRange;
910
use PhpOffice\PhpSpreadsheet\Spreadsheet;
@@ -13,20 +14,31 @@ class RangeTest extends TestCase
1314
{
1415
private string $incompleteMessage = 'Must be revisited';
1516

16-
private Spreadsheet $spreadSheet;
17+
private ?Spreadsheet $spreadSheet = null;
1718

18-
protected function setUp(): void
19+
protected function getSpreadsheet(): Spreadsheet
1920
{
20-
$this->spreadSheet = new Spreadsheet();
21-
$this->spreadSheet->getActiveSheet()
21+
$spreadsheet = new Spreadsheet();
22+
$spreadsheet->getActiveSheet()
2223
->fromArray(array_chunk(range(1, 240), 6), null, 'A1', true);
24+
25+
return $spreadsheet;
26+
}
27+
28+
protected function tearDown(): void
29+
{
30+
if ($this->spreadSheet !== null) {
31+
$this->spreadSheet->disconnectWorksheets();
32+
$this->spreadSheet = null;
33+
}
2334
}
2435

2536
/**
2637
* @dataProvider providerRangeEvaluation
2738
*/
2839
public function testRangeEvaluation(string $formula, int|string $expectedResult): void
2940
{
41+
$this->spreadSheet = $this->getSpreadsheet();
3042
$workSheet = $this->spreadSheet->getActiveSheet();
3143
$workSheet->setCellValue('H1', $formula);
3244

@@ -64,8 +76,20 @@ public static function providerRangeEvaluation(): array
6476
];
6577
}
6678

79+
public function test3dRangeParsing(): void
80+
{
81+
// This test shows that parsing throws exception.
82+
// Next test shows that formula is still treated as a formula
83+
// despite the parse failure.
84+
$this->expectExceptionMessage('3D Range references are not yet supported');
85+
$calculation = new Calculation();
86+
$calculation->disableBranchPruning();
87+
$calculation->parseFormula('=SUM(Worksheet!A1:Worksheet2!B3');
88+
}
89+
6790
public function test3dRangeEvaluation(): void
6891
{
92+
$this->spreadSheet = $this->getSpreadsheet();
6993
$workSheet = $this->spreadSheet->getActiveSheet();
7094
$workSheet->setCellValue('E1', '=SUM(Worksheet!A1:Worksheet2!B3)');
7195

@@ -78,6 +102,7 @@ public function test3dRangeEvaluation(): void
78102
*/
79103
public function testNamedRangeEvaluation(array $ranges, string $formula, int $expectedResult): void
80104
{
105+
$this->spreadSheet = $this->getSpreadsheet();
81106
$workSheet = $this->spreadSheet->getActiveSheet();
82107
foreach ($ranges as $id => $range) {
83108
$this->spreadSheet->addNamedRange(new NamedRange('GROUP' . ++$id, $workSheet, $range));
@@ -116,6 +141,7 @@ public static function providerNamedRangeEvaluation(): array
116141
*/
117142
public function testUTF8NamedRangeEvaluation(array $names, array $ranges, string $formula, int $expectedResult): void
118143
{
144+
$this->spreadSheet = $this->getSpreadsheet();
119145
$workSheet = $this->spreadSheet->getActiveSheet();
120146
foreach ($names as $index => $name) {
121147
$range = $ranges[$index];
@@ -144,6 +170,7 @@ public function testCompositeNamedRangeEvaluation(string $composite, int $expect
144170
if ($this->incompleteMessage !== '') {
145171
self::markTestIncomplete($this->incompleteMessage);
146172
}
173+
$this->spreadSheet = $this->getSpreadsheet();
147174

148175
$workSheet = $this->spreadSheet->getActiveSheet();
149176
$this->spreadSheet->addNamedRange(new NamedRange('COMPOSITE', $workSheet, $composite));

tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder;
88
use PhpOffice\PhpSpreadsheet\Cell\Cell;
9+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
910
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
1011
use PhpOffice\PhpSpreadsheet\Settings;
1112
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
@@ -232,4 +233,31 @@ public static function stringProvider(): array
232233
["Hello\nWorld", true],
233234
];
234235
}
236+
237+
/**
238+
* @dataProvider formulaProvider
239+
*/
240+
public function testFormula(string $value, string $dataType): void
241+
{
242+
$spreadsheet = new Spreadsheet();
243+
$sheet = $spreadsheet->getActiveSheet();
244+
245+
$sheet->getCell('A1')->setValue($value);
246+
self::assertSame($dataType, $sheet->getCell('A1')->getDataType());
247+
if ($dataType === DataType::TYPE_FORMULA) {
248+
self::assertFalse($sheet->getStyle('A1')->getQuotePrefix());
249+
} else {
250+
self::assertTrue($sheet->getStyle('A1')->getQuotePrefix());
251+
}
252+
253+
$spreadsheet->disconnectWorksheets();
254+
}
255+
256+
public static function formulaProvider(): array
257+
{
258+
return [
259+
'normal formula' => ['=SUM(A1:C3)', DataType::TYPE_FORMULA],
260+
'issue 1310' => ['======', DataType::TYPE_STRING],
261+
];
262+
}
235263
}

tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,19 @@ public function testStringValueBinderSuppressFormulaConversion(
211211
$cell->setValue($value);
212212
self::assertSame($expectedValue, $cell->getValue());
213213
self::assertSame($expectedDataType, $cell->getDataType());
214+
if ($expectedDataType === DataType::TYPE_FORMULA) {
215+
self::assertFalse($sheet->getStyle('A1')->getQuotePrefix());
216+
} else {
217+
self::assertTrue($sheet->getStyle('A1')->getQuotePrefix());
218+
}
214219
$spreadsheet->disconnectWorksheets();
215220
}
216221

217222
public static function providerDataValuesSuppressFormulaConversion(): array
218223
{
219224
return [
220-
['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA, false],
225+
'normal formula' => ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA],
226+
'issue 1310' => ['======', '======', DataType::TYPE_STRING],
221227
];
222228
}
223229

tests/data/Cell/DefaultValueBinder.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,7 @@
8383
's',
8484
'1234567890123459012345689012345690',
8585
],
86+
'Issue 1310 Multiple = at start' => ['s', '======'],
87+
'Issue 1310 Variant 1' => ['s', '= ====='],
88+
'Issue 1310 Variant 2' => ['s', '=2*3='],
8689
];

0 commit comments

Comments
 (0)