From 8c84ce439986989e41a9b05a25181f78413ebbe6 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 12 Apr 2022 17:04:07 +0200 Subject: [PATCH 1/2] Support 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. --- phpstan-baseline.neon | 2 +- .../Calculation/Calculation.php | 40 ++++++----- .../Calculation/Engine/RangeTest.php | 72 +++++++++---------- 3 files changed, 60 insertions(+), 54 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 22e7c803..12329a72 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -127,7 +127,7 @@ parameters: - message: "#^Offset 'value' does not exist on array\\|null\\.$#" - count: 3 + count: 5 path: src/PhpSpreadsheet/Calculation/Calculation.php - diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index b336920c..c14f5252 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -33,11 +33,11 @@ class Calculation // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) - const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative - const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; - const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; - const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; + const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; + const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative // Cell ranges ensuring absolute/relative const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; @@ -4135,17 +4135,25 @@ class Calculation $testPrevOp = $stack->last(1); if ($testPrevOp !== null && $testPrevOp['value'] === ':') { // If we have a worksheet reference, then we're playing with a 3D reference - if ($matches[2] == '') { + if ($matches[2] === '') { // Otherwise, we 'inherit' the worksheet reference from the start cell reference // The start of the cell range reference should be the last entry in $output $rangeStartCellRef = $output[count($output) - 1]['value']; - preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches); + if ($rangeStartCellRef === ':') { + // Do we have chained range operators? + $rangeStartCellRef = $output[count($output) - 2]['value']; + } + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); if ($rangeStartMatches[2] > '') { $val = $rangeStartMatches[2] . '!' . $val; } } else { $rangeStartCellRef = $output[count($output) - 1]['value']; - preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches); + if ($rangeStartCellRef === ':') { + // Do we have chained range operators? + $rangeStartCellRef = $output[count($output) - 2]['value']; + } + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); if ($rangeStartMatches[2] !== $matches[2]) { return $this->raiseFormulaError('3D Range references are not yet supported'); } @@ -4461,21 +4469,21 @@ class Calculation // Process the operation in the appropriate manner switch ($token) { - // Comparison (Boolean) Operators - case '>': // Greater than - case '<': // Less than - case '>=': // Greater than or Equal to - case '<=': // Less than or Equal to - case '=': // Equality - case '<>': // Inequality + // Comparison (Boolean) Operators + case '>': // Greater than + case '<': // Less than + case '>=': // Greater than or Equal to + case '<=': // Less than or Equal to + case '=': // Equality + case '<>': // Inequality $result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack); if (isset($storeKey)) { $branchStore[$storeKey] = $result; } break; - // Binary Operators - case ':': // Range + // Binary Operators + case ':': // Range 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 becf1d71..aa5bcccb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -21,15 +21,7 @@ class RangeTest extends TestCase { $this->spreadSheet = new Spreadsheet(); $this->spreadSheet->getActiveSheet() - ->setCellValue('A1', 1) - ->setCellValue('B1', 2) - ->setCellValue('C1', 3) - ->setCellValue('A2', 4) - ->setCellValue('B2', 5) - ->setCellValue('C2', 6) - ->setCellValue('A3', 7) - ->setCellValue('B3', 8) - ->setCellValue('C3', 9); + ->fromArray(array_chunk(range(1, 240), 6), null, 'A1', true); } /** @@ -40,33 +32,39 @@ class RangeTest extends TestCase public function testRangeEvaluation(string $formula, $expectedResult): void { $workSheet = $this->spreadSheet->getActiveSheet(); - $workSheet->setCellValue('E1', $formula); + $workSheet->setCellValue('H1', $formula); - $actualRresult = $workSheet->getCell('E1')->getCalculatedValue(); + $actualRresult = $workSheet->getCell('H1')->getCalculatedValue(); self::assertSame($expectedResult, $actualRresult); } public function providerRangeEvaluation(): array { return[ - ['=SUM(A1:B3,A1:C2)', 48], - ['=COUNT(A1:B3,A1:C2)', 12], - ['=SUM(A1:B3 A1:C2)', 12], - ['=COUNT(A1:B3 A1:C2)', 4], - ['=SUM(A1:A3,C1:C3)', 30], - ['=COUNT(A1:A3,C1:C3)', 6], - ['=SUM(A1:A3 C1:C3)', Functions::null()], - ['=COUNT(A1:A3 C1:C3)', 0], - ['=SUM(A1:B2,B2:C3)', 40], - ['=COUNT(A1:B2,B2:C3)', 8], - ['=SUM(A1:B2 B2:C3)', 5], - ['=COUNT(A1:B2 B2:C3)', 1], - ['=SUM(A1:C1,A3:C3,B1:C3)', 63], - ['=COUNT(A1:C1,A3:C3,B1:C3)', 12], - ['=SUM(A1:C1,A3:C3 B1:C3)', 23], - ['=COUNT(A1:C1,A3:C3 B1:C3)', 5], - ['=SUM(Worksheet!A1:B3,Worksheet!A1:C2)', 48], - ['=SUM(Worksheet!A1:Worksheet!B3,Worksheet!A1:Worksheet!C2)', 48], + 'Sum with Simple Range' => ['=SUM(A1:C3)', 72], + 'Count with Simple Range' => ['=COUNT(A1:C3)', 9], + 'Sum with UNION #1' => ['=SUM(A1:B3,A1:C2)', 75], + 'Count with UNION #1' => ['=COUNT(A1:B3,A1:C2)', 12], + 'Sum with INTERSECTION #1' => ['=SUM(A1:B3 A1:C2)', 18], + 'Count with INTERSECTION #1' => ['=COUNT(A1:B3 A1:C2)', 4], + 'Sum with UNION #2' => ['=SUM(A1:A3,C1:C3)', 48], + 'Count with UNION #2' => ['=COUNT(A1:A3,C1:C3)', 6], + 'Sum with INTERSECTION #2 - No Intersect' => ['=SUM(A1:A3 C1:C3)', Functions::null()], + 'Count with INTERSECTION #2 - No Intersect' => ['=COUNT(A1:A3 C1:C3)', 0], + 'Sum with UNION #3' => ['=SUM(A1:B2,B2:C3)', 64], + 'Count with UNION #3' => ['=COUNT(A1:B2,B2:C3)', 8], + 'Sum with INTERSECTION #3 - Single Cell' => ['=SUM(A1:B2 B2:C3)', 8], + 'Count with INTERSECTION #3 - Single Cell' => ['=COUNT(A1:B2 B2:C3)', 1], + 'Sum with Triple UNION' => ['=SUM(A1:C1,A3:C3,B1:C3)', 99], + 'Count with Triple UNION' => ['=COUNT(A1:C1,A3:C3,B1:C3)', 12], + 'Sum with UNION and INTERSECTION' => ['=SUM(A1:C1,A3:C3 B1:C3)', 35], + 'Count with UNION and INTERSECTION' => ['=COUNT(A1:C1,A3:C3 B1:C3)', 5], + 'Sum with UNION with Worksheet Reference' => ['=SUM(Worksheet!A1:B3,Worksheet!A1:C2)', 75], + 'Sum with UNION with full Worksheet Reference' => ['=SUM(Worksheet!A1:Worksheet!B3,Worksheet!A1:Worksheet!C2)', 75], + 'Sum with Chained UNION #1' => ['=SUM(A3:B1:C2)', 72], + 'Count with Chained UNION #1' => ['=COUNT(A3:B1:C2)', 9], + 'Sum with Chained UNION #2' => ['=SUM(A5:C10:C20:F1)', 7260], + 'Count with Chained UNION#2' => ['=COUNT(A5:C10:C20:F1)', 120], ]; } @@ -97,16 +95,16 @@ class RangeTest extends TestCase public function providerNamedRangeEvaluation(): array { return[ - ['$A$1:$B$3', '$A$1:$C$2', '=SUM(GROUP1,GROUP2)', 48], + ['$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)', 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)', 40], + ['$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)', 5], + ['$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)', 40], - ['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3', '=SUM(GROUP1,GROUP2)', 40], + ['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], ]; } @@ -132,9 +130,9 @@ class RangeTest extends TestCase public function providerUTF8NamedRangeEvaluation(): array { return[ - [['Γειά', 'σου', 'Κόσμε'], ['$A$1', '$B$1:$B$2', '$C$1:$C$3'], '=SUM(Γειά,σου,Κόσμε)', 26], + [['Γειά', 'σου', 'Κόσμε'], ['$A$1', '$B$1:$B$2', '$C$1:$C$3'], '=SUM(Γειά,σου,Κόσμε)', 38], [['Γειά', 'σου', 'Κόσμε'], ['$A$1', '$B$1:$B$2', '$C$1:$C$3'], '=COUNT(Γειά,σου,Κόσμε)', 6], - [['Здравствуй', 'мир'], ['$A$1:$A$3', '$C$1:$C$3'], '=SUM(Здравствуй,мир)', 30], + [['Здравствуй', 'мир'], ['$A$1:$A$3', '$C$1:$C$3'], '=SUM(Здравствуй,мир)', 48], ]; } From 40730c6023b2429dde4fcd7d4953044813998c3b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 13 Apr 2022 14:28:15 +0200 Subject: [PATCH 2/2] 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. --- CHANGELOG.md | 1 + .../Calculation/Calculation.php | 91 +++++++++++++------ .../Calculation/Engine/RangeTest.php | 34 ++++--- 3 files changed, 81 insertions(+), 45 deletions(-) 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], ]; }