Skip to content

Commit f467ac5

Browse files
authored
Merge pull request #4596 from oleibman/intersect
Some Additional Support for Intersection and Union
2 parents 2f1092c + 4fe13f0 commit f467ac5

File tree

6 files changed

+164
-18
lines changed

6 files changed

+164
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
3636
- Php8.5 deprecates use of null as array index. [PR #4634](https://github.com/PHPOffice/PhpSpreadsheet/pull/4634)
3737
- For Php8.5, replace one of our two uses of `__wakeup` with `__unserialize`, and eliminate the other. [PR #4639](https://github.com/PHPOffice/PhpSpreadsheet/pull/4639)
3838
- Use prefix _xlfn for BASE function. [Issue #4638](https://github.com/PHPOffice/PhpSpreadsheet/issues/4638) [PR #4641](https://github.com/PHPOffice/PhpSpreadsheet/pull/4641)
39+
- Additional support for union and intersection. [PR #4596](https://github.com/PHPOffice/PhpSpreadsheet/pull/4596)
3940

4041
## 2025-09-03 - 5.1.0
4142

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,10 @@ private function internalParseFormula(string $formula, ?Cell $cell = null): bool
10951095
$this->branchPruner->initialiseForLoop();
10961096

10971097
$opCharacter = $formula[$index]; // Get the first character of the value at the current index position
1098+
if ($opCharacter === "\xe2") { // intersection or union
1099+
$opCharacter .= $formula[++$index];
1100+
$opCharacter .= $formula[++$index];
1101+
}
10981102

10991103
// Check for two-character operators (e.g. >=, <=, <>)
11001104
if ((isset(self::COMPARISON_OPERATORS[$opCharacter])) && (strlen($formula) > $index) && isset($formula[$index + 1], self::COMPARISON_OPERATORS[$formula[$index + 1]])) {
@@ -1115,7 +1119,7 @@ private function internalParseFormula(string $formula, ?Cell $cell = null): bool
11151119
++$index;
11161120
} elseif ($opCharacter === '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded?
11171121
++$index; // Drop the redundant plus symbol
1118-
} elseif ((($opCharacter === '~') || ($opCharacter === '') || ($opCharacter === '')) && (!$isOperandOrFunction)) {
1122+
} elseif ((($opCharacter === '~') /*|| ($opCharacter === '∩') || ($opCharacter === '∪')*/) && (!$isOperandOrFunction)) {
11191123
// We have to explicitly deny a tilde, union or intersect because they are legal
11201124
return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression
11211125
} elseif ((isset(self::CALCULATION_OPERATORS[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack?
@@ -1233,6 +1237,15 @@ private function internalParseFormula(string $formula, ?Cell $cell = null): bool
12331237
// MS Excel allows this if the content is cell references; but doesn't allow actual values,
12341238
// but at this point, we can't differentiate (so allow both)
12351239
return $this->raiseFormulaError('Formula Error: Unexpected ,');
1240+
/* The following code may be a better choice, but, with
1241+
the other changes for this PR, I can no longer come up
1242+
with a test case that gets here
1243+
$stack->push('Binary Operator', '∪');
1244+
1245+
++$index;
1246+
$expectingOperator = false;
1247+
1248+
continue;*/
12361249
}
12371250

12381251
/** @var array<string, int> $d */
@@ -1927,6 +1940,14 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
19271940
$stack->push('Value', $cellIntersect, $cellRef);
19281941
}
19291942

1943+
break;
1944+
case '': // union
1945+
/** @var mixed[][] $operand1 */
1946+
/** @var mixed[][] $operand2 */
1947+
$cellUnion = array_merge($operand1, $operand2);
1948+
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellUnion));
1949+
$stack->push('Value', $cellUnion, 'A1');
1950+
19301951
break;
19311952
}
19321953
} elseif (($token === '~') || ($token === '%')) {
@@ -2792,6 +2813,14 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
27922813

27932814
$definedNameValue = $namedRange->getValue();
27942815
$definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
2816+
if ($definedNameType === 'Range') {
2817+
if (preg_match('/^(.*!)?(.*)$/', $definedNameValue, $matches) === 1) {
2818+
$matches2 = trim($matches[2]);
2819+
$matches2 = preg_replace('/ +/', '', $matches2) ?? $matches2;
2820+
$matches2 = preg_replace('/,/', '', $matches2) ?? $matches2;
2821+
$definedNameValue = $matches[1] . $matches2;
2822+
}
2823+
}
27952824
$definedNameWorksheet = $namedRange->getWorksheet();
27962825

27972826
if ($definedNameValue[0] !== '=') {

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
453453
}
454454
$newColumn = $this->getColumn();
455455
if (is_array($result)) {
456+
$result = self::convertSpecialArray($result);
456457
$this->formulaAttributes['t'] = 'array';
457458
$this->formulaAttributes['ref'] = $maxCoordinate = $coordinate;
458459
$newRow = $row = $this->getRow();
@@ -581,6 +582,36 @@ public function getCalculatedValue(bool $resetLog = true): mixed
581582
return $this->convertDateTimeInt($this->value);
582583
}
583584

585+
/**
586+
* Convert array like the following (preserve values, lose indexes):
587+
* [
588+
* rowNumber1 => [colLetter1 => value, colLetter2 => value ...],
589+
* rowNumber2 => [colLetter1 => value, colLetter2 => value ...],
590+
* ...
591+
* ].
592+
*
593+
* @param mixed[] $array
594+
*
595+
* @return mixed[]
596+
*/
597+
private static function convertSpecialArray(array $array): array
598+
{
599+
$newArray = [];
600+
foreach ($array as $rowIndex => $row) {
601+
if (!is_int($rowIndex) || $rowIndex <= 0 || !is_array($row)) {
602+
return $array;
603+
}
604+
$keys = array_keys($row);
605+
$key0 = $keys[0] ?? '';
606+
if (!is_string($key0)) {
607+
return $array;
608+
}
609+
$newArray[] = array_values($row);
610+
}
611+
612+
return $newArray;
613+
}
614+
584615
/**
585616
* Set old calculated value (cached).
586617
*

tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
class RangeTest extends TestCase
1515
{
16-
private string $incompleteMessage = 'Must be revisited';
17-
1816
private ?Spreadsheet $spreadSheet = null;
1917

2018
protected function getSpreadsheet(): Spreadsheet
@@ -162,9 +160,6 @@ public static function providerUTF8NamedRangeEvaluation(): array
162160
#[DataProvider('providerCompositeNamedRangeEvaluation')]
163161
public function testCompositeNamedRangeEvaluation(string $composite, int $expectedSum, int $expectedCount): void
164162
{
165-
if ($this->incompleteMessage !== '') {
166-
self::markTestIncomplete($this->incompleteMessage);
167-
}
168163
$this->spreadSheet = $this->getSpreadsheet();
169164

170165
$workSheet = $this->spreadSheet->getActiveSheet();
@@ -182,17 +177,47 @@ public function testCompositeNamedRangeEvaluation(string $composite, int $expect
182177
public static function providerCompositeNamedRangeEvaluation(): array
183178
{
184179
return [
185-
// Calculation engine doesn't yet handle union ranges with overlap
186180
'Union with overlap' => [
187-
'A1:C1,A3:C3,B1:C3',
188-
63,
181+
'$A$1:$C$1,$A$3:$C$3,$B$1:$C$3',
182+
99,
189183
12,
190184
],
191185
'Union and Intersection' => [
192-
'A1:C1,A3:C3 B1:C3',
193-
23,
186+
'$A$1:$C$1,$A$3:$C$3 $B$1:$C$3',
187+
35,
194188
5,
195189
],
196190
];
197191
}
192+
193+
public function testIntersectCellFormula(): void
194+
{
195+
$this->spreadSheet = $this->getSpreadsheet();
196+
197+
$sheet = $this->spreadSheet->getActiveSheet();
198+
$array = [
199+
[null, 'Planets', 'Lives', 'Babies'],
200+
['Batman', 5, 10, 4],
201+
['Superman', 4, 56, 34],
202+
['Spiderman', 23, 45, 67],
203+
['Hulk', 12, 34, 58],
204+
['Steve', 10, 34, 78],
205+
];
206+
$sheet->fromArray($array, null, 'A3', true);
207+
$this->spreadSheet->addNamedRange(new NamedRange('Hulk', $sheet, '$B$7:$D$7'));
208+
$this->spreadSheet->addNamedRange(new NamedRange('Planets', $sheet, '$B$4:$B$8'));
209+
$this->spreadSheet->addNamedRange(new NamedRange('Intersect', $sheet, '$A$6:$D$6 $C$4:$C$8'));
210+
$this->spreadSheet->addNamedRange(new NamedRange('SupHulk', $sheet, '$B$5:$D$5,$B$7:$D$7'));
211+
212+
$sheet->setCellValue('F1', '=Intersect');
213+
$sheet->setCellValue('F2', '=SUM(SupHulk)');
214+
$sheet->setCellValue('F3', '=Planets Hulk');
215+
$sheet->setCellValue('F4', '=B4:D4 B4:C5');
216+
217+
$this->spreadSheet->returnArrayAsArray();
218+
self::assertSame(45, $sheet->getCell('F1')->getCalculatedValue());
219+
self::assertSame(198, $sheet->getCell('F2')->getCalculatedValue());
220+
self::assertSame(12, $sheet->getCell('F3')->getCalculatedValue());
221+
self::assertSame([[5, 10]], $sheet->getCell('F4')->getCalculatedValue());
222+
}
198223
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Cell;
6+
7+
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
use ReflectionMethod;
11+
12+
class ConvertSpecialArrayTest extends TestCase
13+
{
14+
/**
15+
* @param mixed[] $expected
16+
* @param mixed[] $inArray
17+
*/
18+
#[DataProvider('providerSpecialArrays')]
19+
public function testConvertSpecialArray(array $expected, array $inArray): void
20+
{
21+
$reflectionMethod = new ReflectionMethod(Cell::class, 'convertSpecialArray');
22+
$result = $reflectionMethod->invokeArgs(null, [$inArray]);
23+
self::assertSame($expected, $result);
24+
}
25+
26+
public static function providerSpecialArrays(): array
27+
{
28+
return [
29+
'expected form row index to array indexed by column' => [
30+
[
31+
[1, 2],
32+
[3, 4],
33+
],
34+
[
35+
1 => ['A' => 1, 'B' => 2],
36+
2 => ['A' => 3, 'B' => 4],
37+
],
38+
],
39+
'standard array unchanged' => [
40+
[
41+
1 => [1, 2],
42+
2 => [3, 4],
43+
],
44+
[
45+
1 => [1, 2],
46+
2 => [3, 4],
47+
],
48+
],
49+
'uses index 0 so unchanged' => [
50+
[
51+
['A' => 1, 'B' => 2],
52+
['A' => 3, 'B' => 4],
53+
],
54+
[
55+
['A' => 1, 'B' => 2],
56+
['A' => 3, 'B' => 4],
57+
],
58+
],
59+
];
60+
}
61+
}

tests/PhpSpreadsheetTests/Worksheet/Table/Issue3659Test.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace PhpOffice\PhpSpreadsheetTests\Worksheet\Table;
66

7-
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
87
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
98

109
class Issue3659Test extends SetupTeardown
@@ -49,7 +48,7 @@ public function testTableOnOtherSheet(): void
4948
public function testTableAsArray(): void
5049
{
5150
$spreadsheet = $this->getSpreadsheet();
52-
Calculation::getInstance($spreadsheet)->setInstanceArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY);
51+
$spreadsheet->returnArrayAsArray();
5352
$sheet = $this->getSheet();
5453
$sheet->setTitle('Feuil1');
5554
$tableSheet = $spreadsheet->createSheet();
@@ -76,13 +75,13 @@ public function testTableAsArray(): void
7675
$sheet->getCell('F9')->setValue('=Tableau1');
7776
$sheet->getCell('J9')->setValue('=CONCAT(Tableau1)');
7877
$sheet->getCell('J11')->setValue('=SUM(Tableau1[])');
79-
$expectedResult = [2 => ['B' => 10], ['B' => 2], ['B' => 3], ['B' => 4]];
78+
$expectedResult = [[10], [2], [3], [4]];
8079
self::assertSame($expectedResult, $sheet->getCell('F1')->getCalculatedValue());
8180
$expectedResult = [
82-
2 => ['B' => 10, 'C' => 20, 'D' => null],
83-
['B' => 2, 'C' => null, 'D' => null],
84-
['B' => 3, 'C' => null, 'D' => null],
85-
['B' => 4, 'C' => null, 'D' => null],
81+
[10, 20, null],
82+
[2, null, null],
83+
[3, null, null],
84+
[4, null, null],
8685
];
8786
self::assertSame($expectedResult, $sheet->getCell('H1')->getCalculatedValue());
8887
self::assertSame($expectedResult, $sheet->getCell('F9')->getCalculatedValue());

0 commit comments

Comments
 (0)