Skip to content

Commit 3a690a7

Browse files
committed
SINGLE Function, and Gnumeric
SINGLE function can be used to return first value from a dynamic array result, or to return the value of the cell which matches the current row (VALUE error if not match) for a range. Excel allows you to specify an at-sign unary operator rather than SINGLE function; this PR does not permit that. Add support for reading CSE array functions for Gnumeric. Throw an exception if setValueExplicit Formula is invalid (not a string, or doesn't begin with equal sign. This is equivalent to what happens when setValueExplicit Numeric specifies a non-numeric value. Added a number of tests from PR PHPOffice#2787.
1 parent 0b471ef commit 3a690a7

File tree

12 files changed

+591
-8
lines changed

12 files changed

+591
-8
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,7 +3733,9 @@ public static function checkMatrixOperands(mixed &$operand1, mixed &$operand2, i
37333733

37343734
[$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
37353735
[$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
3736-
if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
3736+
if ($resize === 3) {
3737+
$resize = 2;
3738+
} elseif (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
37373739
$resize = 1;
37383740
}
37393741

@@ -4560,6 +4562,7 @@ private function processTokenStack(mixed $tokens, ?string $cellID = null, ?Cell
45604562
// If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
45614563
// so we store the parent cell collection so that we can re-attach it when necessary
45624564
$pCellWorksheet = ($cell !== null) ? $cell->getWorksheet() : null;
4565+
$originalCoordinate = $cell?->getCoordinate();
45634566
$pCellParent = ($cell !== null) ? $cell->getParent() : null;
45644567
$stack = new Stack($this->branchPruner);
45654568

@@ -5061,6 +5064,9 @@ private function processTokenStack(mixed $tokens, ?string $cellID = null, ?Cell
50615064
}
50625065

50635066
// Process the argument with the appropriate function call
5067+
if ($pCellWorksheet !== null && $originalCoordinate !== null) {
5068+
$pCellWorksheet->getCell($originalCoordinate);
5069+
}
50645070
$args = $this->addCellReference($args, $passCellReference, $functionCall, $cell);
50655071

50665072
if (!is_array($functionCall)) {
@@ -5268,7 +5274,7 @@ private function executeNumericBinaryOperation(mixed $operand1, mixed $operand2,
52685274
$operand2[$key] = Functions::flattenArray($value);
52695275
}
52705276
}
5271-
[$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2);
5277+
[$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 3);
52725278

52735279
for ($row = 0; $row < $rows; ++$row) {
52745280
for ($column = 0; $column < $columns; ++$column) {

src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,32 @@
1111

1212
class ExcelArrayPseudoFunctions
1313
{
14-
public static function single(string $cellReference, Cell $cell): array|string
14+
public static function single(string $cellReference, Cell $cell): mixed
1515
{
1616
$worksheet = $cell->getWorksheet();
1717

1818
[$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true);
19+
if (preg_match('/^([$]?[a-z]{1,3})([$]?([0-9]{1,7})):([$]?[a-z]{1,3})([$]?([0-9]{1,7}))$/i', "$referenceCellCoordinate", $matches) === 1) {
20+
$ourRow = $cell->getRow();
21+
$firstRow = (int) $matches[3];
22+
$lastRow = (int) $matches[6];
23+
if ($ourRow < $firstRow || $ourRow > $lastRow) {
24+
return ExcelError::VALUE();
25+
}
26+
$referenceCellCoordinate = $matches[1] . $ourRow;
27+
}
1928
$referenceCell = ($referenceWorksheetName === '')
2029
? $worksheet->getCell((string) $referenceCellCoordinate)
2130
: $worksheet->getParentOrThrow()
2231
->getSheetByNameOrThrow((string) $referenceWorksheetName)
2332
->getCell((string) $referenceCellCoordinate);
2433

2534
$result = $referenceCell->getCalculatedValue();
35+
while (is_array($result)) {
36+
$result = array_shift($result);
37+
}
2638

27-
return [[$result]];
39+
return $result;
2840
}
2941

3042
public static function anchorArray(string $cellReference, Cell $cell): array|string

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE
275275

276276
break;
277277
case DataType::TYPE_FORMULA:
278+
if (!is_string($value) || $value[0] !== '=') {
279+
throw new SpreadsheetException('Invalid value for datatype Formula');
280+
}
278281
$this->value = (string) $value;
279282

280283
break;

src/PhpSpreadsheet/Reader/Gnumeric.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,15 +545,21 @@ private function loadCell(
545545
): void {
546546
$ValueType = $cellAttributes->ValueType;
547547
$ExprID = (string) $cellAttributes->ExprID;
548+
$rows = (int) ($cellAttributes->Rows ?? 0);
549+
$cols = (int) ($cellAttributes->Cols ?? 0);
548550
$type = DataType::TYPE_FORMULA;
551+
$isArrayFormula = ($rows > 0 && $cols > 0);
552+
$arrayFormulaRange = $isArrayFormula ? $this->getArrayFormulaRange($column, $row, $cols, $rows) : null;
549553
if ($ExprID > '') {
550554
if (((string) $cell) > '') {
555+
// Formula
551556
$this->expressions[$ExprID] = [
552557
'column' => $cellAttributes->Col,
553558
'row' => $cellAttributes->Row,
554559
'formula' => (string) $cell,
555560
];
556561
} else {
562+
// Shared Formula
557563
$expression = $this->expressions[$ExprID];
558564

559565
$cell = $this->referenceHelper->updateFormulaReferences(
@@ -565,21 +571,39 @@ private function loadCell(
565571
);
566572
}
567573
$type = DataType::TYPE_FORMULA;
568-
} else {
574+
} elseif ($isArrayFormula === false) {
569575
$vtype = (string) $ValueType;
570576
if (array_key_exists($vtype, self::$mappings['dataType'])) {
571577
$type = self::$mappings['dataType'][$vtype];
572578
}
573-
if ($vtype === '20') { // Boolean
579+
if ($vtype === '20') { // Boolean
574580
$cell = $cell == 'TRUE';
575581
}
576582
}
577583

578584
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type);
585+
if ($arrayFormulaRange === null) {
586+
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(null);
587+
} else {
588+
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayFormulaRange]);
589+
}
579590
if (isset($cellAttributes->ValueFormat)) {
580591
$this->spreadsheet->getActiveSheet()->getCell($column . $row)
581592
->getStyle()->getNumberFormat()
582593
->setFormatCode((string) $cellAttributes->ValueFormat);
583594
}
584595
}
596+
597+
private function getArrayFormulaRange(string $column, int $row, int $cols, int $rows): string
598+
{
599+
$arrayFormulaRange = $column . $row;
600+
$arrayFormulaRange .= ':'
601+
. Coordinate::stringFromColumnIndex(
602+
Coordinate::columnIndexFromString($column)
603+
+ $cols - 1
604+
)
605+
. (string) ($row + $rows - 1);
606+
607+
return $arrayFormulaRange;
608+
}
585609
}

tests/PhpSpreadsheetTests/Calculation/CalculationTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
88
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
99
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10+
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
1011
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1112
use PhpOffice\PhpSpreadsheet\Spreadsheet;
1213
use PHPUnit\Framework\TestCase;
@@ -102,8 +103,14 @@ public function testCellSetAsQuotedText(): void
102103
self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
103104

104105
$cell2 = $workSheet->getCell('A2');
105-
$cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA);
106-
self::assertEquals('ABC', $cell2->getCalculatedValue());
106+
107+
try {
108+
$cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA);
109+
self::assertEquals('ABC', $cell2->getCalculatedValue());
110+
self::fail('setValueExplicit with invalid formula should have thrown exception');
111+
} catch (SpreadsheetException $e) {
112+
self::assertStringContainsString('Invalid value for datatype Formula', $e->getMessage());
113+
}
107114

108115
$cell3 = $workSheet->getCell('A3');
109116
$cell3->setValueExplicit('=', DataType::TYPE_FORMULA);
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
6+
7+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class InternalFunctionsTest extends TestCase
12+
{
13+
private string $arrayReturnType;
14+
15+
protected function setUp(): void
16+
{
17+
$this->arrayReturnType = Calculation::getArrayReturnType();
18+
}
19+
20+
protected function tearDown(): void
21+
{
22+
Calculation::setArrayReturnType($this->arrayReturnType);
23+
}
24+
25+
/**
26+
* @dataProvider anchorArrayDataProvider
27+
*/
28+
public function testAnchorArrayFormula(string $reference, string $range, array $expectedResult): void
29+
{
30+
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY);
31+
$spreadsheet = new Spreadsheet();
32+
$sheet1 = $spreadsheet->getActiveSheet();
33+
$sheet1->setTitle('SheetOne'); // no space in sheet title
34+
$sheet2 = $spreadsheet->createSheet();
35+
$sheet2->setTitle('Sheet Two'); // space in sheet title
36+
37+
$sheet1->setCellValue('C3', '=SEQUENCE(3,3,-4)');
38+
$sheet2->setCellValue('C3', '=SEQUENCE(3,3, 9, -1)');
39+
$sheet1->calculateArrays();
40+
$sheet2->calculateArrays();
41+
$sheet1->setCellValue('A8', "=ANCHORARRAY({$reference})");
42+
43+
$result1 = $sheet1->getCell('A8')->getCalculatedValue();
44+
self::assertSame($expectedResult, $result1);
45+
$attributes1 = $sheet1->getCell('A8')->getFormulaAttributes();
46+
self::assertSame(['t' => 'array', 'ref' => $range], $attributes1);
47+
$spreadsheet->disconnectWorksheets();
48+
}
49+
50+
public static function anchorArrayDataProvider(): array
51+
{
52+
return [
53+
[
54+
'C3',
55+
'A8:C10',
56+
[[-4, -3, -2], [-1, 0, 1], [2, 3, 4]],
57+
],
58+
[
59+
"'Sheet Two'!C3",
60+
'A8:C10',
61+
[[9, 8, 7], [6, 5, 4], [3, 2, 1]],
62+
],
63+
];
64+
}
65+
66+
/**
67+
* @dataProvider singleDataProvider
68+
*/
69+
public function testSingleArrayFormula(string $reference, mixed $expectedResult): void
70+
{
71+
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY);
72+
$spreadsheet = new Spreadsheet();
73+
$sheet1 = $spreadsheet->getActiveSheet();
74+
$sheet1->setTitle('SheetOne'); // no space in sheet title
75+
$sheet2 = $spreadsheet->createSheet();
76+
$sheet2->setTitle('Sheet Two'); // space in sheet title
77+
78+
$sheet1->setCellValue('C3', '=SEQUENCE(3,3,-4)');
79+
$sheet2->setCellValue('C3', '=SEQUENCE(3,3, 9, -1)');
80+
81+
$sheet1->setCellValue('A8', "=SINGLE({$reference})");
82+
$sheet1->setCellValue('G3', 'three');
83+
$sheet1->setCellValue('G4', 'four');
84+
$sheet1->setCellValue('G5', 'five');
85+
$sheet1->setCellValue('G7', 'seven');
86+
$sheet1->setCellValue('G8', 'eight');
87+
$sheet1->setCellValue('G9', 'nine');
88+
89+
$sheet1->calculateArrays();
90+
$sheet2->calculateArrays();
91+
92+
$result1 = $sheet1->getCell('A8')->getCalculatedValue();
93+
self::assertSame($expectedResult, $result1);
94+
$spreadsheet->disconnectWorksheets();
95+
}
96+
97+
public static function singleDataProvider(): array
98+
{
99+
return [
100+
'array cell on same sheet' => [
101+
'C3',
102+
-4,
103+
],
104+
'array cell on different sheet' => [
105+
"'Sheet Two'!C3",
106+
9,
107+
],
108+
'range which includes current row' => [
109+
'G7:G9',
110+
'eight',
111+
],
112+
'range which does not include current row' => [
113+
'G3:G5',
114+
'#VALUE!',
115+
],
116+
];
117+
}
118+
}

0 commit comments

Comments
 (0)