Skip to content

Commit 9bd828e

Browse files
committed
Strange Behavior of CONCATENATE
Fix PHPOffice#4061. CONCATENATE, which has slightly different behavior than CONCAT, and which MS has deprecated for that reason, behaves in an unexpected way when a cell range is presented to it and the spreadsheet does not allow for array results. This would almost certainly occur only for Legacy spreadsheets, but such is what was presented in the issue. The code is changed so that when an array of cells is presented to CONCATENATE, and RETURN_ARRAY_AS_VALUE is in effect, the array will be treated as if it were wrapped in the SINGLE pseudo-function (which is what Excel does by somewhat mysteriously prefixing the cell range with `@`). This is a niche case. This one stands out because of its deprecation and replacement function. It is possible that other functions exhibit this behavior. I have made no attempt to identify others. A similar approach can probably be applied if issues are raised for others.
1 parent 12095e5 commit 9bd828e

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

src/PhpSpreadsheet/Calculation/FunctionArray.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ class FunctionArray extends CalculationBase
405405
'category' => Category::CATEGORY_TEXT_AND_DATA,
406406
'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'],
407407
'argumentCount' => '1+',
408+
'passCellReference' => true,
408409
],
409410
'CONFIDENCE' => [
410411
'category' => Category::CATEGORY_STATISTICAL,

src/PhpSpreadsheet/Calculation/Functions.php

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

55
use PhpOffice\PhpSpreadsheet\Cell\Cell;
6+
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
67
use PhpOffice\PhpSpreadsheet\Shared\Date;
78
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
89

@@ -360,4 +361,47 @@ public static function trimSheetFromCellReference(string $coordinate): string
360361

361362
return $coordinate;
362363
}
364+
365+
/** @param mixed[] $array */
366+
public static function convertArrayToCellRange(array $array): string
367+
{
368+
$retVal = '';
369+
$lastRow = $lastColumn = $firstRow = $firstColumn = 0;
370+
foreach ($array as $rowkey => $row) {
371+
if (!is_array($row) || !is_int($rowkey) || $rowkey < 1) {
372+
$firstRow = 0;
373+
374+
break;
375+
}
376+
if ($firstRow > $rowkey || $firstRow === 0) {
377+
$firstRow = $rowkey;
378+
}
379+
if ($lastRow < $rowkey) {
380+
$lastRow = $rowkey;
381+
}
382+
foreach ($row as $colkey => $cellValue) {
383+
if (!preg_match('/^[A-Z]{1,3}$/', $colkey)) {
384+
$firstRow = 0;
385+
386+
break 2;
387+
}
388+
$column = Coordinate::columnIndexFromString($colkey);
389+
if ($firstColumn > $column || $firstColumn === 0) {
390+
$firstColumn = $column;
391+
}
392+
if ($lastColumn < $column) {
393+
$lastColumn = $column;
394+
}
395+
}
396+
}
397+
if ($firstRow > 0 && $firstColumn > 0 && ($firstRow !== $lastRow || $firstColumn !== $lastColumn)) {
398+
$retVal = Coordinate::stringFromColumnIndex($firstColumn)
399+
. $firstRow
400+
. ':'
401+
. Coordinate::stringFromColumnIndex($lastColumn)
402+
. $lastRow;
403+
}
404+
405+
return $retVal;
406+
}
363407
}

src/PhpSpreadsheet/Calculation/TextData/Concatenate.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
88
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
99
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
10+
use PhpOffice\PhpSpreadsheet\Calculation\Internal\ExcelArrayPseudoFunctions;
11+
use PhpOffice\PhpSpreadsheet\Cell\Cell;
1012
use PhpOffice\PhpSpreadsheet\Cell\DataType;
1113
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1214

