mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-04 06:33:33 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user