Initial work on setting an array formula through code, and populating spillage cells when calculating the value

No provision yet for the single operator, or for handlng #SPILL! or #CALC! errors
This commit is contained in:
MarkBaker
2022-01-31 18:25:49 +01:00
parent 6a51ad4f36
commit 1d941b4ffb
10 changed files with 213 additions and 35 deletions
@@ -15,10 +15,8 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
*
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*
* @return bool
*/
public function bindValue(Cell $cell, $value = null)
public function bindValue(Cell $cell, $value, bool $isArrayFormula = false, ?string $arrayFormulaRange = null): bool
{
if ($value === null) {
return parent::bindValue($cell, $value);
@@ -119,7 +117,7 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
}
// Not bound yet? Use parent...
return parent::bindValue($cell, $value);
return parent::bindValue($cell, $value, $isArrayFormula, $arrayFormulaRange);
}
protected function setImproperFraction(array $matches, Cell $cell): bool
+67 -10
View File
@@ -190,15 +190,27 @@ class Cell
*
* @return $this
*/
public function setValue($value)
public function setValue($value, bool $isArrayFormula = false, ?string $arrayFormulaRange = null)
{
if (!self::getValueBinder()->bindValue($this, $value)) {
if (!self::getValueBinder()->bindValue($this, $value, $isArrayFormula, $arrayFormulaRange)) {
throw new Exception('Value could not be bound to cell.');
}
return $this;
}
protected function formulaAttributes(bool $isArrayFormula, ?string $arrayFormulaRange): array
{
if ($isArrayFormula === true) {
return [
't' => 'array',
'ref' => $arrayFormulaRange === null ? $this->getCoordinate() : $arrayFormulaRange,
];
}
return [];
}
/**
* Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
*
@@ -207,7 +219,7 @@ class Cell
*
* @return Cell
*/
public function setValueExplicit($value, $dataType)
public function setValueExplicit($value, $dataType, bool $isArrayFormula = false, ?string $arrayFormulaRange = null)
{
// set the value according to data type
switch ($dataType) {
@@ -233,7 +245,17 @@ class Cell
break;
case DataType::TYPE_FORMULA:
$this->value = (string) $value;
if (is_string($value) !== true || strpos($value, '=') !== 0) {
$dataType = DataType::TYPE_STRING;
if (in_array($value, Calculation::$excelConstants, true)) {
$value = array_search($value, Calculation::$excelConstants, true);
}
$value = (string) $value;
$this->formulaAttributes = [];
} else {
$this->formulaAttributes = $this->formulaAttributes($isArrayFormula, $arrayFormulaRange);
}
$this->value = $value;
break;
case DataType::TYPE_BOOL:
@@ -283,19 +305,42 @@ class Cell
{
if ($this->dataType == DataType::TYPE_FORMULA) {
try {
$coordinate = $this->getCoordinate();
$worksheet = $this->getWorksheet();
$value = $this->value;
$datatype = $this->dataType;
$formulaAttributes = $this->formulaAttributes;
$index = $this->getWorksheet()->getParent()->getActiveSheetIndex();
$selected = $this->getWorksheet()->getSelectedCells();
$result = Calculation::getInstance(
$this->getWorksheet()->getParent()
)->calculateCellValue($this, $resetLog);
$this->getWorksheet()->setSelectedCells($selected);
$this->getWorksheet()->getParent()->setActiveSheetIndex($index);
// We don't yet handle array returns
if (is_array($result)) {
// We'll need to do a check here for the Singular Operator (@) at some point
// and not populate the spillage cells if it's there
if ($this->isArrayFormula()) {
// Here is where we should set all cellRange values from the result (but within the range limit)
// How are we going to handle a #SPILL! error?
$worksheet->fromArray($result, null, $coordinate, true);
// fromArray() will 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
$worksheet->getCell($coordinate);
$this->updateInCollection();
$this->value = $value;
$this->dataType = $datatype;
$this->formulaAttributes = $formulaAttributes;
}
// Now we just extract the top-left value from the array to get the result for this specific cell
while (is_array($result)) {
$result = array_shift($result);
}
}
$this->getWorksheet()->setSelectedCells($selected);
$this->getWorksheet()->getParent()->setActiveSheetIndex($index);
} catch (Exception $ex) {
if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
return $this->calculatedValue; // Fallback for calculations referencing external files.
@@ -380,14 +425,26 @@ class Cell
/**
* Identify if the cell contains a formula.
*
* @return bool
*/
public function isFormula()
public function isFormula(): bool
{
return $this->dataType == DataType::TYPE_FORMULA;
}
/**
* Identify if the cell contains an array formula.
*/
public function isArrayFormula(): bool
{
if ($this->dataType === DataType::TYPE_FORMULA) {
$formulaAttributes = $this->getFormulaAttributes();
return isset($formulaAttributes['t']) && $formulaAttributes['t'] === 'array';
}
return false;
}
/**
* Does this cell contain Data validation rules?
*
@@ -13,10 +13,8 @@ class DefaultValueBinder implements IValueBinder
*
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*
* @return bool
*/
public function bindValue(Cell $cell, $value)
public function bindValue(Cell $cell, $value, bool $isArrayFormula = false, ?string $arrayFormulaRange = null): bool
{
// sanitize UTF-8 strings
if (is_string($value)) {
@@ -32,7 +30,7 @@ class DefaultValueBinder implements IValueBinder
}
// Set value explicit
$cell->setValueExplicit($value, static::dataTypeForValue($value));
$cell->setValueExplicit($value, static::dataTypeForValue($value), $isArrayFormula, $arrayFormulaRange);
// Done!
return true;
+1 -1
View File
@@ -12,5 +12,5 @@ interface IValueBinder
*
* @return bool
*/
public function bindValue(Cell $cell, $value);
public function bindValue(Cell $cell, $value, bool $isArrayFormula = false, ?string $arrayFormulaRange = null);
}
@@ -77,7 +77,7 @@ class StringValueBinder implements IValueBinder
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*/
public function bindValue(Cell $cell, $value)
public function bindValue(Cell $cell, $value, bool $isArrayFormula = false, ?string $arrayFormulaRange = null): bool
{
if (is_object($value)) {
return $this->bindObjectValue($cell, $value);
@@ -95,7 +95,7 @@ class StringValueBinder implements IValueBinder
} elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) {
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) {
$cell->setValueExplicit($value, DataType::TYPE_FORMULA);
$cell->setValueExplicit($value, DataType::TYPE_FORMULA, $isArrayFormula, $arrayFormulaRange);
} else {
if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
$cell->getStyle()->setQuotePrefix(true);
+5 -2
View File
@@ -826,6 +826,9 @@ class Xlsx extends BaseReader
}
$cell = $docSheet->getCell($r);
$formulaAttributes = $cell->getFormulaAttributes();
$isArrayFormula = isset($formulaAttributes['t']) && $formulaAttributes['t'] === 'array';
$arrayFormulaRange = isset($formulaAttributes['ref']) ? $formulaAttributes['ref'] : null;
// Assign value
if ($cellDataType != '') {
// it is possible, that datatype is numeric but with an empty string, which result in an error
@@ -833,10 +836,10 @@ class Xlsx extends BaseReader
$cellDataType = DataType::TYPE_NULL;
}
if ($cellDataType !== DataType::TYPE_NULL) {
$cell->setValueExplicit($value, $cellDataType);
$cell->setValueExplicit($value, $cellDataType, $isArrayFormula, $arrayFormulaRange);
}
} else {
$cell->setValue($value);
$cell->setValue($value, $isArrayFormula, $arrayFormulaRange);
}
if ($calculatedValue !== null) {
$cell->setCalculatedValue($calculatedValue);
+27 -11
View File
@@ -1110,9 +1110,9 @@ class Worksheet implements IComparable
*
* @return $this
*/
public function setCellValue($coordinate, $value)
public function setCellValue($coordinate, $value, bool $isArrayFormula = false, ?string $arrayFormulaRange = null)
{
$this->getCell($coordinate)->setValue($value);
$this->getCell($coordinate)->setValue($value, $isArrayFormula, $arrayFormulaRange);
return $this;
}
@@ -1126,9 +1126,14 @@ class Worksheet implements IComparable
*
* @return $this
*/
public function setCellValueByColumnAndRow($columnIndex, $row, $value)
{
$this->getCellByColumnAndRow($columnIndex, $row)->setValue($value);
public function setCellValueByColumnAndRow(
$columnIndex,
$row,
$value,
bool $isArrayFormula = false,
?string $arrayFormulaRange = null
) {
$this->getCellByColumnAndRow($columnIndex, $row)->setValue($value, $isArrayFormula, $arrayFormulaRange);
return $this;
}
@@ -1142,10 +1147,15 @@ class Worksheet implements IComparable
*
* @return $this
*/
public function setCellValueExplicit($coordinate, $value, $dataType)
{
public function setCellValueExplicit(
$coordinate,
$value,
$dataType,
bool $isArrayFormula = false,
?string $arrayFormulaRange = null
) {
// Set value
$this->getCell($coordinate)->setValueExplicit($value, $dataType);
$this->getCell($coordinate)->setValueExplicit($value, $dataType, $isArrayFormula, $arrayFormulaRange);
return $this;
}
@@ -1160,9 +1170,15 @@ class Worksheet implements IComparable
*
* @return $this
*/
public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
{
$this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType);
public function setCellValueExplicitByColumnAndRow(
$columnIndex,
$row,
$value,
$dataType,
bool $isArrayFormula = false,
?string $arrayFormulaRange = null
) {
$this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType, $isArrayFormula, $arrayFormulaRange);
return $this;
}
+1 -1
View File
@@ -1270,7 +1270,7 @@ class Worksheet extends WriterPart
$objWriter->writeAttribute('ref', $attributes['ref'] ?? $cell->getCoordinate());
$objWriter->writeAttribute('aca', '1');
$objWriter->writeAttribute('ca', '1');
$objWriter->text(substr($cellValue, 1));
$objWriter->text(Xlfn::addXlfnStripEquals($cellValue));
$objWriter->endElement();
} else {
$objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
@@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Cell;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class CellFormulaTest extends TestCase
{
public function testSetFormulaExplicit(): void
{
$formula = '=A2+B2';
$spreadsheet = new Spreadsheet();
$cell = $spreadsheet->getActiveSheet()->getCell('A1');
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
self::assertSame($formula, $cell->getValue());
self::assertTrue($cell->isFormula());
self::assertFalse($cell->isArrayFormula());
$spreadsheet->disconnectWorksheets();
}
public function testSetFormulaDeterminedByBinder(): void
{
$formula = '=A2+B2';
$spreadsheet = new Spreadsheet();
$cell = $spreadsheet->getActiveSheet()->getCell('A1');
$cell->setValue($formula);
self::assertSame($formula, $cell->getValue());
self::assertTrue($cell->isFormula());
self::assertFalse($cell->isArrayFormula());
$spreadsheet->disconnectWorksheets();
}
public function testSetFormulaInvalidValue(): void
{
$formula = true;
$spreadsheet = new Spreadsheet();
$cell = $spreadsheet->getActiveSheet()->getCell('A1');
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
self::assertSame('TRUE', $cell->getValue());
self::assertFalse($cell->isFormula());
$spreadsheet->disconnectWorksheets();
}
public function testSetFormulaInvalidFormulaValue(): void
{
$formula = 'invalid formula';
$spreadsheet = new Spreadsheet();
$cell = $spreadsheet->getActiveSheet()->getCell('A1');
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
self::assertSame($formula, $cell->getValue());
self::assertFalse($cell->isFormula());
$spreadsheet->disconnectWorksheets();
}
public function testSetArrayFormulaExplicitNoRange(): void
{
$formula = '=SUM(B2:B6*C2:C6)';
$spreadsheet = new Spreadsheet();
$cell = $spreadsheet->getActiveSheet()->getCell('A1');
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA, true);
self::assertSame($formula, $cell->getValue());
self::assertTrue($cell->isFormula());
self::assertTrue($cell->isArrayFormula());
self::assertArrayHasKey('ref', $cell->getFormulaAttributes());
self::assertSame('A1', $cell->getFormulaAttributes()['ref']);
$spreadsheet->disconnectWorksheets();
}
public function testSetArrayFormulaExplicitWithRange(): void
{
$formula = '=SEQUENCE(3,3,-10,2.5)';
$spreadsheet = new Spreadsheet();
$cell = $spreadsheet->getActiveSheet()->getCell('A1');
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA, true, 'A1:C3');
self::assertSame($formula, $cell->getValue());
self::assertTrue($cell->isFormula());
self::assertTrue($cell->isArrayFormula());
self::assertArrayHasKey('ref', $cell->getFormulaAttributes());
self::assertSame('A1:C3', $cell->getFormulaAttributes()['ref']);
$spreadsheet->disconnectWorksheets();
}
}
@@ -16,6 +16,7 @@ class ArrayFormulaTest extends AbstractFunctional
$cellFormulaAttributes = $spreadsheet->getActiveSheet()->getCell('A1')->getFormulaAttributes();
self::assertArrayHasKey('t', $cellFormulaAttributes);
self::assertSame('array', $cellFormulaAttributes['t']);
self::assertArrayHasKey('ref', $cellFormulaAttributes);
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');