Skip to content

Commit

Permalink
SINGLE Function, and Gnumeric
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
oleibman committed Jun 21, 2024
1 parent 0b471ef commit 3a690a7
Show file tree
Hide file tree
Showing 12 changed files with 591 additions and 8 deletions.
10 changes: 8 additions & 2 deletions src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3733,7 +3733,9 @@ public static function checkMatrixOperands(mixed &$operand1, mixed &$operand2, i

[$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
[$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
if ($resize === 3) {
$resize = 2;
} elseif (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
$resize = 1;
}

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

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

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

if (!is_array($functionCall)) {
Expand Down Expand Up @@ -5268,7 +5274,7 @@ private function executeNumericBinaryOperation(mixed $operand1, mixed $operand2,
$operand2[$key] = Functions::flattenArray($value);
}
}
[$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2);
[$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 3);

for ($row = 0; $row < $rows; ++$row) {
for ($column = 0; $column < $columns; ++$column) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,32 @@

class ExcelArrayPseudoFunctions
{
public static function single(string $cellReference, Cell $cell): array|string
public static function single(string $cellReference, Cell $cell): mixed
{
$worksheet = $cell->getWorksheet();

[$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true);
if (preg_match('/^([$]?[a-z]{1,3})([$]?([0-9]{1,7})):([$]?[a-z]{1,3})([$]?([0-9]{1,7}))$/i', "$referenceCellCoordinate", $matches) === 1) {
$ourRow = $cell->getRow();
$firstRow = (int) $matches[3];
$lastRow = (int) $matches[6];
if ($ourRow < $firstRow || $ourRow > $lastRow) {
return ExcelError::VALUE();
}
$referenceCellCoordinate = $matches[1] . $ourRow;
}
$referenceCell = ($referenceWorksheetName === '')
? $worksheet->getCell((string) $referenceCellCoordinate)
: $worksheet->getParentOrThrow()
->getSheetByNameOrThrow((string) $referenceWorksheetName)
->getCell((string) $referenceCellCoordinate);

$result = $referenceCell->getCalculatedValue();
while (is_array($result)) {
$result = array_shift($result);
}

return [[$result]];
return $result;
}

public static function anchorArray(string $cellReference, Cell $cell): array|string
Expand Down
3 changes: 3 additions & 0 deletions src/PhpSpreadsheet/Cell/Cell.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE

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

break;
Expand Down
28 changes: 26 additions & 2 deletions src/PhpSpreadsheet/Reader/Gnumeric.php
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,21 @@ private function loadCell(
): void {
$ValueType = $cellAttributes->ValueType;
$ExprID = (string) $cellAttributes->ExprID;
$rows = (int) ($cellAttributes->Rows ?? 0);
$cols = (int) ($cellAttributes->Cols ?? 0);
$type = DataType::TYPE_FORMULA;
$isArrayFormula = ($rows > 0 && $cols > 0);
$arrayFormulaRange = $isArrayFormula ? $this->getArrayFormulaRange($column, $row, $cols, $rows) : null;
if ($ExprID > '') {
if (((string) $cell) > '') {
// Formula
$this->expressions[$ExprID] = [
'column' => $cellAttributes->Col,
'row' => $cellAttributes->Row,
'formula' => (string) $cell,
];
} else {
// Shared Formula
$expression = $this->expressions[$ExprID];

$cell = $this->referenceHelper->updateFormulaReferences(
Expand All @@ -565,21 +571,39 @@ private function loadCell(
);
}
$type = DataType::TYPE_FORMULA;
} else {
} elseif ($isArrayFormula === false) {
$vtype = (string) $ValueType;
if (array_key_exists($vtype, self::$mappings['dataType'])) {
$type = self::$mappings['dataType'][$vtype];
}
if ($vtype === '20') { // Boolean
if ($vtype === '20') { // Boolean
$cell = $cell == 'TRUE';
}
}

$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type);
if ($arrayFormulaRange === null) {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(null);
} else {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayFormulaRange]);
}
if (isset($cellAttributes->ValueFormat)) {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)
->getStyle()->getNumberFormat()
->setFormatCode((string) $cellAttributes->ValueFormat);
}
}

private function getArrayFormulaRange(string $column, int $row, int $cols, int $rows): string
{
$arrayFormulaRange = $column . $row;
$arrayFormulaRange .= ':'
. Coordinate::stringFromColumnIndex(
Coordinate::columnIndexFromString($column)
+ $cols - 1
)
. (string) ($row + $rows - 1);

return $arrayFormulaRange;
}
}
11 changes: 9 additions & 2 deletions tests/PhpSpreadsheetTests/Calculation/CalculationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -102,8 +103,14 @@ public function testCellSetAsQuotedText(): void
self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());

$cell2 = $workSheet->getCell('A2');
$cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA);
self::assertEquals('ABC', $cell2->getCalculatedValue());

try {
$cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA);
self::assertEquals('ABC', $cell2->getCalculatedValue());
self::fail('setValueExplicit with invalid formula should have thrown exception');
} catch (SpreadsheetException $e) {
self::assertStringContainsString('Invalid value for datatype Formula', $e->getMessage());
}

$cell3 = $workSheet->getCell('A3');
$cell3->setValueExplicit('=', DataType::TYPE_FORMULA);
Expand Down
118 changes: 118 additions & 0 deletions tests/PhpSpreadsheetTests/Calculation/InternalFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Calculation;

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;

class InternalFunctionsTest extends TestCase
{
private string $arrayReturnType;

protected function setUp(): void
{
$this->arrayReturnType = Calculation::getArrayReturnType();
}

protected function tearDown(): void
{
Calculation::setArrayReturnType($this->arrayReturnType);
}

/**
* @dataProvider anchorArrayDataProvider
*/
public function testAnchorArrayFormula(string $reference, string $range, array $expectedResult): void
{
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY);
$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)');
$sheet2->setCellValue('C3', '=SEQUENCE(3,3, 9, -1)');
$sheet1->calculateArrays();
$sheet2->calculateArrays();
$sheet1->setCellValue('A8', "=ANCHORARRAY({$reference})");

$result1 = $sheet1->getCell('A8')->getCalculatedValue();
self::assertSame($expectedResult, $result1);
$attributes1 = $sheet1->getCell('A8')->getFormulaAttributes();
self::assertSame(['t' => 'array', 'ref' => $range], $attributes1);
$spreadsheet->disconnectWorksheets();
}

public static 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, mixed $expectedResult): void
{
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY);
$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)');
$sheet2->setCellValue('C3', '=SEQUENCE(3,3, 9, -1)');

$sheet1->setCellValue('A8', "=SINGLE({$reference})");
$sheet1->setCellValue('G3', 'three');
$sheet1->setCellValue('G4', 'four');
$sheet1->setCellValue('G5', 'five');
$sheet1->setCellValue('G7', 'seven');
$sheet1->setCellValue('G8', 'eight');
$sheet1->setCellValue('G9', 'nine');

$sheet1->calculateArrays();
$sheet2->calculateArrays();

$result1 = $sheet1->getCell('A8')->getCalculatedValue();
self::assertSame($expectedResult, $result1);
$spreadsheet->disconnectWorksheets();
}

public static function singleDataProvider(): array
{
return [
'array cell on same sheet' => [
'C3',
-4,
],
'array cell on different sheet' => [
"'Sheet Two'!C3",
9,
],
'range which includes current row' => [
'G7:G9',
'eight',
],
'range which does not include current row' => [
'G3:G5',
'#VALUE!',
],
];
}
}
Loading

0 comments on commit 3a690a7

Please sign in to comment.