Skip to content

Commit c0c79c7

Browse files
author
MarkBaker
committed
Merge branch 'master' into CalculationEngine-Array-Formulae-Initial-Work
2 parents 1014a03 + 709e2ae commit c0c79c7

File tree

6 files changed

+211
-96
lines changed

6 files changed

+211
-96
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1414
- Implementation of the ISREF() Information function.
1515
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
1616

17-
(i.e a value of "12,345.67" can be read as numeric `1235.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled.
17+
(i.e a value of "12,345.67" can be read as numeric `12345.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled.
1818

1919
This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators.
2020

@@ -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.
@@ -79,6 +80,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
7980

8081
Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic.
8182
- Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651)
83+
- Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739)
84+
8285

8386
## 1.22.0 - 2022-02-18
8487

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ parameters:
8282

8383
-
8484
message: "#^Offset 'value' does not exist on array\\|null\\.$#"
85-
count: 3
85+
count: 5
8686
path: src/PhpSpreadsheet/Calculation/Calculation.php
8787

8888
-

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ class Calculation
3434
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
3535
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
3636
// Cell reference (cell or range of cells, with or without a sheet reference)
37-
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
37+
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
3838
// Cell reference (with or without a sheet reference) ensuring absolute/relative
39-
const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
40-
const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
41-
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
39+
const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
40+
const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
41+
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
4242
// Cell reference (with or without a sheet reference) ensuring absolute/relative
4343
// Cell ranges ensuring absolute/relative
4444
const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
@@ -4149,17 +4149,25 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41494149
$testPrevOp = $stack->last(1);
41504150
if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
41514151
// If we have a worksheet reference, then we're playing with a 3D reference
4152-
if ($matches[2] == '') {
4152+
if ($matches[2] === '') {
41534153
// Otherwise, we 'inherit' the worksheet reference from the start cell reference
41544154
// The start of the cell range reference should be the last entry in $output
41554155
$rangeStartCellRef = $output[count($output) - 1]['value'];
4156-
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4156+
if ($rangeStartCellRef === ':') {
4157+
// Do we have chained range operators?
4158+
$rangeStartCellRef = $output[count($output) - 2]['value'];
4159+
}
4160+
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
41574161
if ($rangeStartMatches[2] > '') {
41584162
$val = $rangeStartMatches[2] . '!' . $val;
41594163
}
41604164
} else {
41614165
$rangeStartCellRef = $output[count($output) - 1]['value'];
4162-
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4166+
if ($rangeStartCellRef === ':') {
4167+
// Do we have chained range operators?
4168+
$rangeStartCellRef = $output[count($output) - 2]['value'];
4169+
}
4170+
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
41634171
if ($rangeStartMatches[2] !== $matches[2]) {
41644172
return $this->raiseFormulaError('3D Range references are not yet supported');
41654173
}
@@ -4173,7 +4181,7 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41734181
$outputItem = $stack->getStackItem('Cell Reference', $val, $val);
41744182

41754183
$output[] = $outputItem;
4176-
} else { // it's a variable, constant, string, number or boolean
4184+
} else { // it's a variable, constant, string, number or boolean
41774185
$localeConstant = false;
41784186
$stackItemType = 'Value';
41794187
$stackItemReference = null;
@@ -4182,39 +4190,62 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41824190
$testPrevOp = $stack->last(1);
41834191
if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
41844192
$stackItemType = 'Cell Reference';
4185-
$startRowColRef = $output[count($output) - 1]['value'];
4186-
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4187-
$rangeSheetRef = $rangeWS1;
4188-
if ($rangeWS1 !== '') {
4189-
$rangeWS1 .= '!';
4190-
}
4191-
$rangeSheetRef = trim($rangeSheetRef, "'");
4192-
[$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4193-
if ($rangeWS2 !== '') {
4194-
$rangeWS2 .= '!';
4193+
if (
4194+
(preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false) &&
4195+
($this->spreadsheet->getNamedRange($val) !== null)
4196+
) {
4197+
$namedRange = $this->spreadsheet->getNamedRange($val);
4198+
if ($namedRange !== null) {
4199+
$stackItemType = 'Defined Name';
4200+
$address = str_replace('$', '', $namedRange->getValue());
4201+
$stackItemReference = $val;
4202+
if (strpos($address, ':') !== false) {
4203+
// We'll need to manipulate the stack for an actual named range rather than a named cell
4204+
$fromTo = explode(':', $address);
4205+
$to = array_pop($fromTo);
4206+
foreach ($fromTo as $from) {
4207+
$output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference);
4208+
$output[] = $stack->getStackItem('Binary Operator', ':');
4209+
}
4210+
$address = $to;
4211+
}
4212+
$val = $address;
4213+
}
41954214
} else {
4196-
$rangeWS2 = $rangeWS1;
4197-
}
4215+
$startRowColRef = $output[count($output) - 1]['value'];
4216+
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4217+
$rangeSheetRef = $rangeWS1;
4218+
if ($rangeWS1 !== '') {
4219+
$rangeWS1 .= '!';
4220+
}
4221+
$rangeSheetRef = trim($rangeSheetRef, "'");
4222+
[$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4223+
if ($rangeWS2 !== '') {
4224+
$rangeWS2 .= '!';
4225+
} else {
4226+
$rangeWS2 = $rangeWS1;
4227+
}
41984228

4199-
$refSheet = $pCellParent;
4200-
if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4201-
$refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4202-
}
4229+
$refSheet = $pCellParent;
4230+
if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4231+
$refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4232+
}
42034233

4204-
if (ctype_digit($val) && $val <= 1048576) {
4205-
// Row range
4206-
$stackItemType = 'Row Reference';
4207-
/** @var int $valx */
4208-
$valx = $val;
4209-
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007
4210-
$val = "{$rangeWS2}{$endRowColRef}{$val}";
4211-
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
4212-
// Column range
4213-
$stackItemType = 'Column Reference';
4214-
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007
4215-
$val = "{$rangeWS2}{$val}{$endRowColRef}";
4234+
if (ctype_digit($val) && $val <= 1048576) {
4235+
// Row range
4236+
$stackItemType = 'Row Reference';
4237+
/** @var int $valx */
4238+
$valx = $val;
4239+
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007
4240+
$val = "{$rangeWS2}{$endRowColRef}{$val}";
4241+
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
4242+
// Column range
4243+
$stackItemType = 'Column Reference';
4244+
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007
4245+
$val = "{$rangeWS2}{$val}{$endRowColRef}";
4246+
}
4247+
$stackItemReference = $val;
42164248
}
4217-
$stackItemReference = $val;
42184249
} elseif ($opCharacter == self::FORMULA_STRING_QUOTE) {
42194250
// UnEscape any quotes within the string
42204251
$val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
@@ -4396,7 +4427,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
43964427
true : (bool) Functions::flattenSingleValue($storeValue);
43974428
if (is_array($storeValue)) {
43984429
$wrappedItem = end($storeValue);
4399-
$storeValue = end($wrappedItem);
4430+
$storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
44004431
}
44014432

44024433
if (
@@ -4428,7 +4459,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
44284459
true : (bool) Functions::flattenSingleValue($storeValue);
44294460
if (is_array($storeValue)) {
44304461
$wrappedItem = end($storeValue);
4431-
$storeValue = end($wrappedItem);
4462+
$storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
44324463
}
44334464

44344465
if (
@@ -4475,21 +4506,29 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
44754506

44764507
// Process the operation in the appropriate manner
44774508
switch ($token) {
4478-
// Comparison (Boolean) Operators
4479-
case '>': // Greater than
4480-
case '<': // Less than
4481-
case '>=': // Greater than or Equal to
4482-
case '<=': // Less than or Equal to
4483-
case '=': // Equality
4484-
case '<>': // Inequality
4509+
// Comparison (Boolean) Operators
4510+
case '>': // Greater than
4511+
case '<': // Less than
4512+
case '>=': // Greater than or Equal to
4513+
case '<=': // Less than or Equal to
4514+
case '=': // Equality
4515+
case '<>': // Inequality
44854516
$result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack);
44864517
if (isset($storeKey)) {
44874518
$branchStore[$storeKey] = $result;
44884519
}
44894520

44904521
break;
4491-
// Binary Operators
4492-
case ':': // Range
4522+
// Binary Operators
4523+
case ':': // Range
4524+
if ($operand1Data['type'] === 'Defined Name') {
4525+
if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false) {
4526+
$definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']);
4527+
if ($definedName !== null) {
4528+
$operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue());
4529+
}
4530+
}
4531+
}
44934532
if (strpos($operand1Data['reference'], '!') !== false) {
44944533
[$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
44954534
} else {

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,19 @@ public function addExternalSheet(Worksheet $worksheet, $sheetIndex = null)
869869
$cell->setXfIndex($cell->getXfIndex() + $countCellXfs);
870870
}
871871

872+
// update the column dimensions Xfs
873+
foreach ($worksheet->getColumnDimensions() as $columnDimension) {
874+
$columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs);
875+
}
876+
877+
// update the row dimensions Xfs
878+
foreach ($worksheet->getRowDimensions() as $rowDimension) {
879+
$xfIndex = $rowDimension->getXfIndex();
880+
if ($xfIndex !== null) {
881+
$rowDimension->setXfIndex($xfIndex + $countCellXfs);
882+
}
883+
}
884+
872885
return $this->addSheet($worksheet, $sheetIndex);
873886
}
874887

0 commit comments

Comments
 (0)