Merge branch 'PHPOffice:master' into Table-for-Xlsx

This commit is contained in:
aswinkumar863 2022-04-16 18:46:32 +05:30 committed by GitHub
commit 59083263e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2241 additions and 359 deletions

View File

@ -21,7 +21,7 @@ $config
'braces' => true, 'braces' => true,
'cast_spaces' => true, 'cast_spaces' => true,
'class_attributes_separation' => ['elements' => ['method' => 'one', 'property' => 'one']], // const are often grouped with other related const 'class_attributes_separation' => ['elements' => ['method' => 'one', 'property' => 'one']], // const are often grouped with other related const
'class_definition' => true, 'class_definition' => false,
'class_keyword_remove' => false, // ::class keyword gives us better support in IDE 'class_keyword_remove' => false, // ::class keyword gives us better support in IDE
'combine_consecutive_issets' => true, 'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true, 'combine_consecutive_unsets' => true,

View File

@ -9,11 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### Added
- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions - Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts.
- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions.
- Implementation of the ISREF() Information function. - Implementation of the ISREF() Information function.
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. - Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
(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. (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.
This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators.
@ -37,6 +38,27 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Deprecated ### Deprecated
- All Excel Function implementations in `Calculation\Functions` (including the Error functions) have been moved to dedicated classes for groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. - All Excel Function implementations in `Calculation\Functions` (including the Error functions) have been moved to dedicated classes for groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted.
- Worksheet methods that reference cells "byColumnandRow". All such methods have an equivalent that references the cell by its address (e.g. '`E3'` rather than `5, 3`).
These functions now accept either a cell address string (`'E3')` or an array with columnId and rowId (`[5, 3]`) or a new `CellAddress` object as their `cellAddress`/`coordinate` argument.
This includes the methods:
- `setCellValueByColumnAndRow()` use the equivalent `setCellValue()`
- `setCellValueExplicitByColumnAndRow()` use the equivalent `setCellValueExplicit()`
- `getCellByColumnAndRow()` use the equivalent `getCell()`
- `cellExistsByColumnAndRow()` use the equivalent `cellExists()`
- `getStyleByColumnAndRow()` use the equivalent `getStyle()`
- `setBreakByColumnAndRow()` use the equivalent `setBreak()`
- `mergeCellsByColumnAndRow()` use the equivalent `mergeCells()`
- `unmergeCellsByColumnAndRow()` use the equivalent `unmergeCells()`
- `protectCellsByColumnAndRow()` use the equivalent `protectCells()`
- `unprotectCellsByColumnAndRow()` use the equivalent `unprotectCells()`
- `setAutoFilterByColumnAndRow()` use the equivalent `setAutoFilter()`
- `freezePaneByColumnAndRow()` use the equivalent `freezePane()`
- `getCommentByColumnAndRow()` use the equivalent `getComment()`
- `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()`
This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method);
and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument.
### Removed ### Removed
@ -44,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed ### 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) - 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) - 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. - Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address.
@ -57,6 +80,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
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. 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.
- 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) - 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)
- 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)
## 1.22.0 - 2022-02-18 ## 1.22.0 - 2022-02-18

40
composer.lock generated
View File

@ -2084,20 +2084,20 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.4.6", "version": "1.5.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "8a7761f1c520e0dad6e04d862fdc697445457cfe" "reference": "bbf68cae24f6dc023c607ea0f87da55dd9d55c2b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8a7761f1c520e0dad6e04d862fdc697445457cfe", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bbf68cae24f6dc023c607ea0f87da55dd9d55c2b",
"reference": "8a7761f1c520e0dad6e04d862fdc697445457cfe", "reference": "bbf68cae24f6dc023c607ea0f87da55dd9d55c2b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.1|^8.0" "php": "^7.2|^8.0"
}, },
"conflict": { "conflict": {
"phpstan/phpstan-shim": "*" "phpstan/phpstan-shim": "*"
@ -2107,11 +2107,6 @@
"phpstan.phar" "phpstan.phar"
], ],
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": { "autoload": {
"files": [ "files": [
"bootstrap.php" "bootstrap.php"
@ -2124,7 +2119,7 @@
"description": "PHPStan - PHP Static Analysis Tool", "description": "PHPStan - PHP Static Analysis Tool",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan/issues", "issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.4.6" "source": "https://github.com/phpstan/phpstan/tree/1.5.4"
}, },
"funding": [ "funding": [
{ {
@ -2144,25 +2139,25 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-02-06T12:56:13+00:00" "time": "2022-04-03T12:39:00+00:00"
}, },
{ {
"name": "phpstan/phpstan-phpunit", "name": "phpstan/phpstan-phpunit",
"version": "1.0.0", "version": "1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git", "url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3" "reference": "09133ce914f1388a8bb8c7f8573aaa3723cff52a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/09133ce914f1388a8bb8c7f8573aaa3723cff52a",
"reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", "reference": "09133ce914f1388a8bb8c7f8573aaa3723cff52a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.1 || ^8.0", "php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.0" "phpstan/phpstan": "^1.5.0"
}, },
"conflict": { "conflict": {
"phpunit/phpunit": "<7.0" "phpunit/phpunit": "<7.0"
@ -2175,9 +2170,6 @@
}, },
"type": "phpstan-extension", "type": "phpstan-extension",
"extra": { "extra": {
"branch-alias": {
"dev-master": "1.0-dev"
},
"phpstan": { "phpstan": {
"includes": [ "includes": [
"extension.neon", "extension.neon",
@ -2197,9 +2189,9 @@
"description": "PHPUnit extensions and rules for PHPStan", "description": "PHPUnit extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues", "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
"source": "https://github.com/phpstan/phpstan-phpunit/tree/1.0.0" "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.0"
}, },
"time": "2021-10-14T08:03:54+00:00" "time": "2022-03-28T09:20:49+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -5476,5 +5468,5 @@
"ext-zlib": "*" "ext-zlib": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.2.0" "plugin-api-version": "2.3.0"
} }

View File

@ -127,7 +127,7 @@ parameters:
- -
message: "#^Offset 'value' does not exist on array\\|null\\.$#" message: "#^Offset 'value' does not exist on array\\|null\\.$#"
count: 3 count: 5
path: src/PhpSpreadsheet/Calculation/Calculation.php path: src/PhpSpreadsheet/Calculation/Calculation.php
- -
@ -340,11 +340,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php path: src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Engineering\\\\BesselK\\:\\:besselK2\\(\\) has no return type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/Engineering/BesselK.php
- -
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Engineering\\\\ConvertBase\\:\\:validatePlaces\\(\\) has parameter \\$places with no type specified\\.$#" message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Engineering\\\\ConvertBase\\:\\:validatePlaces\\(\\) has parameter \\$places with no type specified\\.$#"
count: 1 count: 1
@ -2070,11 +2065,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Reader/Ods.php path: src/PhpSpreadsheet/Reader/Ods.php
-
message: "#^If condition is always true\\.$#"
count: 1
path: src/PhpSpreadsheet/Reader/Ods.php
- -
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:listWorksheetNames\\(\\) should return array\\<string\\> but returns array\\<int, string\\|null\\>\\.$#" message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:listWorksheetNames\\(\\) should return array\\<string\\> but returns array\\<int, string\\|null\\>\\.$#"
count: 1 count: 1
@ -2095,11 +2085,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Reader/Ods.php path: src/PhpSpreadsheet/Reader/Ods.php
-
message: "#^While loop condition is always true\\.$#"
count: 2
path: src/PhpSpreadsheet/Reader/Ods.php
- -
message: "#^Cannot call method getElementsByTagNameNS\\(\\) on DOMElement\\|null\\.$#" message: "#^Cannot call method getElementsByTagNameNS\\(\\) on DOMElement\\|null\\.$#"
count: 3 count: 3
@ -3880,11 +3865,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Shared/Trend/Trend.php path: src/PhpSpreadsheet/Shared/Trend/Trend.php
-
message: "#^Parameter \\#1 \\$order of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Trend\\\\PolynomialBestFit constructor expects int, string given\\.$#"
count: 2
path: src/PhpSpreadsheet/Shared/Trend/Trend.php
- -
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\XMLWriter\\:\\:getData\\(\\) should return string but returns string\\|false\\.$#" message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\XMLWriter\\:\\:getData\\(\\) should return string but returns string\\|false\\.$#"
count: 1 count: 1
@ -4345,11 +4325,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Worksheet/Worksheet.php path: src/PhpSpreadsheet/Worksheet/Worksheet.php
-
message: "#^Result of && is always true\\.$#"
count: 1
path: src/PhpSpreadsheet/Worksheet/Worksheet.php
- -
message: "#^Right side of && is always true\\.$#" message: "#^Right side of && is always true\\.$#"
count: 1 count: 1

View File

@ -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) // 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]*\('; 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) // 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 // 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_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_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
// Cell reference (with or without a sheet reference) ensuring absolute/relative // Cell reference (with or without a sheet reference) ensuring absolute/relative
// Cell ranges ensuring absolute/relative // Cell ranges ensuring absolute/relative
const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
@ -108,7 +108,8 @@ class Calculation
'+' => true, '-' => true, '*' => true, '/' => true, '+' => true, '-' => true, '*' => true, '/' => true,
'^' => true, '&' => true, '%' => false, '~' => false, '^' => true, '&' => true, '%' => false, '~' => false,
'>' => true, '<' => true, '=' => true, '>=' => true, '>' => true, '<' => true, '=' => true, '>=' => true,
'<=' => true, '<>' => true, '|' => true, ':' => true, '<=' => true, '<>' => true, '∩' => true, '' => true,
':' => true,
]; ];
/** /**
@ -120,7 +121,7 @@ class Calculation
'+' => true, '-' => true, '*' => true, '/' => true, '+' => true, '-' => true, '*' => true, '/' => true,
'^' => true, '&' => true, '>' => true, '<' => true, '^' => true, '&' => true, '>' => true, '<' => true,
'=' => true, '>=' => true, '<=' => true, '<>' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true,
'|' => true, ':' => true, '∩' => true, '' => true, ':' => true,
]; ];
/** /**
@ -3872,7 +3873,7 @@ class Calculation
'*' => 0, '/' => 0, // Multiplication and Division '*' => 0, '/' => 0, // Multiplication and Division
'+' => 0, '-' => 0, // Addition and Subtraction '+' => 0, '-' => 0, // Addition and Subtraction
'&' => 0, // Concatenation '&' => 0, // Concatenation
'|' => 0, ':' => 0, // Intersect and Range '' => 0, '∩' => 0, ':' => 0, // Union, Intersect and Range
'>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
]; ];
@ -3884,8 +3885,9 @@ class Calculation
// This list includes all valid operators, whether binary (including boolean) or unary (such as %) // This list includes all valid operators, whether binary (including boolean) or unary (such as %)
// Array key is the operator, the value is its precedence // Array key is the operator, the value is its precedence
private static $operatorPrecedence = [ private static $operatorPrecedence = [
':' => 8, // Range ':' => 9, // Range
'|' => 7, // Intersect '∩' => 8, // Intersect
'' => 7, // Union
'~' => 6, // Negation '~' => 6, // Negation
'%' => 5, // Percentage '%' => 5, // Percentage
'^' => 4, // Exponentiation '^' => 4, // Exponentiation
@ -3957,7 +3959,7 @@ class Calculation
++$index; ++$index;
} elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded? } elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded?
++$index; // Drop the redundant plus symbol ++$index; // Drop the redundant plus symbol
} elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) { // We have to explicitly deny a tilde or pipe, because they are legal } elseif ((($opCharacter == '~') || ($opCharacter == '∩') || ($opCharacter == '')) && (!$isOperandOrFunction)) { // We have to explicitly deny a tilde, union or intersect because they are legal
return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression
} elseif ((isset(self::$operators[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack? } elseif ((isset(self::$operators[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack?
while ( while (
@ -4135,17 +4137,25 @@ class Calculation
$testPrevOp = $stack->last(1); $testPrevOp = $stack->last(1);
if ($testPrevOp !== null && $testPrevOp['value'] === ':') { if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
// If we have a worksheet reference, then we're playing with a 3D reference // 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 // 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 // The start of the cell range reference should be the last entry in $output
$rangeStartCellRef = $output[count($output) - 1]['value']; $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] > '') { if ($rangeStartMatches[2] > '') {
$val = $rangeStartMatches[2] . '!' . $val; $val = $rangeStartMatches[2] . '!' . $val;
} }
} else { } else {
$rangeStartCellRef = $output[count($output) - 1]['value']; $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]) { if ($rangeStartMatches[2] !== $matches[2]) {
return $this->raiseFormulaError('3D Range references are not yet supported'); return $this->raiseFormulaError('3D Range references are not yet supported');
} }
@ -4168,6 +4178,31 @@ class Calculation
$testPrevOp = $stack->last(1); $testPrevOp = $stack->last(1);
if ($testPrevOp !== null && $testPrevOp['value'] === ':') { if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
$stackItemType = 'Cell Reference'; $stackItemType = 'Cell Reference';
if (
!is_numeric($val) &&
((ctype_alpha($val) === false || strlen($val) > 3)) &&
(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 {
$startRowColRef = $output[count($output) - 1]['value']; $startRowColRef = $output[count($output) - 1]['value'];
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true); [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
$rangeSheetRef = $rangeWS1; $rangeSheetRef = $rangeWS1;
@ -4201,6 +4236,7 @@ class Calculation
$val = "{$rangeWS2}{$val}{$endRowColRef}"; $val = "{$rangeWS2}{$val}{$endRowColRef}";
} }
$stackItemReference = $val; $stackItemReference = $val;
}
} elseif ($opCharacter == self::FORMULA_STRING_QUOTE) { } elseif ($opCharacter == self::FORMULA_STRING_QUOTE) {
// UnEscape any quotes within the string // UnEscape any quotes within the string
$val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val))); $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
@ -4307,7 +4343,7 @@ class Calculation
) { ) {
$output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output
} }
$stack->push('Binary Operator', '|'); // Put an Intersect Operator on the stack $stack->push('Binary Operator', ''); // Put an Intersect Operator on the stack
$expectingOperator = false; $expectingOperator = false;
} }
} }
@ -4382,7 +4418,7 @@ class Calculation
true : (bool) Functions::flattenSingleValue($storeValue); true : (bool) Functions::flattenSingleValue($storeValue);
if (is_array($storeValue)) { if (is_array($storeValue)) {
$wrappedItem = end($storeValue); $wrappedItem = end($storeValue);
$storeValue = end($wrappedItem); $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
} }
if ( if (
@ -4414,7 +4450,7 @@ class Calculation
true : (bool) Functions::flattenSingleValue($storeValue); true : (bool) Functions::flattenSingleValue($storeValue);
if (is_array($storeValue)) { if (is_array($storeValue)) {
$wrappedItem = end($storeValue); $wrappedItem = end($storeValue);
$storeValue = end($wrappedItem); $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
} }
if ( if (
@ -4476,6 +4512,14 @@ class Calculation
break; break;
// Binary Operators // Binary Operators
case ':': // Range 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) { if (strpos($operand1Data['reference'], '!') !== false) {
[$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true); [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
} else { } else {
@ -4599,7 +4643,7 @@ class Calculation
} }
break; break;
case '|': // Intersect case '': // Intersect
$rowIntersect = array_intersect_key($operand1, $operand2); $rowIntersect = array_intersect_key($operand1, $operand2);
$cellIntersect = $oCol = $oRow = []; $cellIntersect = $oCol = $oRow = [];
foreach (array_keys($rowIntersect) as $row) { foreach (array_keys($rowIntersect) as $row) {

View File

@ -68,13 +68,28 @@ class BesselK
return self::besselK2($x, $ord); return self::besselK2($x, $ord);
} }
/**
* Mollify Phpstan.
*
* @codeCoverageIgnore
*/
private static function callBesselI(float $x, int $ord): float
{
$rslt = BesselI::BESSELI($x, $ord);
if (!is_float($rslt)) {
throw new Exception('Unexpected array or string');
}
return $rslt;
}
private static function besselK0(float $x): float private static function besselK0(float $x): float
{ {
if ($x <= 2) { if ($x <= 2) {
$fNum2 = $x * 0.5; $fNum2 = $x * 0.5;
$y = ($fNum2 * $fNum2); $y = ($fNum2 * $fNum2);
return -log($fNum2) * BesselI::BESSELI($x, 0) + return -log($fNum2) * self::callBesselI($x, 0) +
(-0.57721566 + $y * (0.42278420 + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2 + $y * (-0.57721566 + $y * (0.42278420 + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2 + $y *
(0.10750e-3 + $y * 0.74e-5)))))); (0.10750e-3 + $y * 0.74e-5))))));
} }
@ -92,7 +107,7 @@ class BesselK
$fNum2 = $x * 0.5; $fNum2 = $x * 0.5;
$y = ($fNum2 * $fNum2); $y = ($fNum2 * $fNum2);
return log($fNum2) * BesselI::BESSELI($x, 1) + return log($fNum2) * self::callBesselI($x, 1) +
(1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y * (1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y *
(-0.110404e-2 + $y * (-0.4686e-4))))))) / $x; (-0.110404e-2 + $y * (-0.4686e-4))))))) / $x;
} }
@ -104,7 +119,7 @@ class BesselK
(0.325614e-2 + $y * (-0.68245e-3))))))); (0.325614e-2 + $y * (-0.68245e-3)))))));
} }
private static function besselK2(float $x, int $ord) private static function besselK2(float $x, int $ord): float
{ {
$fTox = 2 / $x; $fTox = 2 / $x;
$fBkm = self::besselK0($x); $fBkm = self::besselK0($x);

View File

@ -66,6 +66,21 @@ class BesselY
return self::besselY2($x, $ord); return self::besselY2($x, $ord);
} }
/**
* Mollify Phpstan.
*
* @codeCoverageIgnore
*/
private static function callBesselJ(float $x, int $ord): float
{
$rslt = BesselJ::BESSELJ($x, $ord);
if (!is_float($rslt)) {
throw new Exception('Unexpected array or string');
}
return $rslt;
}
private static function besselY0(float $x): float private static function besselY0(float $x): float
{ {
if ($x < 8.0) { if ($x < 8.0) {
@ -75,7 +90,7 @@ class BesselY
$ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y * $ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y *
(47447.26470 + $y * (226.1030244 + $y)))); (47447.26470 + $y * (226.1030244 + $y))));
return $ans1 / $ans2 + 0.636619772 * BesselJ::BESSELJ($x, 0) * log($x); return $ans1 / $ans2 + 0.636619772 * self::callBesselJ($x, 0) * log($x);
} }
$z = 8.0 / $x; $z = 8.0 / $x;
@ -97,7 +112,7 @@ class BesselY
$ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y * $ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y *
(0.1020426050e6 + $y * (0.3549632885e3 + $y))))); (0.1020426050e6 + $y * (0.3549632885e3 + $y)))));
return ($ans1 / $ans2) + 0.636619772 * (BesselJ::BESSELJ($x, 1) * log($x) - 1 / $x); return ($ans1 / $ans2) + 0.636619772 * (self::callBesselJ($x, 1) * log($x) - 1 / $x);
} }
$z = 8.0 / $x; $z = 8.0 / $x;

View File

@ -70,10 +70,12 @@ class Amortization
return $e->getMessage(); return $e->getMessage();
} }
$yearFrac = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); $yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis);
if (is_string($yearFrac)) { if (is_string($yearFracx)) {
return $yearFrac; return $yearFracx;
} }
/** @var float */
$yearFrac = $yearFracx;
$amortiseCoeff = self::getAmortizationCoefficient($rate); $amortiseCoeff = self::getAmortizationCoefficient($rate);
@ -161,10 +163,12 @@ class Amortization
$fCostDelta = $cost - $salvage; $fCostDelta = $cost - $salvage;
// Note, quirky variation for leap years on the YEARFRAC for this function // Note, quirky variation for leap years on the YEARFRAC for this function
$purchasedYear = DateTimeExcel\DateParts::year($purchased); $purchasedYear = DateTimeExcel\DateParts::year($purchased);
$yearFrac = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); $yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis);
if (is_string($yearFrac)) { if (is_string($yearFracx)) {
return $yearFrac; return $yearFracx;
} }
/** @var float */
$yearFrac = $yearFracx;
if ( if (
($basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) && ($basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) &&

View File

@ -199,6 +199,7 @@ class Coupons
return $e->getMessage(); return $e->getMessage();
} }
/** @var int */
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); $daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis);
$next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT);

View File

@ -44,7 +44,9 @@ class Confidence
if (($alpha <= 0) || ($alpha >= 1) || ($stdDev <= 0) || ($size < 1)) { if (($alpha <= 0) || ($alpha >= 1) || ($stdDev <= 0) || ($size < 1)) {
return ExcelError::NAN(); return ExcelError::NAN();
} }
/** @var float */
$temp = Distributions\StandardNormal::inverse(1 - $alpha / 2);
return Functions::scalar(Distributions\StandardNormal::inverse(1 - $alpha / 2) * $stdDev / sqrt($size)); return Functions::scalar($temp * $stdDev / sqrt($size));
} }
} }

View File

@ -56,8 +56,10 @@ class Binomial
if ($cumulative) { if ($cumulative) {
return self::calculateCumulativeBinomial($value, $trials, $probability); return self::calculateCumulativeBinomial($value, $trials, $probability);
} }
/** @var float */
$comb = Combinations::withoutRepetition($trials, $value);
return Combinations::withoutRepetition($trials, $value) * $probability ** $value return $comb * $probability ** $value
* (1 - $probability) ** ($trials - $value); * (1 - $probability) ** ($trials - $value);
} }
@ -107,7 +109,9 @@ class Binomial
$summer = 0; $summer = 0;
for ($i = $successes; $i <= $limit; ++$i) { for ($i = $successes; $i <= $limit; ++$i) {
$summer += Combinations::withoutRepetition($trials, $i) * $probability ** $i /** @var float */
$comb = Combinations::withoutRepetition($trials, $i);
$summer += $comb * $probability ** $i
* (1 - $probability) ** ($trials - $i); * (1 - $probability) ** ($trials - $i);
} }
@ -159,8 +163,10 @@ class Binomial
return ExcelError::NAN(); return ExcelError::NAN();
} }
} }
/** @var float */
$comb = Combinations::withoutRepetition($failures + $successes - 1, $successes - 1);
return (Combinations::withoutRepetition($failures + $successes - 1, $successes - 1)) return $comb
* ($probability ** $successes) * ((1 - $probability) ** $failures); * ($probability ** $successes) * ((1 - $probability) ** $failures);
} }
@ -220,7 +226,9 @@ class Binomial
{ {
$summer = 0; $summer = 0;
for ($i = 0; $i <= $value; ++$i) { for ($i = 0; $i <= $value; ++$i) {
$summer += Combinations::withoutRepetition($trials, $i) * $probability ** $i /** @var float */
$comb = Combinations::withoutRepetition($trials, $i);
$summer += $comb * $probability ** $i
* (1 - $probability) ** ($trials - $i); * (1 - $probability) ** ($trials - $i);
} }

View File

@ -281,6 +281,7 @@ class ChiSquared
// Relative error controlled by the eps parameter // Relative error controlled by the eps parameter
private static function gser($n, $x) private static function gser($n, $x)
{ {
/** @var float */
$gln = Gamma::ln($n / 2); $gln = Gamma::ln($n / 2);
$a = 0.5 * $n; $a = 0.5 * $n;
$ap = $a; $ap = $a;
@ -304,6 +305,7 @@ class ChiSquared
// Relative error controlled by the eps parameter // Relative error controlled by the eps parameter
private static function gcf($n, $x) private static function gcf($n, $x)
{ {
/** @var float */
$gln = Gamma::ln($n / 2); $gln = Gamma::ln($n / 2);
$a = 0.5 * $n; $a = 0.5 * $n;
$b = $x + 1 - $a; $b = $x + 1 - $a;

View File

@ -131,7 +131,9 @@ class LogNormal
if ($stdDev <= 0) { if ($stdDev <= 0) {
return ExcelError::NAN(); return ExcelError::NAN();
} }
/** @var float */
$inverse = StandardNormal::inverse($probability);
return exp($mean + $stdDev * StandardNormal::inverse($probability)); return exp($mean + $stdDev * $inverse);
} }
} }

View File

@ -51,12 +51,16 @@ class Poisson
$summer = 0; $summer = 0;
$floor = floor($value); $floor = floor($value);
for ($i = 0; $i <= $floor; ++$i) { for ($i = 0; $i <= $floor; ++$i) {
$summer += $mean ** $i / MathTrig\Factorial::fact($i); /** @var float */
$fact = MathTrig\Factorial::fact($i);
$summer += $mean ** $i / $fact;
} }
return exp(0 - $mean) * $summer; return exp(0 - $mean) * $summer;
} }
/** @var float */
$fact = MathTrig\Factorial::fact($value);
return (exp(0 - $mean) * $mean ** $value) / MathTrig\Factorial::fact($value); return (exp(0 - $mean) * $mean ** $value) / $fact;
} }
} }

View File

@ -103,8 +103,10 @@ class StandardNormal
if (!is_numeric($value)) { if (!is_numeric($value)) {
return ExcelError::VALUE(); return ExcelError::VALUE();
} }
/** @var float */
$dist = self::distribution($value, true);
return self::distribution($value, true) - 0.5; return $dist - 0.5;
} }
/** /**
@ -139,6 +141,7 @@ class StandardNormal
} }
if ($sigma === null) { if ($sigma === null) {
/** @var float */
$sigma = StandardDeviations::STDEV($dataSet); $sigma = StandardDeviations::STDEV($dataSet);
} }
$n = count($dataSet); $n = count($dataSet);

View File

@ -31,7 +31,7 @@ class Size
$mArgs = self::filter($aArgs); $mArgs = self::filter($aArgs);
$count = Counts::COUNT($mArgs); $count = Counts::COUNT($mArgs);
--$entry; --$entry;
if (($entry < 0) || ($entry >= $count) || ($count == 0)) { if ($count === 0 || $entry < 0 || $entry >= $count) {
return ExcelError::NAN(); return ExcelError::NAN();
} }
rsort($mArgs); rsort($mArgs);
@ -67,7 +67,7 @@ class Size
$mArgs = self::filter($aArgs); $mArgs = self::filter($aArgs);
$count = Counts::COUNT($mArgs); $count = Counts::COUNT($mArgs);
--$entry; --$entry;
if (($entry < 0) || ($entry >= $count) || ($count == 0)) { if ($count === 0 || $entry < 0 || $entry >= $count) {
return ExcelError::NAN(); return ExcelError::NAN();
} }
sort($mArgs); sort($mArgs);

View File

@ -0,0 +1,22 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
interface AddressRange
{
public const MAX_ROW = 1048576;
public const MAX_COLUMN = 'XFD';
/**
* @return mixed
*/
public function from();
/**
* @return mixed
*/
public function to();
public function __toString(): string;
}

View File

@ -179,6 +179,8 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
{ {
// Convert value to number // Convert value to number
[$hours, $minutes] = explode(':', $value); [$hours, $minutes] = explode(':', $value);
$hours = (int) $hours;
$minutes = (int) $minutes;
$days = ($hours / 24) + ($minutes / 1440); $days = ($hours / 24) + ($minutes / 1440);
$cell->setValueExplicit($days, DataType::TYPE_NUMERIC); $cell->setValueExplicit($days, DataType::TYPE_NUMERIC);
@ -193,6 +195,9 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
{ {
// Convert value to number // Convert value to number
[$hours, $minutes, $seconds] = explode(':', $value); [$hours, $minutes, $seconds] = explode(':', $value);
$hours = (int) $hours;
$minutes = (int) $minutes;
$seconds = (int) $seconds;
$days = ($hours / 24) + ($minutes / 1440) + ($seconds / 86400); $days = ($hours / 24) + ($minutes / 1440) + ($seconds / 86400);
$cell->setValueExplicit($days, DataType::TYPE_NUMERIC); $cell->setValueExplicit($days, DataType::TYPE_NUMERIC);

View File

@ -0,0 +1,174 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class CellAddress
{
/**
* @var ?Worksheet
*/
protected $worksheet;
/**
* @var string
*/
protected $cellAddress;
/**
* @var string
*/
protected $columnName;
/**
* @var int
*/
protected $columnId;
/**
* @var int
*/
protected $rowId;
public function __construct(string $cellAddress, ?Worksheet $worksheet)
{
$this->cellAddress = str_replace('$', '', $cellAddress);
[$this->columnName, $rowId] = Coordinate::coordinateFromString($cellAddress);
$this->rowId = (int) $rowId;
$this->columnId = Coordinate::columnIndexFromString($this->columnName);
$this->worksheet = $worksheet;
}
/**
* @param mixed $columnId
* @param mixed $rowId
*/
private static function validateColumnAndRow($columnId, $rowId): void
{
$array = [$columnId, $rowId];
array_walk(
$array,
function ($value): void {
if (!is_numeric($value) || $value <= 0) {
throw new Exception('Row and Column Ids must be positive integer values');
}
}
);
}
/**
* @param mixed $columnId
* @param mixed $rowId
*/
public static function fromColumnAndRow($columnId, $rowId, ?Worksheet $worksheet = null): self
{
self::validateColumnAndRow($columnId, $rowId);
/** @phpstan-ignore-next-line */
return new static(Coordinate::stringFromColumnIndex($columnId) . ((string) $rowId), $worksheet);
}
public static function fromColumnRowArray(array $array, ?Worksheet $worksheet = null): self
{
[$columnId, $rowId] = $array;
return static::fromColumnAndRow($columnId, $rowId, $worksheet);
}
/**
* @param mixed $cellAddress
*/
public static function fromCellAddress($cellAddress, ?Worksheet $worksheet = null): self
{
/** @phpstan-ignore-next-line */
return new static($cellAddress, $worksheet);
}
/**
* The returned address string will contain the worksheet name as well, if available,
* (ie. if a Worksheet was provided to the constructor).
* e.g. "'Mark''s Worksheet'!C5".
*/
public function fullCellAddress(): string
{
if ($this->worksheet !== null) {
$title = str_replace("'", "''", $this->worksheet->getTitle());
return "'{$title}'!{$this->cellAddress}";
}
return $this->cellAddress;
}
public function worksheet(): ?Worksheet
{
return $this->worksheet;
}
/**
* The returned address string will contain just the column/row address,
* (even if a Worksheet was provided to the constructor).
* e.g. "C5".
*/
public function cellAddress(): string
{
return $this->cellAddress;
}
public function rowId(): int
{
return $this->rowId;
}
public function columnId(): int
{
return $this->columnId;
}
public function columnName(): string
{
return $this->columnName;
}
public function nextRow(int $offset = 1): self
{
$newRowId = $this->rowId + $offset;
if ($newRowId < 1) {
$newRowId = 1;
}
return static::fromColumnAndRow($this->columnId, $newRowId);
}
public function previousRow(int $offset = 1): self
{
return $this->nextRow(0 - $offset);
}
public function nextColumn(int $offset = 1): self
{
$newColumnId = $this->columnId + $offset;
if ($newColumnId < 1) {
$newColumnId = 1;
}
return static::fromColumnAndRow($newColumnId, $this->rowId);
}
public function previousColumn(int $offset = 1): self
{
return $this->nextColumn(0 - $offset);
}
/**
* The returned address string will contain the worksheet name as well, if available,
* (ie. if a Worksheet was provided to the constructor).
* e.g. "'Mark''s Worksheet'!C5".
*/
public function __toString()
{
return $this->fullCellAddress();
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class CellRange implements AddressRange
{
/**
* @var CellAddress
*/
protected $from;
/**
* @var CellAddress
*/
protected $to;
public function __construct(CellAddress $from, CellAddress $to)
{
$this->validateFromTo($from, $to);
}
private function validateFromTo(CellAddress $from, CellAddress $to): void
{
// Identify actual top-left and bottom-right values (in case we've been given top-right and bottom-left)
$firstColumn = min($from->columnId(), $to->columnId());
$firstRow = min($from->rowId(), $to->rowId());
$lastColumn = max($from->columnId(), $to->columnId());
$lastRow = max($from->rowId(), $to->rowId());
$fromWorksheet = $from->worksheet();
$toWorksheet = $to->worksheet();
$this->validateWorksheets($fromWorksheet, $toWorksheet);
$this->from = $this->cellAddressWrapper($firstColumn, $firstRow, $fromWorksheet);
$this->to = $this->cellAddressWrapper($lastColumn, $lastRow, $toWorksheet);
}
private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void
{
if ($fromWorksheet !== null && $toWorksheet !== null) {
// We could simply compare worksheets rather than worksheet titles; but at some point we may introduce
// support for 3d ranges; and at that point we drop this check and let the validation fall through
// to the check for same workbook; but unless we check on titles, this test will also detect if the
// worksheets are in different spreadsheets, and the next check will never execute or throw its
// own exception.
if ($fromWorksheet->getTitle() !== $toWorksheet->getTitle()) {
throw new Exception('3d Cell Ranges are not supported');
} elseif ($fromWorksheet->getParent() !== $toWorksheet->getParent()) {
throw new Exception('Worksheets must be in the same spreadsheet');
}
}
}
private function cellAddressWrapper(int $column, int $row, ?Worksheet $worksheet = null): CellAddress
{
$cellAddress = Coordinate::stringFromColumnIndex($column) . (string) $row;
return new class ($cellAddress, $worksheet) extends CellAddress {
public function nextRow(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::nextRow($offset);
$this->rowId = $result->rowId;
$this->cellAddress = $result->cellAddress;
return $this;
}
public function previousRow(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::previousRow($offset);
$this->rowId = $result->rowId;
$this->cellAddress = $result->cellAddress;
return $this;
}
public function nextColumn(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::nextColumn($offset);
$this->columnId = $result->columnId;
$this->columnName = $result->columnName;
$this->cellAddress = $result->cellAddress;
return $this;
}
public function previousColumn(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::previousColumn($offset);
$this->columnId = $result->columnId;
$this->columnName = $result->columnName;
$this->cellAddress = $result->cellAddress;
return $this;
}
};
}
public function from(): CellAddress
{
// Re-order from/to in case the cell addresses have been modified
$this->validateFromTo($this->from, $this->to);
return $this->from;
}
public function to(): CellAddress
{
// Re-order from/to in case the cell addresses have been modified
$this->validateFromTo($this->from, $this->to);
return $this->to;
}
public function __toString(): string
{
// Re-order from/to in case the cell addresses have been modified
$this->validateFromTo($this->from, $this->to);
if ($this->from->cellAddress() === $this->to->cellAddress()) {
return "{$this->from->fullCellAddress()}";
}
$fromAddress = $this->from->fullCellAddress();
$toAddress = $this->to->cellAddress();
return "{$fromAddress}:{$toAddress}";
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class ColumnRange implements AddressRange
{
/**
* @var ?Worksheet
*/
protected $worksheet;
/**
* @var int
*/
protected $from;
/**
* @var int
*/
protected $to;
public function __construct(string $from, ?string $to = null, ?Worksheet $worksheet = null)
{
$this->validateFromTo(
Coordinate::columnIndexFromString($from),
Coordinate::columnIndexFromString($to ?? $from)
);
$this->worksheet = $worksheet;
}
public static function fromColumnIndexes(int $from, int $to, ?Worksheet $worksheet = null): self
{
return new self(Coordinate::stringFromColumnIndex($from), Coordinate::stringFromColumnIndex($to), $worksheet);
}
/**
* @param array<int|string> $array
*/
public static function fromArray(array $array, ?Worksheet $worksheet = null): self
{
array_walk(
$array,
function (&$column): void {
$column = is_numeric($column) ? Coordinate::stringFromColumnIndex((int) $column) : $column;
}
);
/** @var string $from */
/** @var string $to */
[$from, $to] = $array;
return new self($from, $to, $worksheet);
}
private function validateFromTo(int $from, int $to): void
{
// Identify actual top and bottom values (in case we've been given bottom and top)
$this->from = min($from, $to);
$this->to = max($from, $to);
}
public function columnCount(): int
{
return $this->to - $this->from + 1;
}
public function shiftDown(int $offset = 1): self
{
$newFrom = $this->from + $offset;
$newFrom = ($newFrom < 1) ? 1 : $newFrom;
$newTo = $this->to + $offset;
$newTo = ($newTo < 1) ? 1 : $newTo;
return self::fromColumnIndexes($newFrom, $newTo, $this->worksheet);
}
public function shiftUp(int $offset = 1): self
{
return $this->shiftDown(0 - $offset);
}
public function from(): string
{
return Coordinate::stringFromColumnIndex($this->from);
}
public function to(): string
{
return Coordinate::stringFromColumnIndex($this->to);
}
public function fromIndex(): int
{
return $this->from;
}
public function toIndex(): int
{
return $this->to;
}
public function toCellRange(): CellRange
{
return new CellRange(
CellAddress::fromColumnAndRow($this->from, 1, $this->worksheet),
CellAddress::fromColumnAndRow($this->to, AddressRange::MAX_ROW)
);
}
public function __toString(): string
{
$from = $this->from();
$to = $this->to();
if ($this->worksheet !== null) {
$title = str_replace("'", "''", $this->worksheet->getTitle());
return "'{$title}'!{$from}:{$to}";
}
return "{$from}:{$to}";
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class RowRange implements AddressRange
{
/**
* @var ?Worksheet
*/
protected $worksheet;
/**
* @var int
*/
protected $from;
/**
* @var int
*/
protected $to;
public function __construct(int $from, ?int $to = null, ?Worksheet $worksheet = null)
{
$this->validateFromTo($from, $to ?? $from);
$this->worksheet = $worksheet;
}
public static function fromArray(array $array, ?Worksheet $worksheet = null): self
{
[$from, $to] = $array;
return new self($from, $to, $worksheet);
}
private function validateFromTo(int $from, int $to): void
{
// Identify actual top and bottom values (in case we've been given bottom and top)
$this->from = min($from, $to);
$this->to = max($from, $to);
}
public function from(): int
{
return $this->from;
}
public function to(): int
{
return $this->to;
}
public function rowCount(): int
{
return $this->to - $this->from + 1;
}
public function shiftRight(int $offset = 1): self
{
$newFrom = $this->from + $offset;
$newFrom = ($newFrom < 1) ? 1 : $newFrom;
$newTo = $this->to + $offset;
$newTo = ($newTo < 1) ? 1 : $newTo;
return new self($newFrom, $newTo, $this->worksheet);
}
public function shiftLeft(int $offset = 1): self
{
return $this->shiftRight(0 - $offset);
}
public function toCellRange(): CellRange
{
return new CellRange(
CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString('A'), $this->from, $this->worksheet),
CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(AddressRange::MAX_COLUMN), $this->to)
);
}
public function __toString(): string
{
if ($this->worksheet !== null) {
$title = str_replace("'", "''", $this->worksheet->getTitle());
return "'{$title}'!{$this->from}:{$this->to}";
}
return "{$this->from}:{$this->to}";
}
}

View File

@ -105,7 +105,7 @@ class Ods extends BaseReader
$xml->read(); $xml->read();
while ($xml->read()) { while ($xml->read()) {
// Quickly jump through to the office:body node // Quickly jump through to the office:body node
while ($xml->name !== 'office:body') { while (self::getXmlName($xml) !== 'office:body') {
if ($xml->isEmptyElement) { if ($xml->isEmptyElement) {
$xml->read(); $xml->read();
} else { } else {
@ -114,12 +114,13 @@ class Ods extends BaseReader
} }
// Now read each node until we find our first table:table node // Now read each node until we find our first table:table node
while ($xml->read()) { while ($xml->read()) {
if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { $xmlName = self::getXmlName($xml);
if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
// Loop through each table:table node reading the table:name attribute for each worksheet name // Loop through each table:table node reading the table:name attribute for each worksheet name
do { do {
$worksheetNames[] = $xml->getAttribute('table:name'); $worksheetNames[] = $xml->getAttribute('table:name');
$xml->next(); $xml->next();
} while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); } while (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
} }
} }
} }
@ -152,7 +153,7 @@ class Ods extends BaseReader
$xml->read(); $xml->read();
while ($xml->read()) { while ($xml->read()) {
// Quickly jump through to the office:body node // Quickly jump through to the office:body node
while ($xml->name !== 'office:body') { while (self::getXmlName($xml) !== 'office:body') {
if ($xml->isEmptyElement) { if ($xml->isEmptyElement) {
$xml->read(); $xml->read();
} else { } else {
@ -161,7 +162,7 @@ class Ods extends BaseReader
} }
// Now read each node until we find our first table:table node // Now read each node until we find our first table:table node
while ($xml->read()) { while ($xml->read()) {
if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { if (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
$worksheetNames[] = $xml->getAttribute('table:name'); $worksheetNames[] = $xml->getAttribute('table:name');
$tmpInfo = [ $tmpInfo = [
@ -176,7 +177,7 @@ class Ods extends BaseReader
$currCells = 0; $currCells = 0;
do { do {
$xml->read(); $xml->read();
if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { if (self::getXmlName($xml) == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
$rowspan = $xml->getAttribute('table:number-rows-repeated'); $rowspan = $xml->getAttribute('table:number-rows-repeated');
$rowspan = empty($rowspan) ? 1 : $rowspan; $rowspan = empty($rowspan) ? 1 : $rowspan;
$tmpInfo['totalRows'] += $rowspan; $tmpInfo['totalRows'] += $rowspan;
@ -186,22 +187,22 @@ class Ods extends BaseReader
$xml->read(); $xml->read();
do { do {
$doread = true; $doread = true;
if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { if (self::getXmlName($xml) == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
if (!$xml->isEmptyElement) { if (!$xml->isEmptyElement) {
++$currCells; ++$currCells;
$xml->next(); $xml->next();
$doread = false; $doread = false;
} }
} elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { } elseif (self::getXmlName($xml) == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
$mergeSize = $xml->getAttribute('table:number-columns-repeated'); $mergeSize = $xml->getAttribute('table:number-columns-repeated');
$currCells += (int) $mergeSize; $currCells += (int) $mergeSize;
} }
if ($doread) { if ($doread) {
$xml->read(); $xml->read();
} }
} while ($xml->name != 'table:table-row'); } while (self::getXmlName($xml) != 'table:table-row');
} }
} while ($xml->name != 'table:table'); } while (self::getXmlName($xml) != 'table:table');
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
@ -214,6 +215,16 @@ class Ods extends BaseReader
return $worksheetInfo; return $worksheetInfo;
} }
/**
* Counteract Phpstan caching.
*
* @phpstan-impure
*/
private static function getXmlName(XMLReader $xml): string
{
return $xml->name;
}
/** /**
* Loads PhpSpreadsheet from file. * Loads PhpSpreadsheet from file.
*/ */
@ -644,7 +655,7 @@ class Ods extends BaseReader
$setRow = $configItem->nodeValue; $setRow = $configItem->nodeValue;
} }
} }
$this->setSelected($spreadsheet, $wsname, $setCol, $setRow); $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
} }
break; break;
@ -681,12 +692,7 @@ class Ods extends BaseReader
// Multiple spaces? // Multiple spaces?
/** @var DOMAttr $cAttr */ /** @var DOMAttr $cAttr */
$cAttr = $child->attributes->getNamedItem('c'); $cAttr = $child->attributes->getNamedItem('c');
if ($cAttr) { $multiplier = self::getMultiplier($cAttr);
$multiplier = (int) $cAttr->nodeValue;
} else {
$multiplier = 1;
}
$str .= str_repeat(' ', $multiplier); $str .= str_repeat(' ', $multiplier);
} }
@ -698,6 +704,17 @@ class Ods extends BaseReader
return $str; return $str;
} }
private static function getMultiplier(?DOMAttr $cAttr): int
{
if ($cAttr) {
$multiplier = (int) $cAttr->nodeValue;
} else {
$multiplier = 1;
}
return $multiplier;
}
/** /**
* @param string $is * @param string $is
* *

View File

@ -894,7 +894,7 @@ class Xlsx extends BaseReader
} }
foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) { foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) {
$node = $xmlSheet->dataValidations->addChild('dataValidation'); $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation');
foreach ($item->attributes() ?? [] as $attr) { foreach ($item->attributes() ?? [] as $attr) {
$node->addAttribute($attr->getName(), $attr); $node->addAttribute($attr->getName(), $attr);
} }

View File

@ -83,7 +83,7 @@ class Trend
case self::TREND_POLYNOMIAL_5: case self::TREND_POLYNOMIAL_5:
case self::TREND_POLYNOMIAL_6: case self::TREND_POLYNOMIAL_6:
if (!isset(self::$trendCache[$key])) { if (!isset(self::$trendCache[$key])) {
$order = substr($trendType, -1); $order = (int) substr($trendType, -1);
self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues); self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues);
} }
@ -101,7 +101,7 @@ class Trend
} }
if ($trendType != self::TREND_BEST_FIT_NO_POLY) { if ($trendType != self::TREND_BEST_FIT_NO_POLY) {
foreach (self::$trendTypePolynomialOrders as $trendMethod) { foreach (self::$trendTypePolynomialOrders as $trendMethod) {
$order = substr($trendMethod, -1); $order = (int) substr($trendMethod, -1);
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues); $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
if ($bestFit[$trendMethod]->getError()) { if ($bestFit[$trendMethod]->getError()) {
unset($bestFit[$trendMethod]); unset($bestFit[$trendMethod]);

View File

@ -869,6 +869,19 @@ class Spreadsheet
$cell->setXfIndex($cell->getXfIndex() + $countCellXfs); $cell->setXfIndex($cell->getXfIndex() + $countCellXfs);
} }
// update the column dimensions Xfs
foreach ($worksheet->getColumnDimensions() as $columnDimension) {
$columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs);
}
// update the row dimensions Xfs
foreach ($worksheet->getRowDimensions() as $rowDimension) {
$xfIndex = $rowDimension->getXfIndex();
if ($xfIndex !== null) {
$rowDimension->setXfIndex($xfIndex + $countCellXfs);
}
}
return $this->addSheet($worksheet, $sheetIndex); return $this->addSheet($worksheet, $sheetIndex);
} }

View File

@ -293,7 +293,10 @@ class Color extends Supervisor
*/ */
private static function getColourComponent($rgbValue, $offset, $hex = true) private static function getColourComponent($rgbValue, $offset, $hex = true)
{ {
$colour = substr($rgbValue, $offset, 2); $colour = substr($rgbValue, $offset, 2) ?: '';
if (preg_match('/^[0-9a-f]{2}$/i', $colour) !== 1) {
$colour = '00';
}
return ($hex) ? $colour : (int) hexdec($colour); return ($hex) ? $colour : (int) hexdec($colour);
} }

View File

@ -26,9 +26,12 @@ class FractionFormatter extends BaseFormatter
$decimalLength = strlen($decimalPart); $decimalLength = strlen($decimalPart);
$decimalDivisor = 10 ** $decimalLength; $decimalDivisor = 10 ** $decimalLength;
/** @var float */
$GCD = MathTrig\Gcd::evaluate($decimalPart, $decimalDivisor); $GCD = MathTrig\Gcd::evaluate($decimalPart, $decimalDivisor);
/** @var float */
$decimalPartx = $decimalPart;
$adjustedDecimalPart = $decimalPart / $GCD; $adjustedDecimalPart = $decimalPartx / $GCD;
$adjustedDecimalDivisor = $decimalDivisor / $GCD; $adjustedDecimalDivisor = $decimalDivisor / $GCD;
if ((strpos($format, '0') !== false)) { if ((strpos($format, '0') !== false)) {

View File

@ -4,7 +4,11 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet;
use ArrayObject; use ArrayObject;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
@ -1113,17 +1117,109 @@ class Worksheet implements IComparable
return $this->cellCollection->getHighestRowAndColumn(); return $this->cellCollection->getHighestRowAndColumn();
} }
/**
* Validate a cell address.
*
* @param null|array<int>|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5';
* or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*/
protected function validateCellAddress($cellAddress): string
{
if (is_string($cellAddress)) {
[$worksheet, $address] = self::extractSheetTitle($cellAddress, true);
// if (!empty($worksheet) && $worksheet !== $this->getTitle()) {
// throw new Exception('Reference is not for this worksheet');
// }
return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address);
}
if (is_array($cellAddress)) {
$cellAddress = CellAddress::fromColumnRowArray($cellAddress);
}
return (string) $cellAddress;
}
private function tryDefinedName(string $coordinate): string
{
// Uppercase coordinate
$coordinate = strtoupper($coordinate);
// Eliminate leading equal sign
$coordinate = self::pregReplace('/^=/', '', $coordinate);
$defined = $this->parent->getDefinedName($coordinate, $this);
if ($defined !== null) {
if ($defined->getWorksheet() === $this && !$defined->isFormula()) {
$coordinate = self::pregReplace('/^=/', '', $defined->getValue());
}
}
return $coordinate;
}
/**
* Validate a cell address or cell range.
*
* @param AddressRange|array<int>|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12';
* or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]),
* or as a CellAddress or AddressRange object.
*/
protected function validateCellOrCellRange($cellRange): string
{
if (is_string($cellRange) || is_numeric($cellRange)) {
$cellRange = (string) $cellRange;
// Convert a single column reference like 'A' to 'A:A'
$cellRange = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $cellRange);
// Convert a single row reference like '1' to '1:1'
$cellRange = self::pregReplace('/^(\d+)$/', '${1}:${1}', $cellRange);
} elseif (is_object($cellRange) && $cellRange instanceof CellAddress) {
$cellRange = new CellRange($cellRange, $cellRange);
}
return $this->validateCellRange($cellRange);
}
/**
* Validate a cell range.
*
* @param AddressRange|array<int>|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12';
* or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]),
* or as an AddressRange object.
*/
protected function validateCellRange($cellRange): string
{
if (is_string($cellRange)) {
[$worksheet, $addressRange] = self::extractSheetTitle($cellRange, true);
// Convert Column ranges like 'A:C' to 'A1:C1048576'
$addressRange = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange);
// Convert Row ranges like '1:3' to 'A1:XFD3'
$addressRange = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange);
return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange);
}
if (is_array($cellRange)) {
[$from, $to] = array_chunk($cellRange, 2);
$cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to));
}
return (string) $cellRange;
}
/** /**
* Set a cell value. * Set a cell value.
* *
* @param string $coordinate Coordinate of the cell, eg: 'A1' * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
* @param mixed $value Value of the cell * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* @param mixed $value Value for the cell
* *
* @return $this * @return $this
*/ */
public function setCellValue($coordinate, $value) public function setCellValue($coordinate, $value)
{ {
$this->getCell($coordinate)->setValue($value); $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate));
$this->getCell($cellAddress)->setValue($value);
return $this; return $this;
} }
@ -1131,6 +1227,10 @@ class Worksheet implements IComparable
/** /**
* Set a cell value by using numeric cell coordinates. * Set a cell value by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the setCellValue() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* @param mixed $value Value of the cell * @param mixed $value Value of the cell
@ -1139,7 +1239,7 @@ class Worksheet implements IComparable
*/ */
public function setCellValueByColumnAndRow($columnIndex, $row, $value) public function setCellValueByColumnAndRow($columnIndex, $row, $value)
{ {
$this->getCellByColumnAndRow($columnIndex, $row)->setValue($value); $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value);
return $this; return $this;
} }
@ -1147,7 +1247,8 @@ class Worksheet implements IComparable
/** /**
* Set a cell value. * Set a cell value.
* *
* @param string $coordinate Coordinate of the cell, eg: 'A1' * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
* or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* @param mixed $value Value of the cell * @param mixed $value Value of the cell
* @param string $dataType Explicit data type, see DataType::TYPE_* * @param string $dataType Explicit data type, see DataType::TYPE_*
* *
@ -1155,8 +1256,8 @@ class Worksheet implements IComparable
*/ */
public function setCellValueExplicit($coordinate, $value, $dataType) public function setCellValueExplicit($coordinate, $value, $dataType)
{ {
// Set value $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate));
$this->getCell($coordinate)->setValueExplicit($value, $dataType); $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
return $this; return $this;
} }
@ -1164,6 +1265,10 @@ class Worksheet implements IComparable
/** /**
* Set a cell value by using numeric cell coordinates. * Set a cell value by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the setCellValueExplicit() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* @param mixed $value Value of the cell * @param mixed $value Value of the cell
@ -1173,7 +1278,7 @@ class Worksheet implements IComparable
*/ */
public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType) public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
{ {
$this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType); $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType);
return $this; return $this;
} }
@ -1181,22 +1286,25 @@ class Worksheet implements IComparable
/** /**
* Get cell at a specific coordinate. * Get cell at a specific coordinate.
* *
* @param string $coordinate Coordinate of the cell, eg: 'A1' * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
* or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* *
* @return Cell Cell that was found or created * @return Cell Cell that was found or created
*/ */
public function getCell(string $coordinate): Cell public function getCell($coordinate): Cell
{ {
$cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate));
// Shortcut for increased performance for the vast majority of simple cases // Shortcut for increased performance for the vast majority of simple cases
if ($this->cellCollection->has($coordinate)) { if ($this->cellCollection->has($cellAddress)) {
/** @var Cell $cell */ /** @var Cell $cell */
$cell = $this->cellCollection->get($coordinate); $cell = $this->cellCollection->get($cellAddress);
return $cell; return $cell;
} }
/** @var Worksheet $sheet */ /** @var Worksheet $sheet */
[$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate); [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
$cell = $sheet->cellCollection->get($finalCoordinate); $cell = $sheet->cellCollection->get($finalCoordinate);
return $cell ?? $sheet->createNewCell($finalCoordinate); return $cell ?? $sheet->createNewCell($finalCoordinate);
@ -1220,7 +1328,7 @@ class Worksheet implements IComparable
$sheet = $this->parent->getSheetByName($worksheetReference[0]); $sheet = $this->parent->getSheetByName($worksheetReference[0]);
$finalCoordinate = strtoupper($worksheetReference[1]); $finalCoordinate = strtoupper($worksheetReference[1]);
if (!$sheet) { if ($sheet === null) {
throw new Exception('Sheet not found for name: ' . $worksheetReference[0]); throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
} }
} elseif ( } elseif (
@ -1231,7 +1339,7 @@ class Worksheet implements IComparable
$namedRange = $this->validateNamedRange($coordinate, true); $namedRange = $this->validateNamedRange($coordinate, true);
if ($namedRange !== null) { if ($namedRange !== null) {
$sheet = $namedRange->getWorksheet(); $sheet = $namedRange->getWorksheet();
if (!$sheet) { if ($sheet === null) {
throw new Exception('Sheet not found for named range: ' . $namedRange->getName()); throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
} }
@ -1241,7 +1349,7 @@ class Worksheet implements IComparable
} }
} }
if (!$sheet || !$finalCoordinate) { if ($sheet === null || $finalCoordinate === null) {
$sheet = $this; $sheet = $this;
$finalCoordinate = strtoupper($coordinate); $finalCoordinate = strtoupper($coordinate);
} }
@ -1275,6 +1383,10 @@ class Worksheet implements IComparable
/** /**
* Get cell at a specific coordinate by using numeric cell coordinates. * Get cell at a specific coordinate by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the getCell() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* *
@ -1282,18 +1394,7 @@ class Worksheet implements IComparable
*/ */
public function getCellByColumnAndRow($columnIndex, $row): Cell public function getCellByColumnAndRow($columnIndex, $row): Cell
{ {
$columnLetter = Coordinate::stringFromColumnIndex($columnIndex); return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row);
$coordinate = $columnLetter . $row;
if ($this->cellCollection->has($coordinate)) {
/** @var Cell $cell */
$cell = $this->cellCollection->get($coordinate);
return $cell;
}
// Create new cell object, if required
return $this->createNewCell($coordinate);
} }
/** /**
@ -1338,14 +1439,14 @@ class Worksheet implements IComparable
/** /**
* Does the cell at a specific coordinate exist? * Does the cell at a specific coordinate exist?
* *
* @param string $coordinate Coordinate of the cell eg: 'A1' * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
* * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* @return bool
*/ */
public function cellExists($coordinate) public function cellExists($coordinate): bool
{ {
$cellAddress = $this->validateCellAddress($coordinate);
/** @var Worksheet $sheet */ /** @var Worksheet $sheet */
[$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate); [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
return $sheet->cellCollection->has($finalCoordinate); return $sheet->cellCollection->has($finalCoordinate);
} }
@ -1353,12 +1454,14 @@ class Worksheet implements IComparable
/** /**
* Cell at a specific coordinate by using numeric cell coordinates exists? * Cell at a specific coordinate by using numeric cell coordinates exists?
* *
* @Deprecated 1.23.0
* Use the cellExists() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
*
* @return bool
*/ */
public function cellExistsByColumnAndRow($columnIndex, $row) public function cellExistsByColumnAndRow($columnIndex, $row): bool
{ {
return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row); return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
} }
@ -1426,10 +1529,15 @@ class Worksheet implements IComparable
/** /**
* Get style for cell. * Get style for cell.
* *
* @param string $cellCoordinate Cell coordinate (or range) to get style for, eg: 'A1' * @param AddressRange|array<int>|CellAddress|int|string $cellCoordinate
* A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or a CellAddress or AddressRange object.
*/ */
public function getStyle($cellCoordinate): Style public function getStyle($cellCoordinate): Style
{ {
$cellCoordinate = $this->validateCellOrCellRange($cellCoordinate);
// set this sheet as active // set this sheet as active
$this->parent->setActiveSheetIndex($this->parent->getIndex($this)); $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
@ -1439,6 +1547,35 @@ class Worksheet implements IComparable
return $this->parent->getCellXfSupervisor(); return $this->parent->getCellXfSupervisor();
} }
/**
* Get style for cell by using numeric cell coordinates.
*
* @Deprecated 1.23.0
* Use the getStyle() method with a cell address range such as 'C5:F8' instead;,
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*
* @param int $columnIndex1 Numeric column coordinate of the cell
* @param int $row1 Numeric row coordinate of the cell
* @param null|int $columnIndex2 Numeric column coordinate of the range cell
* @param null|int $row2 Numeric row coordinate of the range cell
*
* @return Style
*/
public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
{
if ($columnIndex2 !== null && $row2 !== null) {
$cellRange = new CellRange(
CellAddress::fromColumnAndRow($columnIndex1, $row1),
CellAddress::fromColumnAndRow($columnIndex2, $row2)
);
return $this->getStyle($cellRange);
}
return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1));
}
/** /**
* Get conditional styles for a cell. * Get conditional styles for a cell.
* *
@ -1493,7 +1630,7 @@ class Worksheet implements IComparable
{ {
$coordinate = strtoupper($coordinate); $coordinate = strtoupper($coordinate);
if (strpos($coordinate, ':') !== false) { if (strpos($coordinate, ':') !== false) {
return isset($this->conditionalStylesCollection[strtoupper($coordinate)]); return isset($this->conditionalStylesCollection[$coordinate]);
} }
$cell = $this->getCell($coordinate); $cell = $this->getCell($coordinate);
@ -1545,27 +1682,6 @@ class Worksheet implements IComparable
return $this; return $this;
} }
/**
* Get style for cell by using numeric cell coordinates.
*
* @param int $columnIndex1 Numeric column coordinate of the cell
* @param int $row1 Numeric row coordinate of the cell
* @param null|int $columnIndex2 Numeric column coordinate of the range cell
* @param null|int $row2 Numeric row coordinate of the range cell
*
* @return Style
*/
public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
{
if ($columnIndex2 !== null && $row2 !== null) {
$cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
return $this->getStyle($cellRange);
}
return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1);
}
/** /**
* Duplicate cell style to a range of cells. * Duplicate cell style to a range of cells.
* *
@ -1650,26 +1766,22 @@ class Worksheet implements IComparable
/** /**
* Set break on a cell. * Set break on a cell.
* *
* @param string $coordinate Cell coordinate (e.g. A1) * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
* or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* @param int $break Break type (type of Worksheet::BREAK_*) * @param int $break Break type (type of Worksheet::BREAK_*)
* *
* @return $this * @return $this
*/ */
public function setBreak($coordinate, $break) public function setBreak($coordinate, $break)
{ {
// Uppercase coordinate $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate));
$coordinate = strtoupper($coordinate);
if ($coordinate != '') { if ($break === self::BREAK_NONE) {
if ($break == self::BREAK_NONE) { if (isset($this->breaks[$cellAddress])) {
if (isset($this->breaks[$coordinate])) { unset($this->breaks[$cellAddress]);
unset($this->breaks[$coordinate]);
} }
} else { } else {
$this->breaks[$coordinate] = $break; $this->breaks[$cellAddress] = $break;
}
} else {
throw new Exception('No cell coordinate specified.');
} }
return $this; return $this;
@ -1678,6 +1790,10 @@ class Worksheet implements IComparable
/** /**
* Set break on a cell by using numeric cell coordinates. * Set break on a cell by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the setBreak() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* @param int $break Break type (type of Worksheet::BREAK_*) * @param int $break Break type (type of Worksheet::BREAK_*)
@ -1702,18 +1818,15 @@ class Worksheet implements IComparable
/** /**
* Set merge on a cell range. * Set merge on a cell range.
* *
* @param string $range Cell range (e.g. A1:E1) * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange.
* *
* @return $this * @return $this
*/ */
public function mergeCells($range) public function mergeCells($range)
{ {
// Uppercase coordinate $range = Functions::trimSheetFromCellReference($this->validateCellRange($range));
$range = strtoupper($range);
// Convert 'A:C' to 'A1:C1048576'
$range = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $range);
// Convert '1:3' to 'A1:XFD3'
$range = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $range);
if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) { if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) {
$this->mergeCells[$range] = $range; $this->mergeCells[$range] = $range;
@ -1727,7 +1840,7 @@ class Worksheet implements IComparable
$numberColumns = $lastColumnIndex - $firstColumnIndex; $numberColumns = $lastColumnIndex - $firstColumnIndex;
// create upper left cell if it does not already exist // create upper left cell if it does not already exist
$upperLeft = "$firstColumn$firstRow"; $upperLeft = "{$firstColumn}{$firstRow}";
if (!$this->cellExists($upperLeft)) { if (!$this->cellExists($upperLeft)) {
$this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
} }
@ -1789,6 +1902,11 @@ class Worksheet implements IComparable
/** /**
* Set merge on a cell range by using numeric cell coordinates. * Set merge on a cell range by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the mergeCells() method with a cell address range such as 'C5:F8' instead;,
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*
* @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $columnIndex1 Numeric column coordinate of the first cell
* @param int $row1 Numeric row coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell
* @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $columnIndex2 Numeric column coordinate of the last cell
@ -1798,7 +1916,10 @@ class Worksheet implements IComparable
*/ */
public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
{ {
$cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; $cellRange = new CellRange(
CellAddress::fromColumnAndRow($columnIndex1, $row1),
CellAddress::fromColumnAndRow($columnIndex2, $row2)
);
return $this->mergeCells($cellRange); return $this->mergeCells($cellRange);
} }
@ -1806,14 +1927,15 @@ class Worksheet implements IComparable
/** /**
* Remove merge on a cell range. * Remove merge on a cell range.
* *
* @param string $range Cell range (e.g. A1:E1) * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange.
* *
* @return $this * @return $this
*/ */
public function unmergeCells($range) public function unmergeCells($range)
{ {
// Uppercase coordinate $range = Functions::trimSheetFromCellReference($this->validateCellRange($range));
$range = strtoupper($range);
if (strpos($range, ':') !== false) { if (strpos($range, ':') !== false) {
if (isset($this->mergeCells[$range])) { if (isset($this->mergeCells[$range])) {
@ -1831,6 +1953,11 @@ class Worksheet implements IComparable
/** /**
* Remove merge on a cell range by using numeric cell coordinates. * Remove merge on a cell range by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;,
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*
* @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $columnIndex1 Numeric column coordinate of the first cell
* @param int $row1 Numeric row coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell
* @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $columnIndex2 Numeric column coordinate of the last cell
@ -1840,7 +1967,10 @@ class Worksheet implements IComparable
*/ */
public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
{ {
$cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; $cellRange = new CellRange(
CellAddress::fromColumnAndRow($columnIndex1, $row1),
CellAddress::fromColumnAndRow($columnIndex2, $row2)
);
return $this->unmergeCells($cellRange); return $this->unmergeCells($cellRange);
} }
@ -1871,9 +2001,11 @@ class Worksheet implements IComparable
} }
/** /**
* Set protection on a cell range. * Set protection on a cell or cell range.
* *
* @param string $range Cell (e.g. A1) or cell range (e.g. A1:E1) * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or a CellAddress or AddressRange object.
* @param string $password Password to unlock the protection * @param string $password Password to unlock the protection
* @param bool $alreadyHashed If the password has already been hashed, set this to true * @param bool $alreadyHashed If the password has already been hashed, set this to true
* *
@ -1881,8 +2013,7 @@ class Worksheet implements IComparable
*/ */
public function protectCells($range, $password, $alreadyHashed = false) public function protectCells($range, $password, $alreadyHashed = false)
{ {
// Uppercase coordinate $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range));
$range = strtoupper($range);
if (!$alreadyHashed) { if (!$alreadyHashed) {
$password = Shared\PasswordHasher::hashPassword($password); $password = Shared\PasswordHasher::hashPassword($password);
@ -1895,6 +2026,11 @@ class Worksheet implements IComparable
/** /**
* Set protection on a cell range by using numeric cell coordinates. * Set protection on a cell range by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*
* @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $columnIndex1 Numeric column coordinate of the first cell
* @param int $row1 Numeric row coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell
* @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $columnIndex2 Numeric column coordinate of the last cell
@ -1906,22 +2042,26 @@ class Worksheet implements IComparable
*/ */
public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false) public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
{ {
$cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; $cellRange = new CellRange(
CellAddress::fromColumnAndRow($columnIndex1, $row1),
CellAddress::fromColumnAndRow($columnIndex2, $row2)
);
return $this->protectCells($cellRange, $password, $alreadyHashed); return $this->protectCells($cellRange, $password, $alreadyHashed);
} }
/** /**
* Remove protection on a cell range. * Remove protection on a cell or cell range.
* *
* @param string $range Cell (e.g. A1) or cell range (e.g. A1:E1) * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or a CellAddress or AddressRange object.
* *
* @return $this * @return $this
*/ */
public function unprotectCells($range) public function unprotectCells($range)
{ {
// Uppercase coordinate $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range));
$range = strtoupper($range);
if (isset($this->protectedCells[$range])) { if (isset($this->protectedCells[$range])) {
unset($this->protectedCells[$range]); unset($this->protectedCells[$range]);
@ -1935,6 +2075,11 @@ class Worksheet implements IComparable
/** /**
* Remove protection on a cell range by using numeric cell coordinates. * Remove protection on a cell range by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*
* @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $columnIndex1 Numeric column coordinate of the first cell
* @param int $row1 Numeric row coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell
* @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $columnIndex2 Numeric column coordinate of the last cell
@ -1944,7 +2089,10 @@ class Worksheet implements IComparable
*/ */
public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
{ {
$cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; $cellRange = new CellRange(
CellAddress::fromColumnAndRow($columnIndex1, $row1),
CellAddress::fromColumnAndRow($columnIndex2, $row2)
);
return $this->unprotectCells($cellRange); return $this->unprotectCells($cellRange);
} }
@ -1972,17 +2120,21 @@ class Worksheet implements IComparable
/** /**
* Set AutoFilter. * Set AutoFilter.
* *
* @param AutoFilter|string $autoFilterOrRange * @param AddressRange|array<int>|AutoFilter|string $autoFilterOrRange
* A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange.
* *
* @return $this * @return $this
*/ */
public function setAutoFilter($autoFilterOrRange) public function setAutoFilter($autoFilterOrRange)
{ {
if (is_string($autoFilterOrRange)) { if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
$this->autoFilter->setRange($autoFilterOrRange);
} elseif (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
$this->autoFilter = $autoFilterOrRange; $this->autoFilter = $autoFilterOrRange;
} else {
$cellRange = Functions::trimSheetFromCellReference($this->validateCellRange($autoFilterOrRange));
$this->autoFilter->setRange($cellRange);
} }
return $this; return $this;
@ -1991,6 +2143,11 @@ class Worksheet implements IComparable
/** /**
* Set Autofilter Range by using numeric cell coordinates. * Set Autofilter Range by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;,
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object or AutoFilter object.
*
* @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $columnIndex1 Numeric column coordinate of the first cell
* @param int $row1 Numeric row coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell
* @param int $columnIndex2 Numeric column coordinate of the second cell * @param int $columnIndex2 Numeric column coordinate of the second cell
@ -2000,11 +2157,12 @@ class Worksheet implements IComparable
*/ */
public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
{ {
return $this->setAutoFilter( $cellRange = new CellRange(
Coordinate::stringFromColumnIndex($columnIndex1) . $row1 CellAddress::fromColumnAndRow($columnIndex1, $row1),
. ':' . CellAddress::fromColumnAndRow($columnIndex2, $row2)
Coordinate::stringFromColumnIndex($columnIndex2) . $row2
); );
return $this->setAutoFilter($cellRange);
} }
/** /**
@ -2104,23 +2262,33 @@ class Worksheet implements IComparable
* - B1 will freeze the columns to the left of cell B1 (i.e column A) * - B1 will freeze the columns to the left of cell B1 (i.e column A)
* - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A) * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
* *
* @param null|string $cell Position of the split * @param null|array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
* @param null|string $topLeftCell default position of the right bottom pane * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* Passing a null value for this argument will clear any existing freeze pane for this worksheet.
* @param null|array<int>|CellAddress|string $topLeftCell default position of the right bottom pane
* Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
* or a CellAddress object.
* *
* @return $this * @return $this
*/ */
public function freezePane($cell, $topLeftCell = null) public function freezePane($coordinate, $topLeftCell = null)
{ {
if (is_string($cell) && Coordinate::coordinateIsRange($cell)) { $cellAddress = ($coordinate !== null)
? Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate))
: null;
if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
throw new Exception('Freeze pane can not be set on a range of cells.'); throw new Exception('Freeze pane can not be set on a range of cells.');
} }
$topLeftCell = ($topLeftCell !== null)
? Functions::trimSheetFromCellReference($this->validateCellAddress($topLeftCell))
: null;
if ($cell !== null && $topLeftCell === null) { if ($cellAddress !== null && $topLeftCell === null) {
$coordinate = Coordinate::coordinateFromString($cell); $coordinate = Coordinate::coordinateFromString($cellAddress);
$topLeftCell = $coordinate[0] . $coordinate[1]; $topLeftCell = $coordinate[0] . $coordinate[1];
} }
$this->freezePane = $cell; $this->freezePane = $cellAddress;
$this->topLeftCell = $topLeftCell; $this->topLeftCell = $topLeftCell;
return $this; return $this;
@ -2136,6 +2304,10 @@ class Worksheet implements IComparable
/** /**
* Freeze Pane by using numeric cell coordinates. * Freeze Pane by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the freezePane() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* *
@ -2505,31 +2677,31 @@ class Worksheet implements IComparable
/** /**
* Get comment for cell. * Get comment for cell.
* *
* @param string $cellCoordinate Cell coordinate to get comment for, eg: 'A1' * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
* or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
* *
* @return Comment * @return Comment
*/ */
public function getComment($cellCoordinate) public function getComment($cellCoordinate)
{ {
// Uppercase coordinate $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($cellCoordinate));
$cellCoordinate = strtoupper($cellCoordinate);
if (Coordinate::coordinateIsRange($cellCoordinate)) { if (Coordinate::coordinateIsRange($cellAddress)) {
throw new Exception('Cell coordinate string can not be a range of cells.'); throw new Exception('Cell coordinate string can not be a range of cells.');
} elseif (strpos($cellCoordinate, '$') !== false) { } elseif (strpos($cellAddress, '$') !== false) {
throw new Exception('Cell coordinate string must not be absolute.'); throw new Exception('Cell coordinate string must not be absolute.');
} elseif ($cellCoordinate == '') { } elseif ($cellAddress == '') {
throw new Exception('Cell coordinate can not be zero-length string.'); throw new Exception('Cell coordinate can not be zero-length string.');
} }
// Check if we already have a comment for this cell. // Check if we already have a comment for this cell.
if (isset($this->comments[$cellCoordinate])) { if (isset($this->comments[$cellAddress])) {
return $this->comments[$cellCoordinate]; return $this->comments[$cellAddress];
} }
// If not, create a new comment. // If not, create a new comment.
$newComment = new Comment(); $newComment = new Comment();
$this->comments[$cellCoordinate] = $newComment; $this->comments[$cellAddress] = $newComment;
return $newComment; return $newComment;
} }
@ -2537,6 +2709,10 @@ class Worksheet implements IComparable
/** /**
* Get comment for cell by using numeric cell coordinates. * Get comment for cell by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the getComment() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* *
@ -2595,48 +2771,21 @@ class Worksheet implements IComparable
return self::ensureString(preg_replace($pattern, $replacement, $subject)); return self::ensureString(preg_replace($pattern, $replacement, $subject));
} }
private function tryDefinedName(string $coordinate): string
{
// Uppercase coordinate
$coordinate = strtoupper($coordinate);
// Eliminate leading equal sign
$coordinate = self::pregReplace('/^=/', '', $coordinate);
$defined = $this->parent->getDefinedName($coordinate, $this);
if ($defined !== null) {
if ($defined->getWorksheet() === $this && !$defined->isFormula()) {
$coordinate = self::pregReplace('/^=/', '', $defined->getValue());
}
}
return $coordinate;
}
/** /**
* Select a range of cells. * Select a range of cells.
* *
* @param string $coordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6' * @param AddressRange|array<int>|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or a CellAddress or AddressRange object.
* *
* @return $this * @return $this
*/ */
public function setSelectedCells($coordinate) public function setSelectedCells($coordinate)
{ {
$originalCoordinate = $coordinate; if (is_string($coordinate)) {
$coordinate = $this->tryDefinedName($coordinate); $coordinate = $this->tryDefinedName($coordinate);
// Convert 'A' to 'A:A'
$coordinate = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $coordinate);
// Convert '1' to '1:1'
$coordinate = self::pregReplace('/^(\d+)$/', '${1}:${1}', $coordinate);
// Convert 'A:C' to 'A1:C1048576'
$coordinate = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $coordinate);
// Convert '1:3' to 'A1:XFD3'
$coordinate = self::pregReplace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $coordinate);
if (preg_match('/^\\$?[A-Z]{1,3}\\$?\d{1,7}(:\\$?[A-Z]{1,3}\\$?\d{1,7})?$/', $coordinate) !== 1) {
throw new Exception("Invalid setSelectedCells $originalCoordinate $coordinate");
} }
$coordinate = $this->validateCellOrCellRange($coordinate);
if (Coordinate::coordinateIsRange($coordinate)) { if (Coordinate::coordinateIsRange($coordinate)) {
[$first] = Coordinate::splitRange($coordinate); [$first] = Coordinate::splitRange($coordinate);
@ -2652,6 +2801,10 @@ class Worksheet implements IComparable
/** /**
* Selected cell by using numeric cell coordinates. * Selected cell by using numeric cell coordinates.
* *
* @Deprecated 1.23.0
* Use the setSelectedCells() method with a cell address such as 'C5' instead;,
* or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
*
* @param int $columnIndex Numeric column coordinate of the cell * @param int $columnIndex Numeric column coordinate of the cell
* @param int $row Numeric row coordinate of the cell * @param int $row Numeric row coordinate of the cell
* *

View File

@ -54,6 +54,12 @@ use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
// */ // */
class Worksheet extends BIFFwriter class Worksheet extends BIFFwriter
{ {
/** @var int */
private static $always0 = 0;
/** @var int */
private static $always1 = 1;
/** /**
* Formula parser. * Formula parser.
* *
@ -2923,11 +2929,11 @@ class Worksheet extends BIFFwriter
$flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0); $flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0);
$flags |= (1 == $bTxRotation ? 0x00000008 : 0); $flags |= (1 == $bTxRotation ? 0x00000008 : 0);
// Justify last line flag // Justify last line flag
$flags |= (1 == 1 ? 0x00000010 : 0); $flags |= (1 == self::$always1 ? 0x00000010 : 0);
$flags |= (1 == $bIndent ? 0x00000020 : 0); $flags |= (1 == $bIndent ? 0x00000020 : 0);
$flags |= (1 == $bShrinkToFit ? 0x00000040 : 0); $flags |= (1 == $bShrinkToFit ? 0x00000040 : 0);
// Default // Default
$flags |= (1 == 1 ? 0x00000080 : 0); $flags |= (1 == self::$always1 ? 0x00000080 : 0);
// Protection // Protection
$flags |= (1 == $bProtLocked ? 0x00000100 : 0); $flags |= (1 == $bProtLocked ? 0x00000100 : 0);
$flags |= (1 == $bProtHidden ? 0x00000200 : 0); $flags |= (1 == $bProtHidden ? 0x00000200 : 0);
@ -2936,13 +2942,13 @@ class Worksheet extends BIFFwriter
$flags |= (1 == $bBorderRight ? 0x00000800 : 0); $flags |= (1 == $bBorderRight ? 0x00000800 : 0);
$flags |= (1 == $bBorderTop ? 0x00001000 : 0); $flags |= (1 == $bBorderTop ? 0x00001000 : 0);
$flags |= (1 == $bBorderBottom ? 0x00002000 : 0); $flags |= (1 == $bBorderBottom ? 0x00002000 : 0);
$flags |= (1 == 1 ? 0x00004000 : 0); // Top left to Bottom right border $flags |= (1 == self::$always1 ? 0x00004000 : 0); // Top left to Bottom right border
$flags |= (1 == 1 ? 0x00008000 : 0); // Bottom left to Top right border $flags |= (1 == self::$always1 ? 0x00008000 : 0); // Bottom left to Top right border
// Pattern // Pattern
$flags |= (1 == $bFillStyle ? 0x00010000 : 0); $flags |= (1 == $bFillStyle ? 0x00010000 : 0);
$flags |= (1 == $bFillColor ? 0x00020000 : 0); $flags |= (1 == $bFillColor ? 0x00020000 : 0);
$flags |= (1 == $bFillColorBg ? 0x00040000 : 0); $flags |= (1 == $bFillColorBg ? 0x00040000 : 0);
$flags |= (1 == 1 ? 0x00380000 : 0); $flags |= (1 == self::$always1 ? 0x00380000 : 0);
// Font // Font
$flags |= (1 == $bFormatFont ? 0x04000000 : 0); $flags |= (1 == $bFormatFont ? 0x04000000 : 0);
// Alignment: // Alignment:
@ -2954,7 +2960,7 @@ class Worksheet extends BIFFwriter
// Protection // Protection
$flags |= (1 == $bFormatProt ? 0x40000000 : 0); $flags |= (1 == $bFormatProt ? 0x40000000 : 0);
// Text direction // Text direction
$flags |= (1 == 0 ? 0x80000000 : 0); $flags |= (1 == self::$always0 ? 0x80000000 : 0);
$dataBlockFont = null; $dataBlockFont = null;
$dataBlockAlign = null; $dataBlockAlign = null;
@ -3040,10 +3046,10 @@ class Worksheet extends BIFFwriter
$optionsFlags = 0; $optionsFlags = 0;
$optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0); $optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0);
$optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0); $optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0);
$optionsFlags |= (1 == 1 ? 0x00000008 : 0); $optionsFlags |= (1 == self::$always1 ? 0x00000008 : 0);
$optionsFlags |= (1 == 1 ? 0x00000010 : 0); $optionsFlags |= (1 == self::$always1 ? 0x00000010 : 0);
$optionsFlags |= (1 == 0 ? 0x00000020 : 0); $optionsFlags |= (1 == self::$always0 ? 0x00000020 : 0);
$optionsFlags |= (1 == 1 ? 0x00000080 : 0); $optionsFlags |= (1 == self::$always1 ? 0x00000080 : 0);
$dataBlockFont .= pack('V', $optionsFlags); $dataBlockFont .= pack('V', $optionsFlags);
// Escapement type // Escapement type
$dataBlockFont .= pack('V', $fontEscapement); $dataBlockFont .= pack('V', $fontEscapement);

View File

@ -21,15 +21,7 @@ class RangeTest extends TestCase
{ {
$this->spreadSheet = new Spreadsheet(); $this->spreadSheet = new Spreadsheet();
$this->spreadSheet->getActiveSheet() $this->spreadSheet->getActiveSheet()
->setCellValue('A1', 1) ->fromArray(array_chunk(range(1, 240), 6), null, 'A1', true);
->setCellValue('B1', 2)
->setCellValue('C1', 3)
->setCellValue('A2', 4)
->setCellValue('B2', 5)
->setCellValue('C2', 6)
->setCellValue('A3', 7)
->setCellValue('B3', 8)
->setCellValue('C3', 9);
} }
/** /**
@ -40,33 +32,39 @@ class RangeTest extends TestCase
public function testRangeEvaluation(string $formula, $expectedResult): void public function testRangeEvaluation(string $formula, $expectedResult): void
{ {
$workSheet = $this->spreadSheet->getActiveSheet(); $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); self::assertSame($expectedResult, $actualRresult);
} }
public function providerRangeEvaluation(): array public function providerRangeEvaluation(): array
{ {
return[ return[
['=SUM(A1:B3,A1:C2)', 48], 'Sum with Simple Range' => ['=SUM(A1:C3)', 72],
['=COUNT(A1:B3,A1:C2)', 12], 'Count with Simple Range' => ['=COUNT(A1:C3)', 9],
['=SUM(A1:B3 A1:C2)', 12], 'Sum with UNION #1' => ['=SUM(A1:B3,A1:C2)', 75],
['=COUNT(A1:B3 A1:C2)', 4], 'Count with UNION #1' => ['=COUNT(A1:B3,A1:C2)', 12],
['=SUM(A1:A3,C1:C3)', 30], 'Sum with INTERSECTION #1' => ['=SUM(A1:B3 A1:C2)', 18],
['=COUNT(A1:A3,C1:C3)', 6], 'Count with INTERSECTION #1' => ['=COUNT(A1:B3 A1:C2)', 4],
['=SUM(A1:A3 C1:C3)', Functions::null()], 'Sum with UNION #2' => ['=SUM(A1:A3,C1:C3)', 48],
['=COUNT(A1:A3 C1:C3)', 0], 'Count with UNION #2' => ['=COUNT(A1:A3,C1:C3)', 6],
['=SUM(A1:B2,B2:C3)', 40], 'Sum with INTERSECTION #2 - No Intersect' => ['=SUM(A1:A3 C1:C3)', Functions::null()],
['=COUNT(A1:B2,B2:C3)', 8], 'Count with INTERSECTION #2 - No Intersect' => ['=COUNT(A1:A3 C1:C3)', 0],
['=SUM(A1:B2 B2:C3)', 5], 'Sum with UNION #3' => ['=SUM(A1:B2,B2:C3)', 64],
['=COUNT(A1:B2 B2:C3)', 1], 'Count with UNION #3' => ['=COUNT(A1:B2,B2:C3)', 8],
['=SUM(A1:C1,A3:C3,B1:C3)', 63], 'Sum with INTERSECTION #3 - Single Cell' => ['=SUM(A1:B2 B2:C3)', 8],
['=COUNT(A1:C1,A3:C3,B1:C3)', 12], 'Count with INTERSECTION #3 - Single Cell' => ['=COUNT(A1:B2 B2:C3)', 1],
['=SUM(A1:C1,A3:C3 B1:C3)', 23], 'Sum with Triple UNION' => ['=SUM(A1:C1,A3:C3,B1:C3)', 99],
['=COUNT(A1:C1,A3:C3 B1:C3)', 5], 'Count with Triple UNION' => ['=COUNT(A1:C1,A3:C3,B1:C3)', 12],
['=SUM(Worksheet!A1:B3,Worksheet!A1:C2)', 48], 'Sum with UNION and INTERSECTION' => ['=SUM(A1:C1,A3:C3 B1:C3)', 35],
['=SUM(Worksheet!A1:Worksheet!B3,Worksheet!A1:Worksheet!C2)', 48], '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],
]; ];
} }
@ -82,31 +80,35 @@ class RangeTest extends TestCase
/** /**
* @dataProvider providerNamedRangeEvaluation * @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(); $workSheet = $this->spreadSheet->getActiveSheet();
$this->spreadSheet->addNamedRange(new NamedRange('GROUP1', $workSheet, $group1)); foreach ($ranges as $id => $range) {
$this->spreadSheet->addNamedRange(new NamedRange('GROUP2', $workSheet, $group2)); $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); self::assertSame($expectedResult, $sumRresult);
} }
public function providerNamedRangeEvaluation(): array public function providerNamedRangeEvaluation(): array
{ {
return[ 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'], '=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$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'], '=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], [['$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], [['$A$5', '$C$10:$C$20', '$F$1'], '=SUM(GROUP1:GROUP2:GROUP3)', 7260],
['Worksheet!$A$1:Worksheet!$B$2', 'Worksheet!$B$2:Worksheet!$C$3', '=SUM(GROUP1,GROUP2)', 40], [['$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],
]; ];
} }
@ -132,9 +134,9 @@ class RangeTest extends TestCase
public function providerUTF8NamedRangeEvaluation(): array public function providerUTF8NamedRangeEvaluation(): array
{ {
return[ 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', '$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],
]; ];
} }

View File

@ -0,0 +1,229 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\NamedRange;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class ParseFormulaTest extends TestCase
{
/**
* @dataProvider providerBinaryOperations
*/
public function testParseOperations(array $expectedStack, string $formula): void
{
$spreadsheet = new Spreadsheet();
$spreadsheet->addNamedRange(new NamedRange('GROUP1', $spreadsheet->getActiveSheet(), 'B2:D4'));
$spreadsheet->addNamedRange(new NamedRange('GROUP2', $spreadsheet->getActiveSheet(), 'D4:F6'));
$parser = Calculation::getInstance($spreadsheet);
$stack = $parser->parseFormula($formula);
self::assertSame($expectedStack, $stack);
}
public function providerBinaryOperations(): array
{
return [
'Unary negative with Value' => [
[
['type' => 'Value', 'value' => 3, 'reference' => null],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
'=-3',
],
'Unary negative percentage with Value' => [
[
['type' => 'Value', 'value' => 3, 'reference' => null],
['type' => 'Unary Operator', 'value' => '%', 'reference' => null],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
'=-3%',
],
'Binary minus with Values' => [
[
['type' => 'Value', 'value' => 3, 'reference' => null],
['type' => 'Value', 'value' => 4, 'reference' => null],
['type' => 'Binary Operator', 'value' => '-', 'reference' => null],
],
'=3-4',
],
'Unary negative with Cell Reference' => [
[
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
'=-A1',
],
'Unary negative with FQ Cell Reference' => [
[
['type' => 'Cell Reference', 'value' => "'Sheet 1'!A1", 'reference' => "'Sheet 1'!A1"],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
"=-'Sheet 1'!A1",
],
'Unary negative percentage with Cell Reference' => [
[
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
['type' => 'Unary Operator', 'value' => '%', 'reference' => null],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
'=-A1%',
],
'Unary negative with Defined Name' => [
[
['type' => 'Defined Name', 'value' => 'DEFINED_NAME', 'reference' => 'DEFINED_NAME'],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
'=-DEFINED_NAME',
],
'Unary negative percentage with Defined Name' => [
[
['type' => 'Defined Name', 'value' => 'DEFINED_NAME', 'reference' => 'DEFINED_NAME'],
['type' => 'Unary Operator', 'value' => '%', 'reference' => null],
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
],
'=-DEFINED_NAME%',
],
'Integer Numbers with Operator' => [
[
['type' => 'Value', 'value' => 2, 'reference' => null],
['type' => 'Value', 'value' => 3, 'reference' => null],
['type' => 'Binary Operator', 'value' => '*', 'reference' => null],
],
'=2*3',
],
'Float Numbers with Operator' => [
[
['type' => 'Value', 'value' => 2.5, 'reference' => null],
['type' => 'Value', 'value' => 3.5, 'reference' => null],
['type' => 'Binary Operator', 'value' => '*', 'reference' => null],
],
'=2.5*3.5',
],
'Strings with Operator' => [
[
['type' => 'Value', 'value' => '"HELLO"', 'reference' => null],
['type' => 'Value', 'value' => '"WORLD"', 'reference' => null],
['type' => 'Binary Operator', 'value' => '&', 'reference' => null],
],
'="HELLO"&"WORLD"',
],
'Error' => [
[
['type' => 'Value', 'value' => '#DIV0!', 'reference' => null],
],
'=#DIV0!',
],
'Cell Range' => [
[
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
],
'=A1:C3',
],
'Chained Cell Range' => [
[
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
['type' => 'Cell Reference', 'value' => 'E5', 'reference' => 'E5'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
],
'=A1:C3:E5',
],
'Cell Range Intersection' => [
[
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
['type' => 'Cell Reference', 'value' => 'B2', 'reference' => 'B2'],
['type' => 'Cell Reference', 'value' => 'D4', 'reference' => 'D4'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
['type' => 'Binary Operator', 'value' => '∩', 'reference' => null],
],
'=A1:C3 B2:D4',
],
'Row Range' => [
[
['type' => 'Row Reference', 'value' => 'A2', 'reference' => 'A2'],
['type' => 'Row Reference', 'value' => 'XFD3', 'reference' => 'XFD3'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
],
'=2:3',
],
'Column Range' => [
[
['type' => 'Column Reference', 'value' => 'B1', 'reference' => 'B1'],
['type' => 'Column Reference', 'value' => 'C1048576', 'reference' => 'C1048576'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
],
'=B:C',
],
'Range with Defined Names' => [
[
['type' => 'Defined Name', 'value' => 'GROUP1', 'reference' => 'GROUP1'],
['type' => 'Defined Name', 'value' => 'D4', 'reference' => 'GROUP2'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
['type' => 'Defined Name', 'value' => 'F6', 'reference' => 'GROUP2'],
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
],
'=GROUP1:GROUP2',
],
'Named Range with Binary Operator' => [
[
['type' => 'Defined Name', 'value' => 'DEFINED_NAME_1', 'reference' => 'DEFINED_NAME_1'],
['type' => 'Defined Name', 'value' => 'DEFINED_NAME_2', 'reference' => 'DEFINED_NAME_2'],
['type' => 'Binary Operator', 'value' => '/', 'reference' => null],
],
'=DEFINED_NAME_1/DEFINED_NAME_2',
],
'Named Range Intersection' => [
[
['type' => 'Defined Name', 'value' => 'DEFINED_NAME_1', 'reference' => 'DEFINED_NAME_1'],
['type' => 'Defined Name', 'value' => 'DEFINED_NAME_2', 'reference' => 'DEFINED_NAME_2'],
['type' => 'Binary Operator', 'value' => '∩', 'reference' => null],
],
'=DEFINED_NAME_1 DEFINED_NAME_2',
],
// 'Structured Reference Arithmetic' => [
// [
// ['type' => 'Structured Reference', 'value' => '[@Quantity]', 'reference' => null],
// ['type' => 'Structured Reference', 'value' => '[@[Unit Price]]', 'reference' => null],
// ['type' => 'Binary Operator', 'value' => '*', 'reference' => null],
// ],
// '=[@Quantity]*[@[Unit Price]]',
// ],
// 'Structured Reference Intersection' => [
// [
// ['type' => 'Structured Reference', 'value' => 'DeptSales[[Sales Person]:[Sales Amount]]', 'reference' => null],
// ['type' => 'Structured Reference', 'value' => 'DeptSales[[Region]:[% Commission]]', 'reference' => null],
// ['type' => 'Binary Operator', 'value' => '∩', 'reference' => null],
// ],
// '=DeptSales[[Sales Person]:[Sales Amount]] DeptSales[[Region]:[% Commission]]',
// ],
// 'Cell Range Union' => [
// [
// ['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
// ['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
// ['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
// ['type' => 'Cell Reference', 'value' => 'B2', 'reference' => 'B2'],
// ['type' => 'Cell Reference', 'value' => 'D4', 'reference' => 'D4'],
// ['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
// ['type' => 'Binary Operator', 'value' => '', 'reference' => null],
// ],
// '=A1:C3,B2:D4',
// ],
// 'Named Range Union' => [
// [
// ['type' => 'Defined Name', 'value' => 'DEFINED_NAME_1', 'reference' => 'DEFINED_NAME_1'],
// ['type' => 'Defined Name', 'value' => 'DEFINED_NAME_2', 'reference' => 'DEFINED_NAME_2'],
// ['type' => 'Binary Operator', 'value' => '', 'reference' => null],
// ],
// '=DEFINED_NAME_1,DEFINED_NAME_2',
// ],
];
}
}

View File

@ -0,0 +1,246 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Cell;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class CellAddressTest extends TestCase
{
/**
* @dataProvider providerCreateFromCellAddress
*/
public function testCreateFromCellAddress(
string $cellAddress,
string $expectedColumnName,
int $expectedColumnId,
int $expectedRowId
): void {
$cellAddressObject = CellAddress::fromCellAddress($cellAddress);
self::assertSame($cellAddress, (string) $cellAddressObject);
self::assertSame($cellAddress, $cellAddressObject->cellAddress());
self::assertSame($expectedRowId, $cellAddressObject->rowId());
self::assertSame($expectedColumnId, $cellAddressObject->columnId());
self::assertSame($expectedColumnName, $cellAddressObject->columnName());
}
public function providerCreateFromCellAddress(): array
{
return [
['A1', 'A', 1, 1],
['C5', 'C', 3, 5],
['IV256', 'IV', 256, 256],
];
}
/**
* @dataProvider providerCreateFromCellAddressException
*
* @param mixed $cellAddress
*/
public function testCreateFromCellAddressException($cellAddress): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage(
$cellAddress === ''
? 'Cell coordinate can not be zero-length string'
: "Invalid cell coordinate {$cellAddress}"
);
CellAddress::fromCellAddress($cellAddress);
}
public function providerCreateFromCellAddressException(): array
{
return [
['INVALID'],
[''],
['IV'],
['12'],
[123],
];
}
/**
* @dataProvider providerCreateFromColumnAndRow
*/
public function testCreateFromColumnAndRow(
int $columnId,
int $rowId,
string $expectedCellAddress,
string $expectedColumnName
): void {
$cellAddressObject = CellAddress::fromColumnAndRow($columnId, $rowId);
self::assertSame($expectedCellAddress, (string) $cellAddressObject);
self::assertSame($expectedCellAddress, $cellAddressObject->cellAddress());
self::assertSame($rowId, $cellAddressObject->rowId());
self::assertSame($columnId, $cellAddressObject->columnId());
self::assertSame($expectedColumnName, $cellAddressObject->columnName());
}
/**
* @dataProvider providerCreateFromColumnRowException
*
* @param mixed $columnId
* @param mixed $rowId
*/
public function testCreateFromColumnRowException($columnId, $rowId): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('Row and Column Ids must be positive integer values');
CellAddress::fromColumnAndRow($columnId, $rowId);
}
public function providerCreateFromColumnAndRow(): array
{
return [
[1, 1, 'A1', 'A'],
[3, 5, 'C5', 'C'],
[256, 256, 'IV256', 'IV'],
];
}
/**
* @dataProvider providerCreateFromColumnRowArray
*/
public function testCreateFromColumnRowArray(
int $columnId,
int $rowId,
string $expectedCellAddress,
string $expectedColumnName
): void {
$columnRowArray = [$columnId, $rowId];
$cellAddressObject = CellAddress::fromColumnRowArray($columnRowArray);
self::assertSame($expectedCellAddress, (string) $cellAddressObject);
self::assertSame($expectedCellAddress, $cellAddressObject->cellAddress());
self::assertSame($rowId, $cellAddressObject->rowId());
self::assertSame($columnId, $cellAddressObject->columnId());
self::assertSame($expectedColumnName, $cellAddressObject->columnName());
}
public function providerCreateFromColumnRowArray(): array
{
return [
[1, 1, 'A1', 'A'],
[3, 5, 'C5', 'C'],
[256, 256, 'IV256', 'IV'],
];
}
/**
* @dataProvider providerCreateFromColumnRowException
*
* @param mixed $columnId
* @param mixed $rowId
*/
public function testCreateFromColumnRowArrayException($columnId, $rowId): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('Row and Column Ids must be positive integer values');
$columnRowArray = [$columnId, $rowId];
CellAddress::fromColumnRowArray($columnRowArray);
}
public function providerCreateFromColumnRowException(): array
{
return [
[-1, 1],
[3, 'A'],
];
}
/**
* @dataProvider providerCreateFromCellAddressWithWorksheet
*/
public function testCreateFromCellAddressWithWorksheet(
string $cellAddress,
string $expectedCellAddress,
string $expectedColumnName,
int $expectedColumnId,
int $expectedRowId
): void {
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$cellAddressObject = CellAddress::fromCellAddress($cellAddress, $worksheet);
self::assertSame($expectedCellAddress, (string) $cellAddressObject);
self::assertSame($cellAddress, $cellAddressObject->cellAddress());
self::assertSame($expectedRowId, $cellAddressObject->rowId());
self::assertSame($expectedColumnId, $cellAddressObject->columnId());
self::assertSame($expectedColumnName, $cellAddressObject->columnName());
}
public function providerCreateFromCellAddressWithWorksheet(): array
{
return [
['A1', "'Mark''s Worksheet'!A1", 'A', 1, 1],
['C5', "'Mark''s Worksheet'!C5", 'C', 3, 5],
['IV256', "'Mark''s Worksheet'!IV256", 'IV', 256, 256],
];
}
public function testNextRow(): void
{
$cellAddress = CellAddress::fromCellAddress('C5');
// default single row
$cellAddressC6 = $cellAddress->nextRow();
self::assertSame('C6', (string) $cellAddressC6);
// multiple rows
$cellAddressC9 = $cellAddress->nextRow(4);
self::assertSame('C9', (string) $cellAddressC9);
// negative rows
$cellAddressC3 = $cellAddress->nextRow(-2);
self::assertSame('C3', (string) $cellAddressC3);
// negative beyond the minimum
$cellAddressC1 = $cellAddress->nextRow(-10);
self::assertSame('C1', (string) $cellAddressC1);
// Check that the original object is still unchanged
self::assertSame('C5', (string) $cellAddress);
}
public function testPreviousRow(): void
{
$cellAddress = CellAddress::fromCellAddress('C5');
// default single row
$cellAddressC4 = $cellAddress->previousRow();
self::assertSame('C4', (string) $cellAddressC4);
}
public function testNextColumn(): void
{
$cellAddress = CellAddress::fromCellAddress('C5');
// default single row
$cellAddressD5 = $cellAddress->nextColumn();
self::assertSame('D5', (string) $cellAddressD5);
// multiple rows
$cellAddressG5 = $cellAddress->nextColumn(4);
self::assertSame('G5', (string) $cellAddressG5);
// negative rows
$cellAddressB5 = $cellAddress->nextColumn(-1);
self::assertSame('B5', (string) $cellAddressB5);
// negative beyond the minimum
$cellAddressA5 = $cellAddress->nextColumn(-10);
self::assertSame('A5', (string) $cellAddressA5);
// Check that the original object is still unchanged
self::assertSame('C5', (string) $cellAddress);
}
public function testPreviousColumn(): void
{
$cellAddress = CellAddress::fromCellAddress('C5');
// default single row
$cellAddressC4 = $cellAddress->previousColumn();
self::assertSame('B5', (string) $cellAddressC4);
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Cell;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;
class CellRangeTest extends TestCase
{
public function testCreateCellRange(): void
{
$from = CellAddress::fromCellAddress('B5');
$to = CellAddress::fromCellAddress('E2');
$cellRange = new CellRange($from, $to);
self::assertSame('B2:E5', (string) $cellRange);
}
public function testCreateCellRangeWithWorksheet(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$from = CellAddress::fromCellAddress('B5', $worksheet);
$to = CellAddress::fromCellAddress('E2');
$cellRange = new CellRange($from, $to);
self::assertSame("'Mark''s Worksheet'!B2:E5", (string) $cellRange);
}
public function testCreateCellRangeWithWorksheets(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$from = CellAddress::fromCellAddress('B5', $worksheet);
$to = CellAddress::fromCellAddress('E2', $worksheet);
$cellRange = new CellRange($from, $to);
self::assertSame("'Mark''s Worksheet'!B2:E5", (string) $cellRange);
}
public function testSingleCellRange(): void
{
$from = CellAddress::fromCellAddress('C3');
$to = CellAddress::fromCellAddress('C3');
$cellRange = new CellRange($from, $to);
self::assertSame('C3', (string) $cellRange);
}
public function testSingleCellRangeWithWorksheet(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$from = CellAddress::fromCellAddress('C3', $worksheet);
$to = CellAddress::fromCellAddress('C3');
$cellRange = new CellRange($from, $to);
self::assertSame("'Mark''s Worksheet'!C3", (string) $cellRange);
}
public function testRangeFrom(): void
{
$from = CellAddress::fromCellAddress('B5');
$to = CellAddress::fromCellAddress('E2');
$cellRange = new CellRange($from, $to);
self::assertSame('B2', (string) $cellRange->from());
}
public function testRangeTo(): void
{
$from = CellAddress::fromCellAddress('B5');
$to = CellAddress::fromCellAddress('E2');
$cellRange = new CellRange($from, $to);
self::assertSame('E5', (string) $cellRange->to());
}
public function testCreateCellRangeWithMismatchedWorksheets(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$secondWorksheet = new Worksheet($spreadsheet, 'A Second Worksheet');
$this->expectException(Exception::class);
$this->expectExceptionMessage('3d Cell Ranges are not supported');
$from = CellAddress::fromCellAddress('B5', $worksheet);
$to = CellAddress::fromCellAddress('E2', $secondWorksheet);
new CellRange($from, $to);
}
public function testCreateCellRangeWithMismatchedSpreadsheets(): void
{
$spreadsheet1 = new Spreadsheet();
$worksheet1 = $spreadsheet1->getActiveSheet();
$worksheet1->setTitle("Mark's Worksheet");
$spreadsheet2 = new Spreadsheet();
$worksheet2 = $spreadsheet2->getActiveSheet();
$worksheet2->setTitle("Mark's Worksheet");
$this->expectException(Exception::class);
$this->expectExceptionMessage('Worksheets must be in the same spreadsheet');
$from = CellAddress::fromCellAddress('B5', $worksheet1);
$to = CellAddress::fromCellAddress('E2', $worksheet2);
new CellRange($from, $to);
}
public function testShiftRangeTo(): void
{
$from = CellAddress::fromCellAddress('B5');
$to = CellAddress::fromCellAddress('E2');
$cellRange = new CellRange($from, $to);
self::assertSame('B2:E5', (string) $cellRange);
$cellRange->to()
->nextColumn(2)
->nextRow(2);
self::assertSame('B2', (string) $cellRange->from());
self::assertSame('G7', (string) $cellRange->to());
self::assertSame('B2:G7', (string) $cellRange);
$cellRange->to()
->previousColumn()
->previousRow();
self::assertSame('B2', (string) $cellRange->from());
self::assertSame('F6', (string) $cellRange->to());
self::assertSame('B2:F6', (string) $cellRange);
}
public function testShiftRangeFrom(): void
{
$from = CellAddress::fromCellAddress('B5');
$to = CellAddress::fromCellAddress('E2');
$cellRange = new CellRange($from, $to);
self::assertSame('B2:E5', (string) $cellRange);
$cellRange->from()
->nextColumn(5)
->nextRow(5);
self::assertSame('E5', (string) $cellRange->from());
self::assertSame('G7', (string) $cellRange->to());
self::assertSame('E5:G7', (string) $cellRange);
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Cell;
use PhpOffice\PhpSpreadsheet\Cell\ColumnRange;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class ColumnRangeTest extends TestCase
{
public function testCreateColumnRange(): void
{
$columnRange = new ColumnRange('C', 'E');
self::assertSame('C', $columnRange->from());
self::assertSame('E', $columnRange->to());
self::assertSame(3, $columnRange->fromIndex());
self::assertSame(5, $columnRange->toIndex());
self::assertSame('C:E', (string) $columnRange);
self::assertSame(3, $columnRange->columnCount());
self::assertSame('C1:E1048576', (string) $columnRange->toCellRange());
}
public function testCreateSingleColumnRange(): void
{
$columnRange = new ColumnRange('E');
self::assertSame('E', $columnRange->from());
self::assertSame('E', $columnRange->to());
self::assertSame('E:E', (string) $columnRange);
self::assertSame(1, $columnRange->columnCount());
self::assertSame('E1:E1048576', (string) $columnRange->toCellRange());
}
public function testCreateColumnRangeWithWorksheet(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$columnRange = new ColumnRange('C', 'E', $worksheet);
self::assertSame('C', $columnRange->from());
self::assertSame('E', $columnRange->to());
self::assertSame("'Mark''s Worksheet'!C:E", (string) $columnRange);
self::assertSame("'Mark''s Worksheet'!C1:E1048576", (string) $columnRange->toCellRange());
}
public function testCreateColumnRangeFromArray(): void
{
$columnRange = ColumnRange::fromArray(['C', 'E']);
self::assertSame('C', $columnRange->from());
self::assertSame('E', $columnRange->to());
self::assertSame('C:E', (string) $columnRange);
self::assertSame(3, $columnRange->columnCount());
self::assertSame('C1:E1048576', (string) $columnRange->toCellRange());
}
public function testCreateColumnRangeFromIndexes(): void
{
$columnRange = ColumnRange::fromColumnIndexes(3, 5);
self::assertSame('C', $columnRange->from());
self::assertSame('E', $columnRange->to());
self::assertSame('C:E', (string) $columnRange);
self::assertSame(3, $columnRange->columnCount());
self::assertSame('C1:E1048576', (string) $columnRange->toCellRange());
}
public function testColumnRangeNext(): void
{
$columnRange = new ColumnRange('C', 'E');
$columnRangeNext = $columnRange->shiftDown(3);
self::assertSame('F', $columnRangeNext->from());
self::assertSame('H', $columnRangeNext->to());
// Check that original Column Range isn't changed
self::assertSame('C:E', (string) $columnRange);
}
public function testColumnRangePrevious(): void
{
$columnRange = new ColumnRange('C', 'E');
$columnRangeNext = $columnRange->shiftUp();
self::assertSame('B', $columnRangeNext->from());
self::assertSame('D', $columnRangeNext->to());
// Check that original Column Range isn't changed
self::assertSame('C:E', (string) $columnRange);
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Cell;
use PhpOffice\PhpSpreadsheet\Cell\RowRange;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class RowRangeTest extends TestCase
{
public function testCreateRowRange(): void
{
$rowRange = new RowRange(3, 5);
self::assertSame(3, $rowRange->from());
self::assertSame(5, $rowRange->to());
self::assertSame('3:5', (string) $rowRange);
self::assertSame(3, $rowRange->rowCount());
self::assertSame('A3:XFD5', (string) $rowRange->toCellRange());
}
public function testCreateSingleRowRange(): void
{
$rowRange = new RowRange(3);
self::assertSame(3, $rowRange->from());
self::assertSame(3, $rowRange->to());
self::assertSame('3:3', (string) $rowRange);
self::assertSame(1, $rowRange->rowCount());
}
public function testCreateRowRangeWithWorksheet(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle("Mark's Worksheet");
$rowRange = new RowRange(3, 5, $worksheet);
self::assertSame(3, $rowRange->from());
self::assertSame(5, $rowRange->to());
self::assertSame("'Mark''s Worksheet'!3:5", (string) $rowRange);
}
public function testCreateRowRangeFromArray(): void
{
$rowRange = RowRange::fromArray([3, 5]);
self::assertSame(3, $rowRange->from());
self::assertSame(5, $rowRange->to());
self::assertSame('3:5', (string) $rowRange);
self::assertSame(3, $rowRange->rowCount());
self::assertSame('A3:XFD5', (string) $rowRange->toCellRange());
}
public function testRowRangeNext(): void
{
$rowRange = new RowRange(3, 5);
$rowRangeNext = $rowRange->shiftRight(3);
self::assertSame(6, $rowRangeNext->from());
self::assertSame(8, $rowRangeNext->to());
// Check that original Row Range isn't changed
self::assertSame('3:5', (string) $rowRange);
}
public function testRowRangePrevious(): void
{
$rowRange = new RowRange(3, 5);
$rowRangeNext = $rowRange->shiftLeft();
self::assertSame(2, $rowRangeNext->from());
self::assertSame(4, $rowRangeNext->to());
// Check that original Row Range isn't changed
self::assertSame('3:5', (string) $rowRange);
}
}

View File

@ -188,4 +188,62 @@ class SpreadsheetTest extends TestCase
$sheet->getCell('A1')->getStyle()->getFont()->setBold(true); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true);
$this->object->addExternalSheet($sheet); $this->object->addExternalSheet($sheet);
} }
public function testAddExternalColumnDimensionStyles(): void
{
$spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension');
$sheet1->getCell('A1')->setValue(1);
$sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true);
$sheet1->getColumnDimension('B')->setWidth(10)->setXfIndex($sheet1->getCell('A1')->getXfIndex());
$index = $sheet1->getColumnDimension('B')->getXfIndex();
self::assertEquals(1, $index);
self::assertCount(2, $spreadsheet1->getCellXfCollection());
$spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles');
$sheet2->getCell('A1')->setValue(1);
$sheet2->getCell('A1')->getStyle()->getFont()->setBold(true);
$sheet2->getCell('B2')->getStyle()->getFont()->setSuperscript(true);
$countXfs = count($spreadsheet2->getCellXfCollection());
self::assertEquals(3, $countXfs);
$sheet3 = $spreadsheet2->addExternalSheet($sheet1);
self::assertCount(5, $spreadsheet2->getCellXfCollection());
self::assertTrue($sheet3->getCell('A1')->getStyle()->getFont()->getItalic());
self::assertTrue($sheet3->getCell('B1')->getStyle()->getFont()->getItalic());
self::assertFalse($sheet3->getCell('B1')->getStyle()->getFont()->getBold());
// Prove Xf index changed although style is same.
self::assertEquals($countXfs + $index, $sheet3->getCell('B1')->getXfIndex());
self::assertEquals($countXfs + $index, $sheet3->getColumnDimension('B')->getXfIndex());
}
public function testAddExternalRowDimensionStyles(): void
{
$spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension');
$sheet1->getCell('A1')->setValue(1);
$sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true);
$sheet1->getRowDimension(2)->setXfIndex($sheet1->getCell('A1')->getXfIndex());
$index = $sheet1->getRowDimension(2)->getXfIndex();
self::assertEquals(1, $index);
self::assertCount(2, $spreadsheet1->getCellXfCollection());
$spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles');
$sheet2->getCell('A1')->setValue(1);
$sheet2->getCell('A1')->getStyle()->getFont()->setBold(true);
$sheet2->getCell('B2')->getStyle()->getFont()->setSuperscript(true);
$countXfs = count($spreadsheet2->getCellXfCollection());
self::assertEquals(3, $countXfs);
$sheet3 = $spreadsheet2->addExternalSheet($sheet1);
self::assertCount(5, $spreadsheet2->getCellXfCollection());
self::assertTrue($sheet3->getCell('A1')->getStyle()->getFont()->getItalic());
self::assertTrue($sheet3->getCell('A2')->getStyle()->getFont()->getItalic());
self::assertFalse($sheet3->getCell('A2')->getStyle()->getFont()->getBold());
// Prove Xf index changed although style is same.
self::assertEquals($countXfs + $index, $sheet3->getCell('A2')->getXfIndex());
self::assertEquals($countXfs + $index, $sheet3->getRowDimension(2)->getXfIndex());
}
} }

View File

@ -0,0 +1,188 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Comment;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;
class ByColumnAndRowTest extends TestCase
{
public function testSetCellValueByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValueByColumnAndRow(2, 2, 2);
self::assertSame(2, $sheet->getCell('B2')->getValue());
}
public function testSetCellValueExplicitByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValueExplicitByColumnAndRow(2, 2, '="PHP Rules"', DataType::TYPE_STRING);
self::assertSame('="PHP Rules"', $sheet->getCell('B2')->getValue());
self::assertSame(DataType::TYPE_STRING, $sheet->getCell('B2')->getDataType());
}
public function testCellExistsByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$cellExists = $sheet->cellExistsByColumnAndRow(2, 2);
self::assertFalse($cellExists);
$sheet->setCellValue('B2', 2);
$cellExists = $sheet->cellExistsByColumnAndRow(2, 2);
self::assertTrue($cellExists);
}
public function testGetCellByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('B2', 2);
$cell = $sheet->getCellByColumnAndRow(2, 2);
self::assertSame('B2', $cell->getCoordinate());
self::assertSame(2, $cell->getValue());
}
public function testGetStyleByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->getStyle('B2:C3')->getFont()->setBold(true);
$rangeStyle = $sheet->getStyleByColumnAndRow(2, 2, 3, 3);
self::assertTrue($rangeStyle->getFont()->getBold());
$cellStyle = $sheet->getStyleByColumnAndRow(2, 2);
self::assertTrue($cellStyle->getFont()->getBold());
}
public function testSetBreakByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('B2', 2);
$sheet->setBreakByColumnAndRow(2, 2, Worksheet::BREAK_COLUMN);
$breaks = $sheet->getBreaks();
self::assertArrayHasKey('B2', $breaks);
self::assertSame(Worksheet::BREAK_COLUMN, $breaks['B2']);
}
public function testMergeCellsByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->mergeCellsByColumnAndRow(2, 2, 3, 3);
$mergeRanges = $sheet->getMergeCells();
self::assertArrayHasKey('B2:C3', $mergeRanges);
}
public function testUnergeCellsByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->mergeCells('B2:C3');
$mergeRanges = $sheet->getMergeCells();
self::assertArrayHasKey('B2:C3', $mergeRanges);
$sheet->unmergeCellsByColumnAndRow(2, 2, 3, 3);
$mergeRanges = $sheet->getMergeCells();
self::assertEmpty($mergeRanges);
}
public function testProtectCellsByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->protectCellsByColumnAndRow(2, 2, 3, 3, 'secret', false);
$protectedRanges = $sheet->getProtectedCells();
self::assertArrayHasKey('B2:C3', $protectedRanges);
}
public function testUnprotectCellsByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->protectCells('B2:C3', 'secret', false);
$protectedRanges = $sheet->getProtectedCells();
self::assertArrayHasKey('B2:C3', $protectedRanges);
$sheet->unprotectCellsByColumnAndRow(2, 2, 3, 3);
$protectedRanges = $sheet->getProtectedCells();
self::assertEmpty($protectedRanges);
}
public function testSetAutoFilterByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->setAutoFilterByColumnAndRow(2, 2, 3, 3);
$autoFilter = $sheet->getAutoFilter();
self::assertInstanceOf(AutoFilter::class, $autoFilter);
self::assertSame('B2:C3', $autoFilter->getRange());
}
public function testFreezePaneByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$data = [['A', 'B'], ['C', 'D']];
$sheet->fromArray($data, null, 'B2', true);
$sheet->freezePaneByColumnAndRow(2, 2);
$freezePane = $sheet->getFreezePane();
self::assertSame('B2', $freezePane);
}
public function testGetCommentByColumnAndRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('B2', 2);
$spreadsheet->getActiveSheet()
->getComment('B2')
->getText()->createTextRun('My Test Comment');
$comment = $sheet->getCommentByColumnAndRow(2, 2);
self::assertInstanceOf(Comment::class, $comment);
self::assertSame('My Test Comment', $comment->getText()->getPlainText());
}
}