@@ -17,7 +19,7 @@ class Concatenate
1719
/**
1820
* This implements the CONCAT function, *not* CONCATENATE.
1921
*
20-
* @param mixed[] $args
22+
* @param mixed $args data to be concatenated
2123
*/
2224
public static function CONCATENATE(...$args): string
2325
{
@@ -47,17 +49,33 @@ public static function CONCATENATE(...$args): string
4749
/**
4850
* This implements the CONCATENATE function.
4951
*
50-
* @param mixed[] $args data to be concatenated
52+
* @param mixed $args data to be concatenated
5153
*
5254
* @return array<string>|string
5355
*/
5456
public static function actualCONCATENATE(...$args): array|string
5557
{
58+
$useSingle = false;
59+
$cell = null;
60+
$count = count($args);
61+
if ($args[$count - 1] instanceof Cell) {
62+
/** @var Cell */
63+
$cell = array_pop($args);
64+
$type = $cell->getWorksheet()->getParent()?->getCalculationEngine()->getInstanceArrayReturnType() ?? Calculation::getArrayReturnType();
65+
$useSingle = $type === Calculation::RETURN_ARRAY_AS_VALUE;
66+
}
5667
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) {
5768
return self::CONCATENATE(...$args);
5869
}
5970
$result = '';
6071
foreach ($args as $operand2) {
72+
if ($useSingle && $cell instanceof Cell && is_array($operand2)) {
73+
$temp = Functions::convertArrayToCellRange($operand2);
74+
if ($temp !== '') {
75+
$operand2 = ExcelArrayPseudoFunctions::single($temp, $cell);
76+
}
77+
}
78+
/** @var null|array<mixed>|bool|float|int|string $operand2 */
6179
$result = self::concatenate2Args($result, $operand2);
6280
if (ErrorValue::isError($result, true) === true) {
6381
break;

tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateRangeTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
66

77
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
89

910
class ConcatenateRangeTest extends AllSetupTeardown
1011
{
@@ -38,4 +39,47 @@ public function testIssue4061(): void
3839
$sheet->getCell('F1')->setValue('=CONCAT(A1:A3, "-", C1:C3)');
3940
self::assertSame('abc-123', $sheet->getCell('F1')->getCalculatedValue());
4041
}
42+
43+
public function testIssue4061Value(): void
44+
{
45+
$sheet = $this->getSheet();
46+
$sheet->getCell('A1')->setValue('a');
47+
$sheet->getCell('A2')->setValue('b');
48+
$sheet->getCell('A3')->setValue('c');
49+
$sheet->getCell('C1')->setValue('1');
50+
$sheet->getCell('C2')->setValue('2');
51+
$sheet->getCell('C3')->setValue('3');
52+
$sheet->getCell('B1')->setValue('=CONCATENATE(A:A, "-", C:C)');
53+
$sheet->getCell('B2')->setValue('=CONCATENATE(A:A, "-", C:C)');
54+
$sheet->getCell('B3')->setValue('=CONCATENATE(A:A, "-", C:C)');
55+
Calculation::getInstance($this->getSpreadsheet())
56+
->setInstanceArrayReturnType(
57+
Calculation::RETURN_ARRAY_AS_VALUE
58+
);
59+
self::assertSame('a-1', $sheet->getCell('B1')->getCalculatedValue());
60+
self::assertSame('b-2', $sheet->getCell('B2')->getCalculatedValue());
61+
self::assertSame('c-3', $sheet->getCell('B3')->getCalculatedValue());
62+
}
63+
64+
public function testConvertCellRangeEdgeCases(): void
65+
{
66+
$array1 = [
67+
1 => ['A' => 'a', 'B' => 'd'],
68+
'B' => ['A' => 'b', 'B' => 'e'],
69+
3 => ['A' => 'c', 'B' => 'f'],
70+
];
71+
self::assertSame('', Functions::convertArrayToCellRange($array1));
72+
$array2 = [
73+
1 => ['A' => 'a', 'B' => 'd'],
74+
2 => ['A' => 'b', 6 => 'e'],
75+
3 => ['A' => 'c', 'B' => 'f'],
76+
];
77+
self::assertSame('', Functions::convertArrayToCellRange($array2));
78+
$array3 = [
79+
1 => ['A' => 'a', 'B' => 'd'],
80+
2 => ['A' => 'b', 'B' => 'e'],
81+
3 => ['A' => 'c', 'B' => 'f'],
82+
];
83+
self::assertSame('A1:B3', Functions::convertArrayToCellRange($array3));
84+
}
4185
}

tests/data/Calculation/TextData/CONCATENATE.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Cell\DataType;
66

77
return [
8-
/*[
8+
[
99
'ABCDEFGHIJ',
1010
'ABCDE',
1111
'FGHIJ',
@@ -34,6 +34,6 @@
3434
'A3',
3535
'abc',
3636
'def',
37-
],*/
37+
],
3838
'propagate DIV0' => ['#DIV/0!', '1', 'A2', '3'],
3939
];

0 commit comments

Comments
 (0)