Skip to content

Commit ef2b5b9

Browse files
committed
Mostly Docs and Tests
Also support for Xml format.
1 parent 3a690a7 commit ef2b5b9

File tree

13 files changed

+410
-52
lines changed

13 files changed

+410
-52
lines changed

docs/topics/recipes.md

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ $value = $spreadsheet->getActiveSheet()->getCell('B8')->getCalculatedValue();
376376
### Array Formulas
377377

378378
With version 2.0.3 of PhpSpreadsheet, we've introduced support for Excel "array formulas".
379-
It is an opt-in feature. You need to enable it with the following code:
379+
**It is an opt-in feature.** You need to enable it with the following code:
380380
```php
381381
\PhpOffice\PhpSpreadsheet\Calculation\Calculation::setArrayReturnType(
382382
\PhpOffice\PhpSpreadsheet\Calculation\Calculation::RETURN_ARRAY_AS_ARRAY);
@@ -405,15 +405,16 @@ However, we could have specified an alternative formula to calculate that result
405405

406406
![12-CalculationEngine-Array-Formula.png](./images/12-CalculationEngine-Array-Formula.png)
407407

408-
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).
409-
In recent releases of Excel, Shift-Ctrl-Enter is not required, and Excel does not add the braces.
410-
PhpSpreadsheet will attempt to behave like the recent releases.
408+
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).
409+
410+
**In recent releases of Excel, Ctrl-Shift-Enter is not required, and Excel does not add the braces.
411+
PhpSpreadsheet will attempt to behave like the recent releases.**
411412

412413
Or to identify the biggest increase in like-for-like sales from one month to the next:
413414

414415
![12-CalculationEngine-Array-Formula-3.png](./images/12-CalculationEngine-Array-Formula-3.png)
415416
```php
416-
$spreadsheet->getActiveSheet()->setCellValue('F1','=MAX(B2:B6-C2:C6)', true);
417+
$spreadsheet->getActiveSheet()->setCellValue('F1','=MAX(B2:B6-C2:C6)');
417418
```
418419
Which tells us that the biggest increase in sales between December and January was 30 more (in this case, 30 more Lemons).
419420

@@ -424,8 +425,8 @@ As an example, consider transposing a grid of data: MS Excel provides the `TRANS
424425

425426
![12-CalculationEngine-Array-Formula-2.png](./images/12-CalculationEngine-Array-Formula-2.png)
426427

427-
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.
428-
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.
428+
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.
429+
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.
429430

430431
Note also that we still set this as the formula for the top-left cell of that range, cell `A10`.
431432

@@ -434,66 +435,40 @@ Simply setting an array formula in a cell and specifying the range won't populat
434435
$spreadsheet->getActiveSheet()
435436
->setCellValue(
436437
'A10',
437-
'=SEQUENCE(3,3)',
438-
true,
439-
'A1:C3'
438+
'=SEQUENCE(3,3)'
440439
);
441-
442440
// Will return a null, because the formula for A1 hasn't been calculated to populate the spillage area
443441
$result = $spreadsheet->getActiveSheet()->getCell('C3')->getValue();
444442
```
445443
To do that, we need to retrieve the calculated value for the cell.
446444
```php
447-
$spreadsheet->getActiveSheet()
448-
->setCellValue(
449-
'A10',
450-
'=SEQUENCE(3,3)',
451-
true,
452-
'A1:C3'
453-
);
454-
455445
$spreadsheet->getActiveSheet()->getCell('A1')->getCalculatedValue();
456-
457446
// Will return 9, because the formula for A1 has now been calculated, and the spillage area is populated
458447
$result = $spreadsheet->getActiveSheet()->getCell('C3')->getValue();
459448
```
460-
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.
449+
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.
461450
```php
462451
// Will return integer 1, the value for that cell within the array
463452
$a1result = $spreadsheet->getActiveSheet()->getCell('A1')->getCalculatedValue();
464453
```
465454

466-
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.
467-
```php
468-
// Will return an array [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
469-
$a1result = $spreadsheet->getActiveSheet()->getCell('A1')->getCalculatedValue(true);
470-
```
471-
472455
---
473456

474457
Excel365 introduced a number of new functions that return arrays of results.
475458
These include the `UNIQUE()`, `SORT()`, `SORTBY()`, `FILTER()`, `SEQUENCE()` and `RANDARRAY()` functions.
476459
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.
477460

478-
The way these functions are presented in MS Excel itself is slightly different to that of other array functions.
479-
480461
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.
481462

482463
![12-CalculationEngine-Spillage-Formula.png](./images/12-CalculationEngine-Spillage-Formula.png)
483464

484-
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 (`{}`).
465+
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 (`{}`).
485466

486467
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.
487468

488469
![12-CalculationEngine-Spillage-Formula-2.png](./images/12-CalculationEngine-Spillage-Formula-2.png)
489470

490-
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).
491-
492-
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.
493-
494-
```php
495-
$spreadsheet->getActiveSheet()->setCellValue('A1','=SEQUENCE(3,3,-10,2.5)', true, 'A1:C3');
496-
```
471+
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`.
497472

498473
### The Spill Operator
499474

