Skip to content

Commit 40730c6

Browse files
author
MarkBaker
committed
Handle defined names with the range operator.
It gets awkward when the defined name is for an actual range rather than for an individual named cell; because we need to manipulate the stack when that happens. The code is ugly, and this is a rather simplistic approach, but it works as long as the named range is a cell, a cell range, or even a "chained" range - it won't work if we have union or intersection operators in the defined range - but it does provide formula support that never existed before.
1 parent 8c84ce4 commit 40730c6

File tree

3 files changed

+81
-45
lines changed

3 files changed

+81
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
6666

6767
### Fixed
6868

69+
- Support for "chained" ranges (e.g. `A5:C10:C20:F1`) in the Calculation Engine; and also support for using named ranges with the Range operator (e.g. `NamedRange1:NamedRange2`) [Issue #2730](https://github.com/PHPOffice/PhpSpreadsheet/issues/2730) [PR #2746](https://github.com/PHPOffice/PhpSpreadsheet/pull/2746)
6970
- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689)
7071
- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687)
7172
- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address.

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4167,7 +4167,7 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41674167
$outputItem = $stack->getStackItem('Cell Reference', $val, $val);
41684168

41694169
$output[] = $outputItem;
4170-
} else { // it's a variable, constant, string, number or boolean
4170+
} else { // it's a variable, constant, string, number or boolean
41714171
$localeConstant = false;
41724172
$stackItemType = 'Value';
41734173
$stackItemReference = null;
@@ -4176,39 +4176,62 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41764176
$testPrevOp = $stack->last(1);
41774177
if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
41784178
$stackItemType = 'Cell Reference';
4179-
$startRowColRef = $output[count($output) - 1]['value'];
4180-
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4181-
$rangeSheetRef = $rangeWS1;
4182-
if ($rangeWS1 !== '') {
4183-
$rangeWS1 .= '!';
4184-
}
4185-
$rangeSheetRef = trim($rangeSheetRef, "'");
4186-
[$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4187-
if ($rangeWS2 !== '') {
4188-
$rangeWS2 .= '!';
4179+
if (
4180+
(preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false) &&
4181+
($this->spreadsheet->getNamedRange($val) !== null)
4182+
) {
4183+
$namedRange = $this->spreadsheet->getNamedRange($val);
4184+
if ($namedRange !== null) {
4185+
$stackItemType = 'Defined Name';
4186+
$address = str_replace('$', '', $namedRange->getValue());
4187+
$stackItemReference = $val;
4188+
if (strpos($address, ':') !== false) {
4189+
// We'll need to manipulate the stack for an actual named range rather than a named cell
4190+
$fromTo = explode(':', $address);
4191+
$to = array_pop($fromTo);
4192+
foreach ($fromTo as $from) {
4193+
$output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference);
4194+
$output[] = $stack->getStackItem('Binary Operator', ':');
4195+
}
4196+
$address = $to;
4197+
}
4198+
$val = $address;
4199+
}
41894200
} else {
4190-
$rangeWS2 = $rangeWS1;
4191-
}
4201+
$startRowColRef = $output[count($output) - 1]['value'];
4202+
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4203+
$rangeSheetRef = $rangeWS1;
4204+
if ($rangeWS1 !== '') {
4205+
$rangeWS1 .= '!';
4206+
}
4207+
$rangeSheetRef = trim($rangeSheetRef, "'");
4208+
[$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4209+
if ($rangeWS2 !== '') {
4210+
$rangeWS2 .= '!';
4211+
} else {
4212+
$rangeWS2 = $rangeWS1;
4213+
}
41924214

4193-
$refSheet = $pCellParent;
4194-
if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4195-
$refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4196-
}
4215+
$refSheet = $pCellParent;
4216+
if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4217+
$refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4218+
}
41974219

4198-
if (ctype_digit($val) && $val <= 1048576) {
4199-
// Row range
4200-
$stackItemType = 'Row Reference';
4201-
/** @var int $valx */
4202-
$valx = $val;
4203-
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007
4204-
$val = "{$rangeWS2}{$endRowColRef}{$val}";
4205-
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
4206-
// Column range
4207-
$stackItemType = 'Column Reference';
4208-
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007
4209-
$val = "{$rangeWS2}{$val}{$endRowColRef}";
4220+
if (ctype_digit($val) && $val <= 1048576) {
4221+
// Row range
4222+
$stackItemType = 'Row Reference';
4223+
/** @var int $valx */
4224+
$valx = $val;
4225+
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007
4226+
$val = "{$rangeWS2}{$endRowColRef}{$val}";
4227+
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
4228+
// Column range
4229+
$stackItemType = 'Column Reference';
4230+
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007
4231+
$val = "{$rangeWS2}{$val}{$endRowColRef}";
4232+
}
4233+
$stackItemReference = $val;
42104234
}
4211-
$stackItemReference = $val;
42124235
} elseif ($opCharacter == self::FORMULA_STRING_QUOTE) {
42134236
// UnEscape any quotes within the string
42144237
$val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
@@ -4484,6 +4507,14 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
44844507
break;
44854508
// Binary Operators
44864509
case ':': // Range
4510+
if ($operand1Data['type'] === 'Defined Name') {
4511+
if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false) {
4512+
$definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']);
4513+
if ($definedName !== null) {
4514+
$operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue());
4515+
}
4516+
}
4517+
}
44874518
if (strpos($operand1Data['reference'], '!') !== false) {
44884519
[$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
44894520
} else {

tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,31 +80,35 @@ public function test3dRangeEvaluation(): void
8080
/**
8181
* @dataProvider providerNamedRangeEvaluation
8282
*/
83-
public function testNamedRangeEvaluation(string $group1, string $group2, string $formula, int $expectedResult): void
83+
public function testNamedRangeEvaluation(array $ranges, string $formula, int $expectedResult): void
8484
{
8585
$workSheet = $this->spreadSheet->getActiveSheet();
86-
$this->spreadSheet->addNamedRange(new NamedRange('GROUP1', $workSheet, $group1));
87-
$this->spreadSheet->addNamedRange(new NamedRange('GROUP2', $workSheet, $group2));
86+
foreach ($ranges as $id => $range) {
87+
$this->spreadSheet->addNamedRange(new NamedRange('GROUP' . ++$id, $workSheet, $range));
88+
}
8889

89-
$workSheet->setCellValue('E1', $formula);
90+
$workSheet->setCellValue('H1', $formula);
9091

91-
$sumRresult = $workSheet->getCell('E1')->getCalculatedValue();
92+
$sumRresult = $workSheet->getCell('H1')->getCalculatedValue();
9293
self::assertSame($expectedResult, $sumRresult);
9394
}
9495

9596
public function providerNamedRangeEvaluation(): array
9697
{
9798
return[
98-
['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1,GROUP2)', 75],
99-
['$A$1:$B$3', '$A$1:$C$2', '=COUNT(GROUP1,GROUP2)', 12],
100-
['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1 GROUP2)', 18],
101-
['$A$1:$B$3', '$A$1:$C$2', '=COUNT(GROUP1 GROUP2)', 4],
102-
['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 64],
103-
['$A$1:$B$2', '$B$2:$C$3', '=COUNT(GROUP1,GROUP2)', 8],
104-
['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1 GROUP2)', 8],
105-
['$A$1:$B$2', '$B$2:$C$3', '=COUNT(GROUP1 GROUP2)', 1],
106-
['Worksheet!$A$1:$B$2', 'Worksheet!$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 64],
107-
['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3', '=SUM(GROUP1,GROUP2)', 64],
99+
[['$A$1:$B$3', '$A$1:$C$2'], '=SUM(GROUP1,GROUP2)', 75],
100+
[['$A$1:$B$3', '$A$1:$C$2'], '=COUNT(GROUP1,GROUP2)', 12],
101+
[['$A$1:$B$3', '$A$1:$C$2'], '=SUM(GROUP1 GROUP2)', 18],
102+
[['$A$1:$B$3', '$A$1:$C$2'], '=COUNT(GROUP1 GROUP2)', 4],
103+
[['$A$1:$B$2', '$B$2:$C$3'], '=SUM(GROUP1,GROUP2)', 64],
104+
[['$A$1:$B$2', '$B$2:$C$3'], '=COUNT(GROUP1,GROUP2)', 8],
105+
[['$A$1:$B$2', '$B$2:$C$3'], '=SUM(GROUP1 GROUP2)', 8],
106+
[['$A$1:$B$2', '$B$2:$C$3'], '=COUNT(GROUP1 GROUP2)', 1],
107+
[['$A$5', '$C$10:$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260],
108+
[['$A$5:$A$7', '$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260],
109+
[['$A$5:$A$7', '$C$10:$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260],
110+
[['Worksheet!$A$1:$B$2', 'Worksheet!$B$2:$C$3'], '=SUM(GROUP1,GROUP2)', 64],
111+
[['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3'], '=SUM(GROUP1,GROUP2)', 64],
108112
];
109113
}
110114

0 commit comments

Comments
 (0)