Skip to content

Commit ad2194d

Browse files
committed
CONCATENATE Changes, and Csv/Html/Ods Support
The CONCATENATE function has been treated as equivalent to CONCAT. This is not how it is treated in Excel; it is closer to (and probably identical to) the ampersand concatenate operator. The difference manifests itself when any of the arguments is an array (typically a cell range). Code is added to support this difference. Support for array results is added to Csv Writer, Html Writer, and Ods Reader and Writer. I have not figured out how to get it to work with Xls.
1 parent 846fec7 commit ad2194d

27 files changed

+636
-71
lines changed

docs/references/function-list-by-category.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ CHAR | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Charac
529529
CLEAN | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Trim::nonPrintable
530530
CODE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::code
531531
CONCAT | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Concatenate::CONCATENATE
532-
CONCATENATE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Concatenate::CONCATENATE
532+
CONCATENATE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Concatenate::actualCONCATENATE
533533
DBCS | **Not yet Implemented**
534534
DOLLAR | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Format::DOLLAR
535535
EXACT | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Text::exact

docs/references/function-list-by-name.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ COMBIN | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpread
8989
COMBINA | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations::withRepetition
9090
COMPLEX | CATEGORY_ENGINEERING | \PhpOffice\PhpSpreadsheet\Calculation\Engineering\Complex::COMPLEX
9191
CONCAT | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Concatenate::CONCATENATE
92-
CONCATENATE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Concatenate::CONCATENATE
92+
CONCATENATE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Concatenate::actualCONCATENATE
9393
CONFIDENCE | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Confidence::CONFIDENCE
9494
CONFIDENCE.NORM | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Confidence::CONFIDENCE
9595
CONFIDENCE.T | CATEGORY_STATISTICAL | **Not yet Implemented**

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ public static function getExcelConstants(string $key): bool|null
598598
],
599599
'CONCATENATE' => [
600600
'category' => Category::CATEGORY_TEXT_AND_DATA,
601-
'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'],
601+
'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'],
602602
'argumentCount' => '1+',
603603
],
604604
'CONFIDENCE' => [
@@ -3703,7 +3703,7 @@ public function _calculateFormulaValue(string $formula, ?string $cellID = null,
37033703
* 1 = shrink to fit
37043704
* 2 = extend to fit
37053705
*/
3706-
private static function checkMatrixOperands(mixed &$operand1, mixed &$operand2, int $resize = 1): array
3706+
public static function checkMatrixOperands(mixed &$operand1, mixed &$operand2, int $resize = 1): array
37073707
{
37083708
// Examine each of the two operands, and turn them into an array if they aren't one already
37093709
// Note that this function should only be called if one or both of the operand is already an array
@@ -5643,7 +5643,7 @@ public function getSuppressFormulaErrors(): bool
56435643
return $this->suppressFormulaErrorsNew;
56445644
}
56455645

5646-
private static function boolToString(mixed $operand1): mixed
5646+
public static function boolToString(mixed $operand1): mixed
56475647
{
56485648
if (is_bool($operand1)) {
56495649
$operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];

src/PhpSpreadsheet/Calculation/TextData/Concatenate.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
44

55
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
67
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
78
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
89
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
@@ -14,7 +15,7 @@ class Concatenate
1415
use ArrayEnabled;
1516

1617
/**
17-
* CONCATENATE.
18+
* This implements the CONCAT function, *not* CONCATENATE.
1819
*
1920
* @param array $args
2021
*/
@@ -43,6 +44,57 @@ public static function CONCATENATE(...$args): string
4344
return $returnValue;
4445
}
4546

47+
/**
48+
* This implements the CONCATENATE function.
49+
*
50+
* @param array $args data to be concatenated
51+
*/
52+
public static function actualCONCATENATE(...$args): array|string
53+
{
54+
$result = '';
55+
foreach ($args as $operand2) {
56+
$result = self::concatenate2Args($result, $operand2);
57+
if (ErrorValue::isError($result) === true) {
58+
break;
59+
}
60+
}
61+
62+
return $result;
63+
}
64+
65+
private static function concatenate2Args(array|string $operand1, null|array|bool|float|int|string $operand2): array|string
66+
{
67+
if (is_array($operand1) || is_array($operand2)) {
68+
$operand1 = Calculation::boolToString($operand1);
69+
$operand2 = Calculation::boolToString($operand2);
70+
[$rows, $columns] = Calculation::checkMatrixOperands($operand1, $operand2, 2);
71+
$errorFound = false;
72+
for ($row = 0; $row < $rows && !$errorFound; ++$row) {
73+
for ($column = 0; $column < $columns; ++$column) {
74+
if (ErrorValue::isError($operand2[$row][$column])) {
75+
return $operand2[$row][$column];
76+
}
77+
$operand1[$row][$column]
78+
= Calculation::boolToString($operand1[$row][$column])
79+
. Calculation::boolToString($operand2[$row][$column]);
80+
if (mb_strlen($operand1[$row][$column]) > DataType::MAX_STRING_LENGTH) {
81+
$operand1 = ExcelError::CALC();
82+
$errorFound = true;
83+
84+
break;
85+
}
86+
}
87+
}
88+
} else {
89+
$operand1 .= (string) Calculation::boolToString($operand2);
90+
if (mb_strlen($operand1) > DataType::MAX_STRING_LENGTH) {
91+
$operand1 = ExcelError::CALC();
92+
}
93+
}
94+
95+
return $operand1;
96+
}
97+
4698
/**
4799
* TEXTJOIN.
48100
*

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ private function convertDateTimeInt(mixed $result): mixed
349349
public function getCalculatedValueString(): string
350350
{
351351
$value = $this->getCalculatedValue();
352+
while (is_array($value)) {
353+
$value = array_shift($value);
354+
}
352355

353356
return ($value === '' || is_scalar($value) || $value instanceof Stringable) ? "$value" : '';
354357
}
@@ -362,17 +365,19 @@ public function getCalculatedValueString(): string
362365
*/
363366
public function getCalculatedValue(bool $resetLog = true): mixed
364367
{
368+
$title = 'unknown';
365369
if ($this->dataType === DataType::TYPE_FORMULA) {
366370
try {
367-
$index = $this->getWorksheet()->getParentOrThrow()->getActiveSheetIndex();
368-
$selected = $this->getWorksheet()->getSelectedCells();
371+
$thisworksheet = $this->getWorksheet();
372+
$title = $thisworksheet->getTitle();
373+
$index = $thisworksheet->getParentOrThrow()->getActiveSheetIndex();
374+
$selected = $thisworksheet->getSelectedCells();
369375
$result = Calculation::getInstance(
370-
$this->getWorksheet()->getParent()
376+
$thisworksheet->getParent()
371377
)->calculateCellValue($this, $resetLog);
372378
$result = $this->convertDateTimeInt($result);
373-
$this->getWorksheet()->setSelectedCells($selected);
374-
$this->getWorksheet()->getParentOrThrow()->setActiveSheetIndex($index);
375-
// We don't yet handle array returns
379+
$thisworksheet->setSelectedCells($selected);
380+
$thisworksheet->getParentOrThrow()->setActiveSheetIndex($index);
376381
if (is_array($result) && Calculation::getArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
377382
while (is_array($result)) {
378383
$result = array_shift($result);
@@ -390,21 +395,28 @@ public function getCalculatedValue(bool $resetLog = true): mixed
390395
}
391396
}
392397
}
398+
$newColumn = $this->getColumn();
393399
if (is_array($result)) {
394400
$newRow = $row = $this->getRow();
395401
$column = $this->getColumn();
396402
foreach ($result as $resultRow) {
397-
$newColumn = $column;
398-
$resultRowx = is_array($resultRow) ? $resultRow : [$resultRow];
399-
foreach ($resultRowx as $resultValue) {
403+
if (is_array($resultRow)) {
404+
$newColumn = $column;
405+
foreach ($resultRow as $resultValue) {
406+
if ($row !== $newRow || $column !== $newColumn) {
407+
$thisworksheet->getCell($newColumn . $newRow)->setValue($resultValue);
408+
}
409+
++$newColumn;
410+
}
411+
++$newRow;
412+
} else {
400413
if ($row !== $newRow || $column !== $newColumn) {
401-
$this->getWorksheet()->getCell($newColumn . $newRow)->setValue($resultValue);
414+
$thisworksheet->getCell($newColumn . $newRow)->setValue($resultRow);
402415
}
403416
++$newColumn;
404417
}
405-
++$newRow;
406418
}
407-
$this->getWorksheet()->getCell($column . $row);
419+
$thisworksheet->getCell($column . $row);
408420
}
409421
} catch (SpreadsheetException $ex) {
410422
if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
@@ -414,7 +426,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
414426
}
415427

416428
throw new CalculationException(
417-
$this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
429+
$title . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
418430
$ex->getCode(),
419431
$ex
420432
);

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,9 @@ private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDat
309309
}
310310
$attr = $c->f->attributes();
311311
$cellDataType = DataType::TYPE_FORMULA;
312-
$value = "={$c->f}";
312+
$formula = (string) $c->f;
313+
$formula = str_replace(['_xlfn.', '_xlws.'], '', $formula);
314+
$value = "=$formula";
313315
$calculatedValue = self::$castBaseType($c);
314316

315317
// Shared formula?

src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
220220
if (count($cfRule->formula) >= 1) {
221221
foreach ($cfRule->formula as $formulax) {
222222
$formula = (string) $formulax;
223+
$formula = str_replace(['_xlfn.', '_xlws.'], '', $formula);
223224
if ($formula === 'TRUE') {
224225
$objConditional->addCondition(true);
225226
} elseif ($formula === 'FALSE') {

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3673,4 +3673,14 @@ public function copyCells(string $fromCell, string $toCells, bool $copyStyle = t
36733673
}
36743674
}
36753675
}
3676+
3677+
public function calculateArrays(bool $preCalculateFormulas = true): void
3678+
{
3679+
if ($preCalculateFormulas && Calculation::getArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
3680+
$keys = $this->cellCollection->getCoordinates();
3681+
foreach ($keys as $key) {
3682+
$this->getCell($key)->getCalculatedValue();
3683+
}
3684+
}
3685+
}
36763686
}

src/PhpSpreadsheet/Writer/Csv.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ public function save($filename, int $flags = 0): void
7676

7777
$saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
7878
Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
79-
$saveArrayReturnType = Calculation::getArrayReturnType();
80-
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
79+
$sheet->calculateArrays($this->preCalculateFormulas);
8180

8281
// Open file
8382
$this->openFileHandle($filename);
@@ -110,7 +109,6 @@ public function save($filename, int $flags = 0): void
110109
}
111110

112111
$this->maybeCloseFileHandle();
113-
Calculation::setArrayReturnType($saveArrayReturnType);
114112
Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
115113
}
116114

src/PhpSpreadsheet/Writer/Html.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,15 @@ public function save($filename, int $flags = 0): void
173173
*/
174174
public function generateHtmlAll(): string
175175
{
176+
$sheets = $this->generateSheetPrep();
177+
foreach ($sheets as $sheet) {
178+
$sheet->calculateArrays($this->preCalculateFormulas);
179+
}
176180
// garbage collect
177181
$this->spreadsheet->garbageCollect();
178182

179183
$saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
180184
Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
181-
$saveArrayReturnType = Calculation::getArrayReturnType();
182-
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
183185

184186
// Build CSS
185187
$this->buildCSS(!$this->useInlineCss);
@@ -204,7 +206,6 @@ public function generateHtmlAll(): string
204206
$html = $callback($html);
205207
}
206208

207-
Calculation::setArrayReturnType($saveArrayReturnType);
208209
Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
209210

210211
return $html;
@@ -404,11 +405,9 @@ public function generateHTMLHeader(bool $includeStyles = false): string
404405
return $html;
405406
}
406407

408+
/** @return Worksheet[] */
407409
private function generateSheetPrep(): array
408410
{
409-
// Ensure that Spans have been calculated?
410-
$this->calculateSpans();
411-
412411
// Fetch sheets
413412
if ($this->sheetIndex === null) {
414413
$sheets = $this->spreadsheet->getAllSheets();
@@ -456,6 +455,8 @@ private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int
456455
*/
457456
public function generateSheetData(): string
458457
{
458+
// Ensure that Spans have been calculated?
459+
$this->calculateSpans();
459460
$sheets = $this->generateSheetPrep();
460461

461462
// Construct HTML
@@ -484,7 +485,7 @@ public function generateSheetData(): string
484485
$html .= $startTag;
485486

486487
// Write row if there are HTML table cells in it
487-
if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
488+
if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParentOrThrow()->getIndex($sheet)][$row])) {
488489
// Start a new rowData
489490
$rowData = [];
490491
// Loop through columns

0 commit comments

Comments
 (0)