@@ -502,19 +477,34 @@ To simplify this, MS Excel has introduced the "Spill" Operator (`#`).
502477

503478
![12-CalculationEngine-Spillage-Operator.png](./images/12-CalculationEngine-Spillage-Operator.png)
504479

505-
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.
480+
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.
506481
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.
507482

508-
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.
483+
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.
509484

510485
To create this same function in PhpSpreadsheet, use:
511486
```php
512-
$spreadsheet->getActiveSheet()->setCellValue('D1','=SUM(ABS(ANCHORARRAY(A1)))', true);
487+
$spreadsheet->getActiveSheet()->setCellValue('D1','=SUM(ABS(ANCHORARRAY(A1)))');
513488
```
514-
Note that this does need to be flagged as an array function with the `$isArrayFormula` argument.
515489

516490
When the file is saved, and opened in MS Excel, it will be rendered correctly.
517491

492+
### The At-sign Operator
493+
494+
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.
495+
496+
### Updating Cell in Spill Area
497+
498+
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.
499+
```php
500+
$sheet->setCellValue('A1', '=SORT{7;5;1}');
501+
$sheet->getCell('A1')->getCalculatedValue(); // populates A1-A3
502+
$sheet->isCellInSpillRange('A2'); // true
503+
$sheet->isCellInSpillRange('A3'); // true
504+
$sheet->isCellInSpillRange('A4'); // false
505+
$sheet->isCellInSpillRange('A1'); // false
506+
```
507+
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`.
518508

519509
## Locale Settings for Formulas
520510

@@ -552,7 +542,7 @@ $spreadsheet->getActiveSheet()->setCellValue('B8',$internalFormula);
552542
```
553543

554544
Currently, formula translation only translates the function names, the
555-
constants TRUE and FALSE, and the function argument separators. Cell addressing using R1C1 formatting is not supported.
545+
constants TRUE and FALSE (and NULL), Excel error messages, and the function argument separators. Cell addressing using R1C1 formatting is not supported.
556546

557547
At present, the following locale settings are supported:
558548

@@ -576,6 +566,8 @@ Russian | русский язык | ru
576566
Swedish | Svenska | sv
577567
Turkish | Türkçe | tr
578568

569+
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.
570+
579571
## Write a newline character "\n" in a cell (ALT+"Enter")
580572

581573
In Microsoft Office Excel you get a line break in a cell by hitting

src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static function single(string $cellReference, Cell $cell): mixed
2020
$ourRow = $cell->getRow();
2121
$firstRow = (int) $matches[3];
2222
$lastRow = (int) $matches[6];
23-
if ($ourRow < $firstRow || $ourRow > $lastRow) {
23+
if ($ourRow < $firstRow || $ourRow > $lastRow || $matches[1] !== $matches[4]) {
2424
return ExcelError::VALUE();
2525
}
2626
$referenceCellCoordinate = $matches[1] . $ourRow;

src/PhpSpreadsheet/Reader/Xml.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,16 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
403403

404404
$columnID = 'A';
405405
foreach ($rowData->Cell as $cell) {
406+
$arrayRef = '';
406407
$cell_ss = self::getAttributes($cell, self::NAMESPACES_SS);
407408
if (isset($cell_ss['Index'])) {
408409
$columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
409410
}
410411
$cellRange = $columnID . $rowID;
412+
if (isset($cell_ss['ArrayRange'])) {
413+
$arrayRange = (string) $cell_ss['ArrayRange'];
414+
$arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, Coordinate::columnIndexFromString($columnID));
415+
}
411416

412417
if ($this->getReadFilter() !== null) {
413418
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
@@ -440,6 +445,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
440445
if (isset($cell_ss['Formula'])) {
441446
$cellDataFormula = $cell_ss['Formula'];
442447
$hasCalculatedValue = true;
448+
if ($arrayRef !== '') {
449+
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayRef]);
450+
}
443451
}
444452
if (isset($cell->Data)) {
445453
$cellData = $cell->Data;

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3695,4 +3695,24 @@ public function calculateArrays(bool $preCalculateFormulas = true): void
36953695
}
36963696
}
36973697
}
3698+
3699+
public function isCellInSpillRange(string $coordinate): bool
3700+
{
3701+
if (Calculation::getArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
3702+
return false;
3703+
}
3704+
$this->calculateArrays();
3705+
$keys = $this->cellCollection->getCoordinates();
3706+
foreach ($keys as $key) {
3707+
$attributes = $this->getCell($key)->getFormulaAttributes();
3708+
if (isset($attributes['ref'])) {
3709+
if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
3710+
// false for first cell in range, true otherwise
3711+
return $coordinate !== $key;
3712+
}
3713+
}
3714+
}
3715+
3716+
return false;
3717+
}
36983718
}

tests/PhpSpreadsheetTests/Calculation/InternalFunctionsTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public static function singleDataProvider(): array
113113
'G3:G5',
114114
'#VALUE!',
115115
],
116+
'range which includes current row but spans columns' => [
117+
'F7:G9',
118+
'#VALUE!',
119+
],
116120
];
117121
}
118122
}

0 commit comments

Comments
 (0)