Unit tests for pseudo-functions

This commit is contained in:
MarkBaker
2022-02-03 15:38:55 +01:00
parent ceb1c04ca9
commit 54d49eddf6
4 changed files with 138 additions and 22 deletions
@@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
@@ -15,12 +16,13 @@ class ExcelArrayPseudoFunctions
$worksheet = $cell->getWorksheet();
[$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true);
$result = ($referenceWorksheetName === '')
? $worksheet->getCell($referenceCellCoordinate)->getCalculatedValue()
$referenceCell = ($referenceWorksheetName === '')
? $worksheet->getCell($referenceCellCoordinate)
: $worksheet->getParent()
->getSheetByName($referenceWorksheetName)
->getCell($referenceCellCoordinate)->getCalculatedValue();
->getCell($referenceCellCoordinate);
$result = $referenceCell->getCalculatedValue();
return [[$result]];
}
@@ -32,30 +34,50 @@ class ExcelArrayPseudoFunctions
$value = $cell->getValue();
[$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true);
$referenceCell = ($referenceWorksheetName === '')
? $worksheet->getCell($referenceCellCoordinate)
: $worksheet->getParent()
->getSheetByName($referenceWorksheetName)
->getCell($referenceCellCoordinate);
// We should always use the sizing for the array formula range from the referenced cell formula
$referenceRange = null;
if ($referenceCell->isFormula() && $referenceCell->isArrayFormula()) {
$referenceRange = $referenceCell->arrayFormulaRange();
}
$calcEngine = Calculation::getInstance($worksheet->getParent());
$result = $calcEngine->calculateCellValue(
($referenceWorksheetName === '')
? $worksheet->getCell($referenceCellCoordinate)
: $worksheet->getParent()
->getSheetByName($referenceWorksheetName)
->getCell($referenceCellCoordinate)
);
$result = $calcEngine->calculateCellValue($referenceCell);
if (!is_array($result)) {
$result = [[$result]];
}
// Set the result
$worksheet->fromArray(
$result,
null,
$coordinate,
true
);
// Ensure that our array result dimensions match the specified array formula range dimensions,
// from the referenced cell, expanding or shrinking it as necessary.
if ($referenceRange !== null) {
$result = Functions::resizeMatrix(
$result,
...Coordinate::rangeDimension($referenceRange ?? $coordinate)
);
}
// Set the result for our target cell (with spillage)
// But if we do write it, we get problems with #SPILL! Errors if the spreadsheet is saved
// TODO How are we going to identify and handle a #SPILL! or a #CALC! error?
// $worksheet->fromArray(
// $result,
// null,
// $coordinate,
// true
// );
// Calculate the array formula range that we should set for our target, based on our target cell coordinate
[$col, $row] = Coordinate::indexesFromString($coordinate);
$row += count($result) - 1;
$col = Coordinate::stringFromColumnIndex($col + count($result[0]) - 1);
$formulaAttributes = ['t' => 'array', 'ref' => "{$coordinate}:{$col}{$row}"];
// fromArray() will reset the value for this cell with the calculation result
// Using fromArray() would reset the value for this cell with the calculation result
// as well as updating the spillage cells,
// so we need to restore this cell to its formula value, attributes, and datatype
$cell = $worksheet->getCell($coordinate);
+3 -2
View File
@@ -2900,12 +2900,13 @@ class Worksheet implements IComparable
* @param string $range Range to extract title from
* @param bool $returnRange Return range? (see example)
*
* @return mixed
* @return string|string[]
*/
public static function extractSheetTitle($range, $returnRange = false)
{
// Sheet title included?
if (($sep = strrpos($range, '!')) === false) {
$sep = strrpos($range, '!');
if ($sep === false) {
return $returnRange ? ['', $range] : '';
}
+13 -1
View File
@@ -1245,6 +1245,16 @@ class Worksheet extends WriterPart
$objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
}
private const CM_SPILLAGE_ARRAY_FUNCTIONS = '/\b(' .
'anchorarray|' .
'filter|' .
'randarray|' .
'sequence|' .
'sort|' .
'sortby|' .
'unique' .
')\(/ui';
private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $cell): void
{
$calculatedValue = $this->getParentWriter()->getPreCalculateFormulas()
@@ -1265,7 +1275,9 @@ class Worksheet extends WriterPart
$attributes = $cell->getFormulaAttributes();
if (($attributes['t'] ?? null) === 'array') {
$objWriter->writeAttribute('cm', '1');
if (preg_match(self::CM_SPILLAGE_ARRAY_FUNCTIONS, $cellValue) === 1) {
$objWriter->writeAttribute('cm', '1');
}
$objWriter->startElement('f');
$objWriter->writeAttribute('t', 'array');
@@ -0,0 +1,81 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class InternalFunctionsTest extends TestCase
{
/**
* @dataProvider anchorArrayDataProvider
*/
public function testAnchorArrayFormula(string $reference, string $range, array $expectedResult): void
{
$spreadsheet = new Spreadsheet();
$sheet1 = $spreadsheet->getActiveSheet();
$sheet1->setTitle('SheetOne'); // no space in sheet title
$sheet2 = $spreadsheet->createSheet();
$sheet2->setTitle('Sheet Two'); // space in sheet title
$sheet1->setCellValue('C3', '=SEQUENCE(3,3,-4)', true, 'C3:E5');
$sheet2->setCellValue('C3', '=SEQUENCE(3,3, 9, -1)', true, 'C3:E5');
$sheet1->setCellValue('A8', "=ANCHORARRAY({$reference})", true, $range);
$result1 = $sheet1->getCell('A8')->getCalculatedValue(true, true);
self::assertSame($expectedResult, $result1);
$attributes1 = $sheet1->getCell('A8')->getFormulaAttributes();
self::assertSame(['t' => 'array', 'ref' => $range], $attributes1);
}
public function anchorArrayDataProvider(): array
{
return [
[
'C3',
'A8:C10',
[[-4, -3, -2], [-1, 0, 1], [2, 3, 4]],
],
[
"'Sheet Two'!C3",
'A8:C10',
[[9, 8, 7], [6, 5, 4], [3, 2, 1]],
],
];
}
/**
* @dataProvider singleDataProvider
*/
public function testSingleArrayFormula(string $reference, array $expectedResult): void
{
$spreadsheet = new Spreadsheet();
$sheet1 = $spreadsheet->getActiveSheet();
$sheet1->setTitle('SheetOne'); // no space in sheet title
$sheet2 = $spreadsheet->createSheet();
$sheet2->setTitle('Sheet Two'); // space in sheet title
$sheet1->setCellValue('C3', '=SEQUENCE(3,3,-4)', true, 'C3:E5');
$sheet2->setCellValue('C3', '=SEQUENCE(3,3, 9, -1)', true, 'C3:E5');
$sheet1->setCellValue('A8', "=SINGLE({$reference})");
$result1 = $sheet1->getCell('A8')->getCalculatedValue(true, true);
self::assertSame($expectedResult, $result1);
}
public function singleDataProvider(): array
{
return [
[
'C3',
[[-4]],
],
[
"'Sheet Two'!C3",
[[9]],
],
];
}
}