Skip to content

Commit 1f85f15

Browse files
authored
Merge pull request #2899 from PHPOffice/Issue-2874_Calculation-Engine-Quoted-Worksheet-Regexp
Issue 2874 calculation engine quoted worksheet regexp
2 parents 97381d4 + 9f17273 commit 1f85f15

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ class Calculation
3333
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
3434
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
3535
// Cell reference (cell or range of cells, with or without a sheet reference)
36-
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
36+
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
3737
// Cell reference (with or without a sheet reference) ensuring absolute/relative
38-
const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
39-
const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
40-
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
38+
const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
39+
const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
40+
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
4141
// Cell reference (with or without a sheet reference) ensuring absolute/relative
4242
// Cell ranges ensuring absolute/relative
4343
const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
4444
const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})';
4545
// Defined Names: Named Range of cells, or Named Formulae
46-
const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)';
46+
const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)';
4747
// Error
4848
const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
4949

@@ -4203,7 +4203,7 @@ private function internalParseFormula($formula, ?Cell $cell = null)
42034203
$expectingOperator = false;
42044204
}
42054205
$stack->push('Brace', '(');
4206-
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
4206+
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) {
42074207
// Watch for this case-change when modifying to allow cell references in different worksheets...
42084208
// Should only be applied to the actual cell column, not the worksheet name
42094209
// If the last entry on the stack was a : operator, then we have a cell range reference
@@ -4326,6 +4326,8 @@ private function internalParseFormula($formula, ?Cell $cell = null)
43264326
$val = $rowRangeReference[1];
43274327
$length = strlen($rowRangeReference[1]);
43284328
$stackItemType = 'Row Reference';
4329+
// unescape any apostrophes or double quotes in worksheet name
4330+
$val = str_replace(["''", '""'], ["'", '"'], $val);
43294331
$column = 'A';
43304332
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
43314333
$column = $pCellParent->getHighestDataColumn($val);
@@ -4338,6 +4340,8 @@ private function internalParseFormula($formula, ?Cell $cell = null)
43384340
$val = $columnRangeReference[1];
43394341
$length = strlen($val);
43404342
$stackItemType = 'Column Reference';
4343+
// unescape any apostrophes or double quotes in worksheet name
4344+
$val = str_replace(["''", '""'], ["'", '"'], $val);
43414345
$row = '1';
43424346
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
43434347
$row = $pCellParent->getHighestDataRow($val);

tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,78 @@ public function providerBinaryOperations(): array
162162
],
163163
'=B:C',
164164
],
165+
'Combined Cell Reference and Column Range' => [
166+
[
167+
['type' => 'Column Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"],
168+
['type' => 'Column Reference', 'value' => "'sheet1'!A1048576", 'reference' => "'sheet1'!A1048576"],
169+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
170+
['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null],
171+
['type' => 'Function', 'value' => 'MIN(', 'reference' => null],
172+
['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"],
173+
['type' => 'Binary Operator', 'value' => '+', 'reference' => null],
174+
],
175+
"=MIN('sheet1'!A:A) + 'sheet1'!A1",
176+
],
177+
'Combined Cell Reference and Column Range with quote' => [
178+
[
179+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
180+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"],
181+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
182+
['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null],
183+
['type' => 'Function', 'value' => 'MIN(', 'reference' => null],
184+
['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
185+
['type' => 'Binary Operator', 'value' => '+', 'reference' => null],
186+
],
187+
"=MIN('Mark''s sheet1'!A:A) + 'Mark''s sheet1'!A1",
188+
],
189+
'Combined Cell Reference and Column Range with unescaped quote' => [
190+
[
191+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
192+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"],
193+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
194+
['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null],
195+
['type' => 'Function', 'value' => 'MIN(', 'reference' => null],
196+
['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
197+
['type' => 'Binary Operator', 'value' => '+', 'reference' => null],
198+
],
199+
"=MIN('Mark's sheet1'!A:A) + 'Mark's sheet1'!A1",
200+
],
201+
'Combined Column Range and Cell Reference' => [
202+
[
203+
['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"],
204+
['type' => 'Column Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"],
205+
['type' => 'Column Reference', 'value' => "'sheet1'!A1048576", 'reference' => "'sheet1'!A1048576"],
206+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
207+
['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null],
208+
['type' => 'Function', 'value' => 'MIN(', 'reference' => null],
209+
['type' => 'Binary Operator', 'value' => '+', 'reference' => null],
210+
],
211+
"='sheet1'!A1 + MIN('sheet1'!A:A)",
212+
],
213+
'Combined Column Range and Cell Reference with quote' => [
214+
[
215+
['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
216+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
217+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"],
218+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
219+
['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null],
220+
['type' => 'Function', 'value' => 'MIN(', 'reference' => null],
221+
['type' => 'Binary Operator', 'value' => '+', 'reference' => null],
222+
],
223+
"='Mark''s sheet1'!A1 + MIN('Mark''s sheet1'!A:A)",
224+
],
225+
'Combined Column Range and Cell Reference with unescaped quote' => [
226+
[
227+
['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
228+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"],
229+
['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"],
230+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
231+
['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null],
232+
['type' => 'Function', 'value' => 'MIN(', 'reference' => null],
233+
['type' => 'Binary Operator', 'value' => '+', 'reference' => null],
234+
],
235+
"='Mark's sheet1'!A1 + MIN('Mark's sheet1'!A:A)",
236+
],
165237
'Range with Defined Names' => [
166238
[
167239
['type' => 'Defined Name', 'value' => 'GROUP1', 'reference' => 'GROUP1'],

0 commit comments

Comments
 (0)