Skip to content

Commit e4b8863

Browse files
author
MarkBaker
committed
Suport for chained range operators in the Calculation Engine (e.g. A3:B1:C2 which gives an effective combined range of A1:C3 or A5:C10:C20:F1 which gives an effective combined range of A1:F20).
Next step will be allowing Named Cells/Ranges to be chained in the same way.
1 parent a6cb80f commit e4b8863

File tree

2 files changed

+60
-53
lines changed

2 files changed

+60
-53
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

+24-16
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ 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})';
@@ -4135,17 +4135,25 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41354135
$testPrevOp = $stack->last(1);
41364136
if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
41374137
// If we have a worksheet reference, then we're playing with a 3D reference
4138-
if ($matches[2] == '') {
4138+
if ($matches[2] === '') {
41394139
// Otherwise, we 'inherit' the worksheet reference from the start cell reference
41404140
// The start of the cell range reference should be the last entry in $output
41414141
$rangeStartCellRef = $output[count($output) - 1]['value'];
4142-
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4142+
if ($rangeStartCellRef === ':') {
4143+
// Do we have chained range operators?
4144+
$rangeStartCellRef = $output[count($output) - 2]['value'];
4145+
}
4146+
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
41434147
if ($rangeStartMatches[2] > '') {
41444148
$val = $rangeStartMatches[2] . '!' . $val;
41454149
}
41464150
} else {
41474151
$rangeStartCellRef = $output[count($output) - 1]['value'];
4148-
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4152+
if ($rangeStartCellRef === ':') {
4153+
// Do we have chained range operators?
4154+
$rangeStartCellRef = $output[count($output) - 2]['value'];
4155+
}
4156+
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
41494157
if ($rangeStartMatches[2] !== $matches[2]) {
41504158
return $this->raiseFormulaError('3D Range references are not yet supported');
41514159
}
@@ -4461,21 +4469,21 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
44614469

44624470
// Process the operation in the appropriate manner
44634471
switch ($token) {
4464-
// Comparison (Boolean) Operators
4465-
case '>': // Greater than
4466-
case '<': // Less than
4467-
case '>=': // Greater than or Equal to
4468-
case '<=': // Less than or Equal to
4469-
case '=': // Equality
4470-
case '<>': // Inequality
4472+
// Comparison (Boolean) Operators
4473+
case '>': // Greater than
4474+
case '<': // Less than
4475+
case '>=': // Greater than or Equal to
4476+
case '<=': // Less than or Equal to
4477+
case '=': // Equality
4478+
case '<>': // Inequality
44714479
$result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack);
44724480
if (isset($storeKey)) {
44734481
$branchStore[$storeKey] = $result;
44744482
}
44754483

44764484
break;
4477-
// Binary Operators
4478-
case ':': // Range
4485+
// Binary Operators
4486+
case ':': // Range
44794487
if (strpos($operand1Data['reference'], '!') !== false) {
44804488
[$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
44814489
} else {

tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php

+36-37
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,8 @@ protected function setUp(): void
2121
{
2222
$this->spreadSheet = new Spreadsheet();
2323
$this->spreadSheet->getActiveSheet()
24-
->setCellValue('A1', 1)
25-
->setCellValue('B1', 2)
26-
->setCellValue('C1', 3)
27-
->setCellValue('A2', 4)
28-
->setCellValue('B2', 5)
29-
->setCellValue('C2', 6)
30-
->setCellValue('A3', 7)
31-
->setCellValue('B3', 8)
32-
->setCellValue('C3', 9);
24+
->fromArray(array_chunk(range(1, 240), 6), null, 'A1', true);
25+
3326
}
3427

3528
/**
@@ -40,33 +33,39 @@ protected function setUp(): void
4033
public function testRangeEvaluation(string $formula, $expectedResult): void
4134
{
4235
$workSheet = $this->spreadSheet->getActiveSheet();
43-
$workSheet->setCellValue('E1', $formula);
36+
$workSheet->setCellValue('H1', $formula);
4437

45-
$actualRresult = $workSheet->getCell('E1')->getCalculatedValue();
38+
$actualRresult = $workSheet->getCell('H1')->getCalculatedValue();
4639
self::assertSame($expectedResult, $actualRresult);
4740
}
4841

4942
public function providerRangeEvaluation(): array
5043
{
5144
return[
52-
['=SUM(A1:B3,A1:C2)', 48],
53-
['=COUNT(A1:B3,A1:C2)', 12],
54-
['=SUM(A1:B3 A1:C2)', 12],
55-
['=COUNT(A1:B3 A1:C2)', 4],
56-
['=SUM(A1:A3,C1:C3)', 30],
57-
['=COUNT(A1:A3,C1:C3)', 6],
58-
['=SUM(A1:A3 C1:C3)', Functions::null()],
59-
['=COUNT(A1:A3 C1:C3)', 0],
60-
['=SUM(A1:B2,B2:C3)', 40],
61-
['=COUNT(A1:B2,B2:C3)', 8],
62-
['=SUM(A1:B2 B2:C3)', 5],
63-
['=COUNT(A1:B2 B2:C3)', 1],
64-
['=SUM(A1:C1,A3:C3,B1:C3)', 63],
65-
['=COUNT(A1:C1,A3:C3,B1:C3)', 12],
66-
['=SUM(A1:C1,A3:C3 B1:C3)', 23],
67-
['=COUNT(A1:C1,A3:C3 B1:C3)', 5],
68-
['=SUM(Worksheet!A1:B3,Worksheet!A1:C2)', 48],
69-
['=SUM(Worksheet!A1:Worksheet!B3,Worksheet!A1:Worksheet!C2)', 48],
45+
'Sum with Simple Range' => ['=SUM(A1:C3)', 72],
46+
'Count with Simple Range' => ['=COUNT(A1:C3)', 9],
47+
'Sum with UNION #1' => ['=SUM(A1:B3,A1:C2)', 75],
48+
'Count with UNION #1' => ['=COUNT(A1:B3,A1:C2)', 12],
49+
'Sum with INTERSECTION #1' => ['=SUM(A1:B3 A1:C2)', 18],
50+
'Count with INTERSECTION #1' => ['=COUNT(A1:B3 A1:C2)', 4],
51+
'Sum with UNION #2' => ['=SUM(A1:A3,C1:C3)', 48],
52+
'Count with UNION #2' => ['=COUNT(A1:A3,C1:C3)', 6],
53+
'Sum with INTERSECTION #2 - No Intersect' => ['=SUM(A1:A3 C1:C3)', Functions::null()],
54+
'Count with INTERSECTION #2 - No Intersect' => ['=COUNT(A1:A3 C1:C3)', 0],
55+
'Sum with UNION #3' => ['=SUM(A1:B2,B2:C3)', 64],
56+
'Count with UNION #3' => ['=COUNT(A1:B2,B2:C3)', 8],
57+
'Sum with INTERSECTION #3 - Single Cell' => ['=SUM(A1:B2 B2:C3)', 8],
58+
'Count with INTERSECTION #3 - Single Cell' => ['=COUNT(A1:B2 B2:C3)', 1],
59+
'Sum with Triple UNION' => ['=SUM(A1:C1,A3:C3,B1:C3)', 99],
60+
'Count with Triple UNION' => ['=COUNT(A1:C1,A3:C3,B1:C3)', 12],
61+
'Sum with UNION and INTERSECTION' => ['=SUM(A1:C1,A3:C3 B1:C3)', 35],
62+
'Count with UNION and INTERSECTION' => ['=COUNT(A1:C1,A3:C3 B1:C3)', 5],
63+
'Sum with UNION with Worksheet Reference' => ['=SUM(Worksheet!A1:B3,Worksheet!A1:C2)', 75],
64+
'Sum with UNION with full Worksheet Reference' => ['=SUM(Worksheet!A1:Worksheet!B3,Worksheet!A1:Worksheet!C2)', 75],
65+
'Sum with Chained UNION #1' => ['=SUM(A3:B1:C2)', 72],
66+
'Count with Chained UNION #1' => ['=COUNT(A3:B1:C2)', 9],
67+
'Sum with Chained UNION #2' => ['=SUM(A5:C10:C20:F1)', 7260],
68+
'Count with Chained UNION#2' => ['=COUNT(A5:C10:C20:F1)', 120],
7069
];
7170
}
7271

@@ -97,16 +96,16 @@ public function testNamedRangeEvaluation(string $group1, string $group2, string
9796
public function providerNamedRangeEvaluation(): array
9897
{
9998
return[
100-
['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1,GROUP2)', 48],
99+
['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1,GROUP2)', 75],
101100
['$A$1:$B$3', '$A$1:$C$2', '=COUNT(GROUP1,GROUP2)', 12],
102-
['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1 GROUP2)', 12],
101+
['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1 GROUP2)', 18],
103102
['$A$1:$B$3', '$A$1:$C$2', '=COUNT(GROUP1 GROUP2)', 4],
104-
['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 40],
103+
['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 64],
105104
['$A$1:$B$2', '$B$2:$C$3', '=COUNT(GROUP1,GROUP2)', 8],
106-
['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1 GROUP2)', 5],
105+
['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1 GROUP2)', 8],
107106
['$A$1:$B$2', '$B$2:$C$3', '=COUNT(GROUP1 GROUP2)', 1],
108-
['Worksheet!$A$1:$B$2', 'Worksheet!$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 40],
109-
['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3', '=SUM(GROUP1,GROUP2)', 40],
107+
['Worksheet!$A$1:$B$2', 'Worksheet!$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 64],
108+
['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3', '=SUM(GROUP1,GROUP2)', 64],
110109
];
111110
}
112111

@@ -132,9 +131,9 @@ public function testUTF8NamedRangeEvaluation(array $names, array $ranges, string
132131
public function providerUTF8NamedRangeEvaluation(): array
133132
{
134133
return[
135-
[['Γειά', 'σου', 'Κόσμε'], ['$A$1', '$B$1:$B$2', '$C$1:$C$3'], '=SUM(Γειά,σου,Κόσμε)', 26],
134+
[['Γειά', 'σου', 'Κόσμε'], ['$A$1', '$B$1:$B$2', '$C$1:$C$3'], '=SUM(Γειά,σου,Κόσμε)', 38],
136135
[['Γειά', 'σου', 'Κόσμε'], ['$A$1', '$B$1:$B$2', '$C$1:$C$3'], '=COUNT(Γειά,σου,Κόσμε)', 6],
137-
[['Здравствуй', 'мир'], ['$A$1:$A$3', '$C$1:$C$3'], '=SUM(Здравствуй,мир)', 30],
136+
[['Здравствуй', 'мир'], ['$A$1:$A$3', '$C$1:$C$3'], '=SUM(Здравствуй,мир)', 48],
138137
];
139138
}
140139

0 commit comments

Comments
 (0)