Skip to content

Commit d4585ed

Browse files
authored
Merge pull request #2746 from PHPOffice/Issue-2730_Combined-Ranges
Support for "chained" range operators in the Calculation Engine
2 parents 4856376 + 8b83e8a commit d4585ed

File tree

4 files changed

+135
-93
lines changed

4 files changed

+135
-93
lines changed

CHANGELOG.md

+1
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.

phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ parameters:
127127

128128
-
129129
message: "#^Offset 'value' does not exist on array\\|null\\.$#"
130-
count: 3
130+
count: 5
131131
path: src/PhpSpreadsheet/Calculation/Calculation.php
132132

133133
-

src/PhpSpreadsheet/Calculation/Calculation.php

+85-46
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
}
@@ -4159,7 +4167,7 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41594167
$outputItem = $stack->getStackItem('Cell Reference', $val, $val);
41604168

41614169
$output[] = $outputItem;
4162-
} else { // it's a variable, constant, string, number or boolean
4170+
} else { // it's a variable, constant, string, number or boolean
41634171
$localeConstant = false;
41644172
$stackItemType = 'Value';
41654173
$stackItemReference = null;
@@ -4168,39 +4176,62 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41684176
$testPrevOp = $stack->last(1);
41694177
if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
41704178
$stackItemType = 'Cell Reference';
4171-
$startRowColRef = $output[count($output) - 1]['value'];
4172-
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4173-
$rangeSheetRef = $rangeWS1;
4174-
if ($rangeWS1 !== '') {
4175-
$rangeWS1 .= '!';
4176-
}
4177-
$rangeSheetRef = trim($rangeSheetRef, "'");
4178-
[$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4179-
if ($rangeWS2 !== '') {
4180-
$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+
}
41814200
} else {
4182-
$rangeWS2 = $rangeWS1;
4183-
}
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+
}
41844214

4185-
$refSheet = $pCellParent;
4186-
if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4187-
$refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4188-
}
4215+
$refSheet = $pCellParent;
4216+
if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4217+
$refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4218+
}
41894219

4190-
if (ctype_digit($val) && $val <= 1048576) {
4191-
// Row range
4192-
$stackItemType = 'Row Reference';
4193-
/** @var int $valx */
4194-
$valx = $val;
4195-
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007
4196-
$val = "{$rangeWS2}{$endRowColRef}{$val}";
4197-
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
4198-
// Column range
4199-
$stackItemType = 'Column Reference';
4200-
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007
4201-
$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;
42024234
}
4203-
$stackItemReference = $val;
42044235
} elseif ($opCharacter == self::FORMULA_STRING_QUOTE) {
42054236
// UnEscape any quotes within the string
42064237
$val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
@@ -4461,21 +4492,29 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
44614492

44624493
// Process the operation in the appropriate manner
44634494
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
4495+
// Comparison (Boolean) Operators
4496+
case '>': // Greater than
4497+
case '<': // Less than
4498+
case '>=': // Greater than or Equal to
4499+
case '<=': // Less than or Equal to
4500+
case '=': // Equality
4501+
case '<>': // Inequality
44714502
$result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack);
44724503
if (isset($storeKey)) {
44734504
$branchStore[$storeKey] = $result;
44744505
}
44754506

44764507
break;
4477-
// Binary Operators
4478-
case ':': // Range
4508+
// Binary Operators
4509+
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+
}
44794518
if (strpos($operand1Data['reference'], '!') !== false) {
44804519
[$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
44814520
} else {

0 commit comments

Comments
 (0)