From ef2b5b9e001bc239efedf4578b5f71c14b0ff2ae Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 21 Jun 2024 21:55:05 -0700 Subject: [PATCH] Mostly Docs and Tests Also support for Xml format. --- docs/topics/recipes.md | 76 ++++++------- .../Internal/ExcelArrayPseudoFunctions.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 8 ++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 20 ++++ .../Calculation/InternalFunctionsTest.php | 4 + .../Functional/ArrayFunctionsSpillTest.php | 48 +++++++++ .../Reader/Gnumeric/ArrayFormulaTest.php | 20 +++- .../Reader/Ods/ArrayFormulaTest.php | 101 ++++++++++++++++++ .../Reader/Xml/ArrayFormulaTest.php | 26 +++++ .../Writer/Ods/ArrayTest.php | 16 ++- .../Writer/Xlsx/ArrayFunctionsTest.php | 66 +++++++++++- tests/data/Reader/Ods/ArrayFormulaTest.ods | Bin 0 -> 14534 bytes tests/data/Reader/Xml/ArrayFormula.xml | 75 +++++++++++++ 13 files changed, 410 insertions(+), 52 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php create mode 100644 tests/data/Reader/Ods/ArrayFormulaTest.ods create mode 100644 tests/data/Reader/Xml/ArrayFormula.xml diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index a1cfebc5e..5344d95a7 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -376,7 +376,7 @@ $value = $spreadsheet->getActiveSheet()->getCell('B8')->getCalculatedValue(); ### Array Formulas With version 2.0.3 of PhpSpreadsheet, we've introduced support for Excel "array formulas". -It is an opt-in feature. You need to enable it with the following code: +**It is an opt-in feature.** You need to enable it with the following code: ```php \PhpOffice\PhpSpreadsheet\Calculation\Calculation::setArrayReturnType( \PhpOffice\PhpSpreadsheet\Calculation\Calculation::RETURN_ARRAY_AS_ARRAY); @@ -405,15 +405,16 @@ However, we could have specified an alternative formula to calculate that result ![12-CalculationEngine-Array-Formula.png](./images/12-CalculationEngine-Array-Formula.png) -Entering the formula `=SUM(B2:B6*C2:C6)` will calculate the same result; but because it's using arrays, we need to enter it as an "array formula". In MS Excel itself, we'd do this by using `Shift-Ctrl-Enter` rather than simply `Enter` when we define the formula in the formula edit box. MS Excel then shows that this is an array formula in the formula edit box by wrapping it in the `{}` braces (you don't enter these in the formula yourself; MS Excel does it). -In recent releases of Excel, Shift-Ctrl-Enter is not required, and Excel does not add the braces. -PhpSpreadsheet will attempt to behave like the recent releases. +Entering the formula `=SUM(B2:B6*C2:C6)` will calculate the same result; but because it's using arrays, we need to enter it as an "array formula". In MS Excel itself, we'd do this by using `Ctrl-Shift-Enter` rather than simply `Enter` when we define the formula in the formula edit box. MS Excel then shows that this is an array formula in the formula edit box by wrapping it in the `{}` braces (you don't enter these in the formula yourself; MS Excel does it). + +**In recent releases of Excel, Ctrl-Shift-Enter is not required, and Excel does not add the braces. +PhpSpreadsheet will attempt to behave like the recent releases.** Or to identify the biggest increase in like-for-like sales from one month to the next: ![12-CalculationEngine-Array-Formula-3.png](./images/12-CalculationEngine-Array-Formula-3.png) ```php -$spreadsheet->getActiveSheet()->setCellValue('F1','=MAX(B2:B6-C2:C6)', true); +$spreadsheet->getActiveSheet()->setCellValue('F1','=MAX(B2:B6-C2:C6)'); ``` Which tells us that the biggest increase in sales between December and January was 30 more (in this case, 30 more Lemons). @@ -424,8 +425,8 @@ As an example, consider transposing a grid of data: MS Excel provides the `TRANS ![12-CalculationEngine-Array-Formula-2.png](./images/12-CalculationEngine-Array-Formula-2.png) -When we do this in MS Excel, we need to indicate ___all___ the cells that will contain the transposed data from cells `A1` to `D7`. We do this by selecting the cells where we want to display our transposed data either by holding the left mouse button down while we move with the mouse, or pressing `Shift` and using the arrow keys. -Once we've selected all the cells to hold our data, then we enter the formula `TRANSPOSE(A1:D7)` in the formula edit box, remembering to use `Shift-Ctrl-Enter` to tell MS Excel that this is an array formula. +When we do this in MS Excel, we used to need to indicate ___all___ the cells that will contain the transposed data from cells `A1` to `D7`. We do this by selecting the cells where we want to display our transposed data either by holding the left mouse button down while we move with the mouse, or pressing `Shift` and using the arrow keys. +Once we've selected all the cells to hold our data, then we enter the formula `TRANSPOSE(A1:D7)` in the formula edit box, remembering to use `Ctrl-Shift-Enter` to tell MS Excel that this is an array formula. In recent Excel, you can just enter `=TRANSPOSE(A1:D7)` into cell A10. Note also that we still set this as the formula for the top-left cell of that range, cell `A10`. @@ -434,66 +435,40 @@ Simply setting an array formula in a cell and specifying the range won't populat $spreadsheet->getActiveSheet() ->setCellValue( 'A10', - '=SEQUENCE(3,3)', - true, - 'A1:C3' + '=SEQUENCE(3,3)' ); - // Will return a null, because the formula for A1 hasn't been calculated to populate the spillage area $result = $spreadsheet->getActiveSheet()->getCell('C3')->getValue(); ``` To do that, we need to retrieve the calculated value for the cell. ```php -$spreadsheet->getActiveSheet() - ->setCellValue( - 'A10', - '=SEQUENCE(3,3)', - true, - 'A1:C3' - ); - $spreadsheet->getActiveSheet()->getCell('A1')->getCalculatedValue(); - // Will return 9, because the formula for A1 has now been calculated, and the spillage area is populated $result = $spreadsheet->getActiveSheet()->getCell('C3')->getValue(); ``` -When we call `getCalculatedValue()` for a cell that contains an array formula, PhpSpreadsheet returns the single value that would appear in that cell in MS Excel. +If returning arrays has been enabled, `getCalculatedValue` will return an array when appropriate, and will populate the spill range. If returning arrays has not been enabled, when we call `getCalculatedValue()` for a cell that contains an array formula, PhpSpreadsheet will return the single value from the topmost leftmost cell, and will leave other cells unchanged. ```php // Will return integer 1, the value for that cell within the array $a1result = $spreadsheet->getActiveSheet()->getCell('A1')->getCalculatedValue(); ``` -If we want to return the full array, then we need to call `getCalculatedValue()` with an additional argument, a boolean `true` to return the value as an array. -```php -// Will return an array [[1, 2, 3], [4, 5, 6], [7, 8, 9]] -$a1result = $spreadsheet->getActiveSheet()->getCell('A1')->getCalculatedValue(true); -``` - --- Excel365 introduced a number of new functions that return arrays of results. These include the `UNIQUE()`, `SORT()`, `SORTBY()`, `FILTER()`, `SEQUENCE()` and `RANDARRAY()` functions. While not all of these have been implemented by the Calculation Engine in PhpSpreadsheet, so they cannot all be calculated within your PHP applications, they can still be read from and written to Xlsx files. -The way these functions are presented in MS Excel itself is slightly different to that of other array functions. - The `SEQUENCE()` function generates a series of values (in this case, starting with `-10` and increasing in steps of `2.5`); and here we're telling the formula to populate a 3x3 grid with these values. ![12-CalculationEngine-Spillage-Formula.png](./images/12-CalculationEngine-Spillage-Formula.png) -Note that this is visually different to the multi-cell array formulas like `TRANSPOSE()`. When we are positioned in the "spill" range for the grid, MS Excel highlights the area with a blue border; and the formula displayed in the formula editing field isn't wrapped in braces (`{}`). +Note that this is visually different from using `Ctrl-Shift-Enter` for the formula. When we are positioned in the "spill" range for the grid, MS Excel highlights the area with a blue border; and the formula displayed in the formula editing field isn't wrapped in braces (`{}`). And if we select any other cell inside the "spill" area other than the top-left cell, the formula in the formula edit field is greyed rather than displayed in black. ![12-CalculationEngine-Spillage-Formula-2.png](./images/12-CalculationEngine-Spillage-Formula-2.png) -When we enter this formula in MS Excel, we don't need to select the range of cells that it should occupy; nor do we need to enter it using `Ctrl-Shift-Enter`. MS Excel identifies that it is a multi-cell array formula because of the function that it uses, the `SEQUENCE()` function (and if there are nested function calls in the formula, then it must be the outermost functionin the tree). - -However, PhpSpreadsheet isn't quite as intelligent (yet) and doesn't parse the formula to identify if it should be treated as an array formula or not; a formula is just a string of characters until it is actually evaluated. If we want to use this function through code, we still need to specify that it is an "array" function with the `$isArrayFormula` argument, and the range of cells that it should cover. - -```php -$spreadsheet->getActiveSheet()->setCellValue('A1','=SEQUENCE(3,3,-10,2.5)', true, 'A1:C3'); -``` +When we enter this formula in MS Excel, we don't need to select the range of cells that it should occupy; nor do we need to enter it using `Ctrl-Shift-Enter`. ### The Spill Operator @@ -502,19 +477,34 @@ To simplify this, MS Excel has introduced the "Spill" Operator (`#`). ![12-CalculationEngine-Spillage-Operator.png](./images/12-CalculationEngine-Spillage-Operator.png) -Using our `SEQUENCE()"`example, where the formula cell is `A1` and the result spills across the range `A1:C3`, we can use the Spill operator `A1#` to reference all the cells in that spillage range. +Using our `SEQUENCE()`example, where the formula cell is `A1` and the result spills across the range `A1:C3`, we can use the Spill operator `A1#` to reference all the cells in that spillage range. In this case, we're taking the absolute value of each cell in that range, and adding them together using the `SUM()` function to give us a result of 50. -PhpSpreadsheet doesn't currently support entry of a formula like this directly; but interally MS Excel implements the Spill Operator as a function (`ANCHORARRAY()`). MS Excel itself doesn't allow you to use this function in a formula, you have to use the "Spill" operator; but PhpSpreadsheet does allow you to use this internal Excel function. +PhpSpreadsheet supports entry of a formula like this using the Spill operator. Alternatively, MS Excel internally implements the Spill Operator as a function (`ANCHORARRAY()`). MS Excel itself doesn't allow you to use this function in a formula, you have to use the "Spill" operator; but PhpSpreadsheet does allow you to use this internal Excel function. PhpSpreadsheet will convert the spill operator to ANCHORARRAY on write (so it may appear that your formula has changed, but it hasn't really); it is not necessary to convert it back on read. To create this same function in PhpSpreadsheet, use: ```php -$spreadsheet->getActiveSheet()->setCellValue('D1','=SUM(ABS(ANCHORARRAY(A1)))', true); +$spreadsheet->getActiveSheet()->setCellValue('D1','=SUM(ABS(ANCHORARRAY(A1)))'); ``` -Note that this does need to be flagged as an array function with the `$isArrayFormula` argument. When the file is saved, and opened in MS Excel, it will be rendered correctly. +### The At-sign Operator + +If you want to reference just the first cell of an array formula within another formula, you could do so by prefixing it with an at-sign. You can also select the entry in a range which matches the current row in this way; so, if you enter `=@A1:A5` in cell G3, the result will be the value from A3. MS Excel again implements this under the covers by converting to a function SINGLE. PhpSpreadsheet allows the use of the SINGLE function. It does not yet support the at-sign operator, which can have a different meaning in other contexts. + +### Updating Cell in Spill Area + +Excel prevents you from updating a cell in the spill area. PhpSpreadsheet does not - it seems like it might be quite expensive, needing to reevaluate the entire worksheet with each `setValue`. PhpSpreadsheet does provide a method to be used prior to calling `setValue` if desired. +```php +$sheet->setCellValue('A1', '=SORT{7;5;1}'); +$sheet->getCell('A1')->getCalculatedValue(); // populates A1-A3 +$sheet->isCellInSpillRange('A2'); // true +$sheet->isCellInSpillRange('A3'); // true +$sheet->isCellInSpillRange('A4'); // false +$sheet->isCellInSpillRange('A1'); // false +``` +The last result might be surprising. Excel allows you to alter the formula cell itself, so `isCellInSpillRange` treats the formula cell as not in range. It should also be noted that, if array returns are not enabled, `isCellInSpillRange` will always return `false`. ## Locale Settings for Formulas @@ -552,7 +542,7 @@ $spreadsheet->getActiveSheet()->setCellValue('B8',$internalFormula); ``` Currently, formula translation only translates the function names, the -constants TRUE and FALSE, and the function argument separators. Cell addressing using R1C1 formatting is not supported. +constants TRUE and FALSE (and NULL), Excel error messages, and the function argument separators. Cell addressing using R1C1 formatting is not supported. At present, the following locale settings are supported: @@ -576,6 +566,8 @@ Russian | русский язык | ru Swedish | Svenska | sv Turkish | Türkçe | tr +If anybody can provide translations for additional languages, particularly Basque (Euskara), Catalan (Català), Croatian (Hrvatski jezik), Galician (Galego), Greek (Ελληνικά), Slovak (Slovenčina) or Slovenian (Slovenščina); please feel free to volunteer your services, and we'll happily show you what is needed to contribute a new language. + ## Write a newline character "\n" in a cell (ALT+"Enter") In Microsoft Office Excel you get a line break in a cell by hitting diff --git a/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php b/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php index df7ccc5ef..2bccab8da 100644 --- a/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php +++ b/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php @@ -20,7 +20,7 @@ class ExcelArrayPseudoFunctions $ourRow = $cell->getRow(); $firstRow = (int) $matches[3]; $lastRow = (int) $matches[6]; - if ($ourRow < $firstRow || $ourRow > $lastRow) { + if ($ourRow < $firstRow || $ourRow > $lastRow || $matches[1] !== $matches[4]) { return ExcelError::VALUE(); } $referenceCellCoordinate = $matches[1] . $ourRow; diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 979e74c5e..e86fcd4c7 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -403,11 +403,16 @@ class Xml extends BaseReader $columnID = 'A'; foreach ($rowData->Cell as $cell) { + $arrayRef = ''; $cell_ss = self::getAttributes($cell, self::NAMESPACES_SS); if (isset($cell_ss['Index'])) { $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']); } $cellRange = $columnID . $rowID; + if (isset($cell_ss['ArrayRange'])) { + $arrayRange = (string) $cell_ss['ArrayRange']; + $arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, Coordinate::columnIndexFromString($columnID)); + } if ($this->getReadFilter() !== null) { if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { @@ -440,6 +445,9 @@ class Xml extends BaseReader if (isset($cell_ss['Formula'])) { $cellDataFormula = $cell_ss['Formula']; $hasCalculatedValue = true; + if ($arrayRef !== '') { + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayRef]); + } } if (isset($cell->Data)) { $cellData = $cell->Data; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 24d0c6365..8119547d3 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3695,4 +3695,24 @@ class Worksheet implements IComparable } } } + + public function isCellInSpillRange(string $coordinate): bool + { + if (Calculation::getArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) { + return false; + } + $this->calculateArrays(); + $keys = $this->cellCollection->getCoordinates(); + foreach ($keys as $key) { + $attributes = $this->getCell($key)->getFormulaAttributes(); + if (isset($attributes['ref'])) { + if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) { + // false for first cell in range, true otherwise + return $coordinate !== $key; + } + } + } + + return false; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/InternalFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/InternalFunctionsTest.php index 9914d7302..d51d9a5fa 100644 --- a/tests/PhpSpreadsheetTests/Calculation/InternalFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/InternalFunctionsTest.php @@ -113,6 +113,10 @@ class InternalFunctionsTest extends TestCase 'G3:G5', '#VALUE!', ], + 'range which includes current row but spans columns' => [ + 'F7:G9', + '#VALUE!', + ], ]; } } diff --git a/tests/PhpSpreadsheetTests/Functional/ArrayFunctionsSpillTest.php b/tests/PhpSpreadsheetTests/Functional/ArrayFunctionsSpillTest.php index 52d4c75a6..408ab9904 100644 --- a/tests/PhpSpreadsheetTests/Functional/ArrayFunctionsSpillTest.php +++ b/tests/PhpSpreadsheetTests/Functional/ArrayFunctionsSpillTest.php @@ -37,6 +37,12 @@ class ArrayFunctionsSpillTest extends TestCase $sheet->setCellValue('B1', '=UNIQUE(A1:A12)'); $expected = [['#SPILL!'], [null], [null], [null], ['OCCUPIED']]; self::assertSame($expected, $sheet->rangeToArray('B1:B5', calculateFormulas: true, formatData: false, reduceArrays: true), 'spill with B5 unchanged'); + self::assertFalse($sheet->isCellInSpillRange('B1')); + self::assertFalse($sheet->isCellInSpillRange('B2')); + self::assertFalse($sheet->isCellInSpillRange('B3')); + self::assertFalse($sheet->isCellInSpillRange('B4')); + self::assertFalse($sheet->isCellInSpillRange('B5')); + self::assertFalse($sheet->isCellInSpillRange('Z9')); $calculation->clearCalculationCache(); $columnArray = [[1], [2], [2], [2], [3], [3], [3], [3], [4], [4], [4], [4]]; @@ -44,6 +50,12 @@ class ArrayFunctionsSpillTest extends TestCase $sheet->setCellValue('B1', '=UNIQUE(A1:A12)'); $expected = [[1], [2], [3], [4], ['OCCUPIED']]; self::assertSame($expected, $sheet->rangeToArray('B1:B5', calculateFormulas: true, formatData: false, reduceArrays: true), 'fill B1:B4 with B5 unchanged'); + self::assertFalse($sheet->isCellInSpillRange('B1')); + self::assertTrue($sheet->isCellInSpillRange('B2')); + self::assertTrue($sheet->isCellInSpillRange('B3')); + self::assertTrue($sheet->isCellInSpillRange('B4')); + self::assertFalse($sheet->isCellInSpillRange('B5')); + self::assertFalse($sheet->isCellInSpillRange('Z9')); $calculation->clearCalculationCache(); $columnArray = [[1], [3], [3], [3], [3], [3], [3], [3], [3], [3], [3], [3]]; @@ -51,6 +63,12 @@ class ArrayFunctionsSpillTest extends TestCase $sheet->setCellValue('B1', '=UNIQUE(A1:A12)'); $expected = [[1], [3], [null], [null], ['OCCUPIED']]; self::assertSame($expected, $sheet->rangeToArray('B1:B5', calculateFormulas: true, formatData: false, reduceArrays: true), 'fill B1:B2(changed from prior) set B3:B4 to null B5 unchanged'); + self::assertFalse($sheet->isCellInSpillRange('B1')); + self::assertTrue($sheet->isCellInSpillRange('B2')); + self::assertFalse($sheet->isCellInSpillRange('B3')); + self::assertFalse($sheet->isCellInSpillRange('B4')); + self::assertFalse($sheet->isCellInSpillRange('B5')); + self::assertFalse($sheet->isCellInSpillRange('Z9')); $calculation->clearCalculationCache(); $columnArray = [[1], [2], [3], [3], [3], [3], [3], [3], [3], [3], [3], [3]]; @@ -67,6 +85,36 @@ class ArrayFunctionsSpillTest extends TestCase self::assertSame($expected, $sheet->rangeToArray('B1:B5', calculateFormulas: true, formatData: false, reduceArrays: true), 'spill clears B2:B4 with B5 unchanged'); $calculation->clearCalculationCache(); + $sheet->setCellValue('Z1', '=SORT({7;5;1})'); + $sheet->getCell('Z1')->getCalculatedValue(); // populates Z1-Z3 + self::assertTrue($sheet->isCellInSpillRange('Z2')); + self::assertTrue($sheet->isCellInSpillRange('Z3')); + self::assertFalse($sheet->isCellInSpillRange('Z4')); + self::assertFalse($sheet->isCellInSpillRange('Z1')); + + $spreadsheet->disconnectWorksheets(); + } + + public function testNonArrayOutput(): void + { + $spreadsheet = new Spreadsheet(); + $calculation = Calculation::getInstance($spreadsheet); + + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B5', 'OCCUPIED'); + + $columnArray = [[1], [2], [2], [2], [3], [3], [3], [3], [4], [4], [4], [4]]; + $sheet->fromArray($columnArray, 'A1'); + $sheet->setCellValue('B1', '=UNIQUE(A1:A12)'); + $expected = [[1], [null], [null], [null], ['OCCUPIED']]; + self::assertSame($expected, $sheet->rangeToArray('B1:B5', calculateFormulas: true, formatData: false, reduceArrays: true), 'only fill B1'); + self::assertFalse($sheet->isCellInSpillRange('B1')); + self::assertFalse($sheet->isCellInSpillRange('B2')); + self::assertFalse($sheet->isCellInSpillRange('B3')); + self::assertFalse($sheet->isCellInSpillRange('B4')); + self::assertFalse($sheet->isCellInSpillRange('B5')); + self::assertFalse($sheet->isCellInSpillRange('Z9')); + $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php index cba92a6d3..861a2e05e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php @@ -28,7 +28,7 @@ class ArrayFormulaTest extends TestCase string $cellAddress, string $expectedRange, string $expectedFormula, - array $expectedValue + array|float $expectedValue ): void { $filename = 'tests/data/Reader/Gnumeric/ArrayFormulaTest.gnumeric'; $reader = new Gnumeric(); @@ -37,13 +37,21 @@ class ArrayFormulaTest extends TestCase $cell = $worksheet->getCell($cellAddress); self::assertSame(DataType::TYPE_FORMULA, $cell->getDataType()); - self::assertSame(['t' => 'array', 'ref' => $expectedRange], $cell->getFormulaAttributes()); + if (is_array($expectedValue)) { + self::assertSame(['t' => 'array', 'ref' => $expectedRange], $cell->getFormulaAttributes()); + } else { + self::assertEmpty($cell->getFormulaAttributes()); + } self::assertSame($expectedFormula, strtoupper($cell->getValue())); Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); $worksheet->calculateArrays(); $cell = $worksheet->getCell($cellAddress); self::assertSame($expectedValue, $cell->getCalculatedValue()); - self::assertSame($expectedValue, $worksheet->rangeToArray($expectedRange, formatData: false, reduceArrays: true)); + if (is_array($expectedValue)) { + self::assertSame($expectedValue, $worksheet->rangeToArray($expectedRange, formatData: false, reduceArrays: true)); + } else { + self::assertSame([[$expectedValue]], $worksheet->rangeToArray($expectedRange, formatData: false, reduceArrays: true)); + } $spreadsheet->disconnectWorksheets(); } @@ -62,6 +70,12 @@ class ArrayFormulaTest extends TestCase '=SIN({-1,0,1,2})', [[-0.8414709848078965, 0.0, 0.8414709848078965, 0.9092974268256817]], ], + [ + 'G3', + 'G3:G3', + '=MAX(SIN({-1,0,1,2}))', + 0.9092974268256817, + ], [ 'D4', 'D4:E5', diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php new file mode 100644 index 000000000..2b37d3314 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php @@ -0,0 +1,101 @@ +arrayReturnType = Calculation::getArrayReturnType(); + } + + protected function tearDown(): void + { + Calculation::setArrayReturnType($this->arrayReturnType); + } + + /** + * @dataProvider arrayFormulaReaderProvider + */ + public function testArrayFormulaReader( + string $cellAddress, + string $expectedRange, + string $expectedFormula, + array|float $expectedValue + ): void { + $filename = 'tests/data/Reader/Ods/ArrayFormulaTest.ods'; + $reader = new Ods(); + $spreadsheet = $reader->load($filename); + $worksheet = $spreadsheet->getActiveSheet(); + + $cell = $worksheet->getCell($cellAddress); + self::assertSame(DataType::TYPE_FORMULA, $cell->getDataType()); + self::assertSame(['t' => 'array', 'ref' => $expectedRange], $cell->getFormulaAttributes()); + self::assertSame($expectedFormula, strtoupper($cell->getValue())); + Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); + $worksheet->calculateArrays(); + $cell = $worksheet->getCell($cellAddress); + self::assertSame($expectedValue, $cell->getCalculatedValue()); + if (is_array($expectedValue)) { + self::assertSame($expectedValue, $worksheet->rangeToArray($expectedRange, formatData: false, reduceArrays: true)); + } else { + self::assertSame([[$expectedValue]], $worksheet->rangeToArray($expectedRange, formatData: false, reduceArrays: true)); + } + $spreadsheet->disconnectWorksheets(); + } + + public static function arrayFormulaReaderProvider(): array + { + return [ + [ + 'B2', + 'B2:C3', + '={2,3}*{4;5}', + [[8, 12], [10, 15]], + ], + [ + 'E1', + 'E1:H1', + '=SIN({-1,0,1,2})', + [[-0.8414709848078965, 0.0, 0.8414709848078965, 0.9092974268256817]], + ], + [ + 'E3', + 'E3:E3', + '=MAX(SIN({-1,0,1,2}))', + 0.9092974268256817, + ], + [ + 'D5', + 'D5:E6', + '=A5:B5*A5:A6', + [[4, 6], [8, 12]], + ], + [ + 'D8', + 'D8:E9', + '=A8:B8*A8:A9', + [[9, 12], [15, 20]], + ], + [ + 'D11', + 'D11:E12', + '=A11:B11*A11:A12', + [[16, 20], [24, 30]], + ], + [ + 'D14', + 'D14:E15', + '=A14:B14*A14:A15', + [[25, 30], [35, 42]], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php new file mode 100644 index 000000000..6ba32aa33 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php @@ -0,0 +1,26 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + + self::assertSame(DataType::TYPE_FORMULA, $sheet->getCell('B1')->getDataType()); + self::assertSame(['t' => 'array', 'ref' => 'B1:B3'], $sheet->getCell('B1')->getFormulaAttributes()); + self::assertSame('=CONCATENATE(A1:A3,"-",C1:C3)', strtoupper($sheet->getCell('B1')->getValue())); + self::assertSame('a-1', $sheet->getCell('B1')->getOldCalculatedValue()); + self::assertSame('b-2', $sheet->getCell('B2')->getValue()); + self::assertSame('c-3', $sheet->getCell('B3')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/ArrayTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/ArrayTest.php index 07f945477..281b1c865 100644 --- a/tests/PhpSpreadsheetTests/Writer/Ods/ArrayTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Ods/ArrayTest.php @@ -63,12 +63,20 @@ class ArrayTest extends AbstractFunctional $sheet->getCell('B1')->setValue('=UNIQUE(A1:A3)'); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Ods'); $sheet = $reloadedSpreadsheet->getActiveSheet(); - self::assertEquals('1', $sheet->getCell('A1')->getValue()); - self::assertEquals('1', $sheet->getCell('A2')->getValue()); - self::assertEquals('3', $sheet->getCell('A3')->getValue()); - self::assertEquals('3', $sheet->getCell('B2')->getValue()); + self::assertSame(1, $sheet->getCell('A1')->getValue()); + self::assertSame(1, $sheet->getCell('A2')->getValue()); + self::assertSame(3, $sheet->getCell('A3')->getValue()); + self::assertSame(3, $sheet->getCell('B2')->getValue()); + self::assertTrue($sheet->getCell('B1')->isFormula()); + self::assertSame(1, $sheet->getCell('B1')->getOldCalculatedValue()); self::assertNull($sheet->getCell('B3')->getValue()); self::assertEquals('=UNIQUE(A1:A3)', $sheet->getCell('B1')->getValue()); + $cellFormulaAttributes = $sheet->getCell('B1')->getFormulaAttributes(); + self::assertArrayHasKey('t', $cellFormulaAttributes); + self::assertSame('array', $cellFormulaAttributes['t']); + self::assertArrayHasKey('ref', $cellFormulaAttributes); + self::assertSame('B1:B2', $cellFormulaAttributes['ref']); + self::assertSame([[1], [3]], $sheet->getCell('B1')->getCalculatedValue()); $spreadsheet->disconnectWorksheets(); $reloadedSpreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php index d7265651d..038f8c235 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php @@ -333,10 +333,18 @@ class ArrayFunctionsTest extends TestCase self::assertSame($expectedUnique, $sheet2->getCell('H1')->getCalculatedValue()); for ($row = 1; $row <= 5; ++$row) { if ($row > 1) { - self::assertSame($expectedUnique[$row - 1][0], $sheet2->getCell("H$row")->getCalculatedValue(), "cell H$row"); + self::assertSame($expectedUnique[$row - 1][0], $sheet2->getCell("H$row")->getValue(), "cell H$row"); + } else { + self::assertTrue($sheet2->getCell("H$row")->isFormula()); + self::assertSame($expectedUnique[$row - 1][0], $sheet2->getCell("H$row")->getOldCalculatedValue(), "cell H$row"); } - self::assertSame($expectedUnique[$row - 1][1], $sheet2->getCell("I$row")->getCalculatedValue(), "cell I$row"); + self::assertSame($expectedUnique[$row - 1][1], $sheet2->getCell("I$row")->getValue(), "cell I$row"); } + $cellFormulaAttributes = $sheet2->getCell('H1')->getFormulaAttributes(); + self::assertArrayHasKey('t', $cellFormulaAttributes); + self::assertSame('array', $cellFormulaAttributes['t']); + self::assertArrayHasKey('ref', $cellFormulaAttributes); + self::assertSame('H1:I5', $cellFormulaAttributes['ref']); $spreadsheet2->disconnectWorksheets(); } @@ -374,4 +382,58 @@ class ArrayFunctionsTest extends TestCase self::assertSame('x', $sheet2->getCell('A3')->getValue()); $spreadsheet2->disconnectWorksheets(); } + + public function testArrayStringOutput(): void + { + Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $columnArray = [ + ['item1'], + ['item2'], + ['item3'], + ['item1'], + ['item1'], + ['item6'], + ['item7'], + ['item1'], + ['item9'], + ['item1'], + ]; + $sheet->fromArray($columnArray, 'A1'); + $sheet->setCellValue('C1', '=UNIQUE(A1:A10)'); + $writer = new XlsxWriter($spreadsheet); + $this->outputFile = File::temporaryFilename(); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $reader = new XlsxReader(); + $spreadsheet2 = $reader->load($this->outputFile); + $sheet2 = $spreadsheet2->getActiveSheet(); + $expectedUnique = [ + ['item1'], + ['item2'], + ['item3'], + ['item6'], + ['item7'], + ['item9'], + ]; + self::assertCount(6, $expectedUnique); + self::assertSame($expectedUnique, $sheet2->getCell('C1')->getCalculatedValue()); + self::assertSame($expectedUnique[0][0], $sheet2->getCell('C1')->getCalculatedValueString()); + for ($row = 2; $row <= 6; ++$row) { + self::assertSame($expectedUnique[$row - 1][0], $sheet2->getCell("C$row")->getCalculatedValue(), "cell C$row"); + } + $spreadsheet2->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFile; + $file .= '#xl/worksheets/sheet1.xml'; + $data = file_get_contents($file); + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString('_xlfn.UNIQUE(A1:A10)item1', $data, '6 results for UNIQUE'); + } + } } diff --git a/tests/data/Reader/Ods/ArrayFormulaTest.ods b/tests/data/Reader/Ods/ArrayFormulaTest.ods new file mode 100644 index 0000000000000000000000000000000000000000..84b5cd02a2e00241ecac09ec974974d129b8ee3d GIT binary patch literal 14534 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 i-%q*Qb+sTNBltyQDXLPQjPEiO$R& z2jB*4ytN6VwJZS5)lpl&Q?qR;by6Aaku;iHzqS&$wg+eLj6Qb-f!L6CqDyzlv(1>1 z?B~Yf`OSNKF|TfSg+tLw6;Tn)&EJ1iiJLnUh&?-_r>u+TI!! zZP;uaxx*DBZH;03+)p-W0IA!&{Gqx^crv4H>Q?QS8!0cb;%KGdLOSf3Wn50>bLudN zrTjy+>xnq4He7hX?FG@#UT=Rj*v)+(pvQ{zv)3OW5+MA2GSScdxZm_EwdlXOem{YL z@H0#0H)-A%|0#Ls&$a*N`*+00@88jXf%7|==Ayn z$mE}~M87HaK9J~l9?yUC{W0G4XWa5{>br-z&&B&!6!U*`{W0w6XDsY*D!p&E#qY@2 zUyOej74@5O7Vqz4q<*3N684Xp_s`w5P5tM+_5bGlqxSfznSaw1*Wc)ARYhc!pO4|* Oe=P5njv~*`U;ht;>*eVH literal 0 HcmV?d00001 diff --git a/tests/data/Reader/Xml/ArrayFormula.xml b/tests/data/Reader/Xml/ArrayFormula.xml new file mode 100644 index 000000000..602a1b8a8 --- /dev/null +++ b/tests/data/Reader/Xml/ArrayFormula.xml @@ -0,0 +1,75 @@ + + + + + Owen Leibman + Owen Leibman + 2024-06-17T19:03:14Z + 2024-06-17T19:04:33Z + 16.00 + + + + + + 10300 + 19420 + 32767 + 32767 + False + False + + + + + + + + a + a-1 + 1 + + + b + b-2 + 2 + + + c + c-3 + 3 + +
+ + +
+