diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af4e647..a309672a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- 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) - 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) - Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) - Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index c14f5252..fe310be4 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4167,7 +4167,7 @@ class Calculation $outputItem = $stack->getStackItem('Cell Reference', $val, $val); $output[] = $outputItem; - } else { // it's a variable, constant, string, number or boolean + } else { // it's a variable, constant, string, number or boolean $localeConstant = false; $stackItemType = 'Value'; $stackItemReference = null; @@ -4176,39 +4176,62 @@ class Calculation $testPrevOp = $stack->last(1); if ($testPrevOp !== null && $testPrevOp['value'] === ':') { $stackItemType = 'Cell Reference'; - $startRowColRef = $output[count($output) - 1]['value']; - [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true); - $rangeSheetRef = $rangeWS1; - if ($rangeWS1 !== '') { - $rangeWS1 .= '!'; - } - $rangeSheetRef = trim($rangeSheetRef, "'"); - [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); - if ($rangeWS2 !== '') { - $rangeWS2 .= '!'; + if ( + (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false) && + ($this->spreadsheet->getNamedRange($val) !== null) + ) { + $namedRange = $this->spreadsheet->getNamedRange($val); + if ($namedRange !== null) { + $stackItemType = 'Defined Name'; + $address = str_replace('$', '', $namedRange->getValue()); + $stackItemReference = $val; + if (strpos($address, ':') !== false) { + // We'll need to manipulate the stack for an actual named range rather than a named cell + $fromTo = explode(':', $address); + $to = array_pop($fromTo); + foreach ($fromTo as $from) { + $output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference); + $output[] = $stack->getStackItem('Binary Operator', ':'); + } + $address = $to; + } + $val = $address; + } } else { - $rangeWS2 = $rangeWS1; - } + $startRowColRef = $output[count($output) - 1]['value']; + [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true); + $rangeSheetRef = $rangeWS1; + if ($rangeWS1 !== '') { + $rangeWS1 .= '!'; + } + $rangeSheetRef = trim($rangeSheetRef, "'"); + [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); + if ($rangeWS2 !== '') { + $rangeWS2 .= '!'; + } else { + $rangeWS2 = $rangeWS1; + } - $refSheet = $pCellParent; - if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) { - $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef); - } + $refSheet = $pCellParent; + if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) { + $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef); + } - if (ctype_digit($val) && $val <= 1048576) { - // Row range - $stackItemType = 'Row Reference'; - /** @var int $valx */ - $valx = $val; - $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007 - $val = "{$rangeWS2}{$endRowColRef}{$val}"; - } elseif (ctype_alpha($val) && strlen($val) <= 3) { - // Column range - $stackItemType = 'Column Reference'; - $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007 - $val = "{$rangeWS2}{$val}{$endRowColRef}"; + if (ctype_digit($val) && $val <= 1048576) { + // Row range + $stackItemType = 'Row Reference'; + /** @var int $valx */ + $valx = $val; + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007 + $val = "{$rangeWS2}{$endRowColRef}{$val}"; + } elseif (ctype_alpha($val) && strlen($val) <= 3) { + // Column range + $stackItemType = 'Column Reference'; + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007 + $val = "{$rangeWS2}{$val}{$endRowColRef}"; + } + $stackItemReference = $val; } - $stackItemReference = $val; } elseif ($opCharacter == self::FORMULA_STRING_QUOTE) { // UnEscape any quotes within the string $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val))); @@ -4484,6 +4507,14 @@ class Calculation break; // Binary Operators case ':': // Range + if ($operand1Data['type'] === 'Defined Name') { + if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false) { + $definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']); + if ($definedName !== null) { + $operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue()); + } + } + } if (strpos($operand1Data['reference'], '!') !== false) { [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true); } else { diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index aa5bcccb..caa7f396 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -80,31 +80,35 @@ class RangeTest extends TestCase /** * @dataProvider providerNamedRangeEvaluation */ - public function testNamedRangeEvaluation(string $group1, string $group2, string $formula, int $expectedResult): void + public function testNamedRangeEvaluation(array $ranges, string $formula, int $expectedResult): void { $workSheet = $this->spreadSheet->getActiveSheet(); - $this->spreadSheet->addNamedRange(new NamedRange('GROUP1', $workSheet, $group1)); - $this->spreadSheet->addNamedRange(new NamedRange('GROUP2', $workSheet, $group2)); + foreach ($ranges as $id => $range) { + $this->spreadSheet->addNamedRange(new NamedRange('GROUP' . ++$id, $workSheet, $range)); + } - $workSheet->setCellValue('E1', $formula); + $workSheet->setCellValue('H1', $formula); - $sumRresult = $workSheet->getCell('E1')->getCalculatedValue(); + $sumRresult = $workSheet->getCell('H1')->getCalculatedValue(); self::assertSame($expectedResult, $sumRresult); } public function providerNamedRangeEvaluation(): array { return[ - ['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1,GROUP2)', 75], - ['$A$1:$B$3', '$A$1:$C$2', '=COUNT(GROUP1,GROUP2)', 12], - ['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1 GROUP2)', 18], - ['$A$1:$B$3', '$A$1:$C$2', '=COUNT(GROUP1 GROUP2)', 4], - ['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 64], - ['$A$1:$B$2', '$B$2:$C$3', '=COUNT(GROUP1,GROUP2)', 8], - ['$A$1:$B$2', '$B$2:$C$3', '=SUM(GROUP1 GROUP2)', 8], - ['$A$1:$B$2', '$B$2:$C$3', '=COUNT(GROUP1 GROUP2)', 1], - ['Worksheet!$A$1:$B$2', 'Worksheet!$B$2:$C$3', '=SUM(GROUP1,GROUP2)', 64], - ['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3', '=SUM(GROUP1,GROUP2)', 64], + [['$A$1:$B$3', '$A$1:$C$2'], '=SUM(GROUP1,GROUP2)', 75], + [['$A$1:$B$3', '$A$1:$C$2'], '=COUNT(GROUP1,GROUP2)', 12], + [['$A$1:$B$3', '$A$1:$C$2'], '=SUM(GROUP1 GROUP2)', 18], + [['$A$1:$B$3', '$A$1:$C$2'], '=COUNT(GROUP1 GROUP2)', 4], + [['$A$1:$B$2', '$B$2:$C$3'], '=SUM(GROUP1,GROUP2)', 64], + [['$A$1:$B$2', '$B$2:$C$3'], '=COUNT(GROUP1,GROUP2)', 8], + [['$A$1:$B$2', '$B$2:$C$3'], '=SUM(GROUP1 GROUP2)', 8], + [['$A$1:$B$2', '$B$2:$C$3'], '=COUNT(GROUP1 GROUP2)', 1], + [['$A$5', '$C$10:$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260], + [['$A$5:$A$7', '$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260], + [['$A$5:$A$7', '$C$10:$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260], + [['Worksheet!$A$1:$B$2', 'Worksheet!$B$2:$C$3'], '=SUM(GROUP1,GROUP2)', 64], + [['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3'], '=SUM(GROUP1,GROUP2)', 64], ]; }