Skip to content

Commit 4847e05

Browse files
authored
Merge pull request #2692 from PHPOffice/Implementation-of-UNIQUE()-Function
Initial work implementing the new UNIQUE() Lookup/Reference array function
2 parents 576fbc4 + c8cf193 commit 4847e05

File tree

5 files changed

+312
-3
lines changed

5 files changed

+312
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
99

1010
### Added
1111

12-
- Implementation of the ISREF() information function.
12+
- Implementation of the UNIQUE() Lookup/Reference (array) function
13+
- Implementation of the ISREF() Information function.
1314
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
1415

1516
(i.e a value of "12,345.67" can be read as numeric `1235.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled.

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2583,7 +2583,7 @@ class Calculation
25832583
],
25842584
'UNIQUE' => [
25852585
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2586-
'functionCall' => [Functions::class, 'DUMMY'],
2586+
'functionCall' => [LookupRef\Unique::class, 'unique'],
25872587
'argumentCount' => '1+',
25882588
],
25892589
'UPPER' => [

src/PhpSpreadsheet/Calculation/Information/ExcelError.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,20 @@ public static function NAME()
127127
/**
128128
* DIV0.
129129
*
130-
* @return string #Not Yet Implemented
130+
* @return string #DIV/0!
131131
*/
132132
public static function DIV0()
133133
{
134134
return self::$errorCodes['divisionbyzero'];
135135
}
136+
137+
/**
138+
* CALC.
139+
*
140+
* @return string #Not Yet Implemented
141+
*/
142+
public static function CALC()
143+
{
144+
return '#CALC!';
145+
}
136146
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
7+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
8+
9+
class Unique
10+
{
11+
/**
12+
* UNIQUE
13+
* The UNIQUE function searches for value either from a one-row or one-column range or from an array.
14+
*
15+
* @param mixed $lookupVector The range of cells being searched
16+
* @param mixed $byColumn Whether the uniqueness should be determined by row (the default) or by column
17+
* @param mixed $exactlyOnce Whether the function should return only entries that occur just once in the list
18+
*
19+
* @return mixed The unique values from the search range
20+
*/
21+
public static function unique($lookupVector, $byColumn = false, $exactlyOnce = false)
22+
{
23+
if (!is_array($lookupVector)) {
24+
// Scalars are always returned "as is"
25+
return $lookupVector;
26+
}
27+
28+
$byColumn = (bool) $byColumn;
29+
$exactlyOnce = (bool) $exactlyOnce;
30+
31+
return ($byColumn === true)
32+
? self::uniqueByColumn($lookupVector, $exactlyOnce)
33+
: self::uniqueByRow($lookupVector, $exactlyOnce);
34+
}
35+
36+
/**
37+
* @return mixed
38+
*/
39+
private static function uniqueByRow(array $lookupVector, bool $exactlyOnce)
40+
{
41+
// When not $byColumn, we count whole rows or values, not individual values
42+
// so implode each row into a single string value
43+
array_walk(
44+
$lookupVector,
45+
function (array &$value): void {
46+
$value = implode(chr(0x00), $value);
47+
}
48+
);
49+
50+
$result = self::countValuesCaseInsensitive($lookupVector);
51+
52+
if ($exactlyOnce === true) {
53+
$result = self::exactlyOnceFilter($result);
54+
}
55+
56+
if (count($result) === 0) {
57+
return ExcelError::CALC();
58+
}
59+
60+
$result = array_keys($result);
61+
62+
// restore rows from their strings
63+
array_walk(
64+
$result,
65+
function (string &$value): void {
66+
$value = explode(chr(0x00), $value);
67+
}
68+
);
69+
70+
return (count($result) === 1) ? array_pop($result) : $result;
71+
}
72+
73+
/**
74+
* @return mixed
75+
*/
76+
private static function uniqueByColumn(array $lookupVector, bool $exactlyOnce)
77+
{
78+
$flattenedLookupVector = Functions::flattenArray($lookupVector);
79+
80+
if (count($lookupVector, COUNT_RECURSIVE) > count($flattenedLookupVector, COUNT_RECURSIVE) + 1) {
81+
// We're looking at a full column check (multiple rows)
82+
$transpose = Matrix::transpose($lookupVector);
83+
$result = self::uniqueByRow($transpose, $exactlyOnce);
84+
85+
return (is_array($result)) ? Matrix::transpose($result) : $result;
86+
}
87+
88+
$result = self::countValuesCaseInsensitive($flattenedLookupVector);
89+
90+
if ($exactlyOnce === true) {
91+
$result = self::exactlyOnceFilter($result);
92+
}
93+
94+
if (count($result) === 0) {
95+
return ExcelError::CALC();
96+
}
97+
98+
$result = array_keys($result);
99+
100+
return $result;
101+
}
102+
103+
private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array
104+
{
105+
$caseInsensitiveCounts = array_count_values(
106+
array_map(
107+
function (string $value) {
108+
return StringHelper::strToUpper($value);
109+
},
110+
$caseSensitiveLookupValues
111+
)
112+
);
113+
114+
$caseSensitiveCounts = [];
115+
foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) {
116+
if (is_numeric($caseInsensitiveKey)) {
117+
$caseSensitiveCounts[$caseInsensitiveKey] = $count;
118+
} else {
119+
foreach ($caseSensitiveLookupValues as $caseSensitiveValue) {
120+
if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) {
121+
$caseSensitiveCounts[$caseSensitiveValue] = $count;
122+
123+
break;
124+
}
125+
}
126+
}
127+
}
128+
129+
return $caseSensitiveCounts;
130+
}
131+
132+
private static function exactlyOnceFilter(array $values): array
133+
{
134+
return array_filter(
135+
$values,
136+
function ($value) {
137+
return $value === 1;
138+
}
139+
);
140+
}
141+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
6+
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class UniqueTest extends TestCase
10+
{
11+
/**
12+
* @dataProvider uniqueTestProvider
13+
*/
14+
public function testUnique(array $expectedResult, ...$args): void
15+
{
16+
$result = LookupRef\Unique::unique(...$args);
17+
self::assertEquals($expectedResult, $result);
18+
}
19+
20+
public function testUniqueException(): void
21+
{
22+
$rowLookupData = [
23+
['Andrew', 'Brown'],
24+
['Betty', 'Johnson'],
25+
['Betty', 'Johnson'],
26+
['Andrew', 'Brown'],
27+
['David', 'White'],
28+
['Andrew', 'Brown'],
29+
['David', 'White'],
30+
];
31+
32+
$columnLookupData = [
33+
['PHP', 'Rocks', 'php', 'rocks'],
34+
];
35+
36+
$result = LookupRef\Unique::unique($rowLookupData, false, true);
37+
self::assertEquals(ExcelError::CALC(), $result);
38+
39+
$result = LookupRef\Unique::unique($columnLookupData, true, true);
40+
self::assertEquals(ExcelError::CALC(), $result);
41+
}
42+
43+
public function testUniqueWithScalar(): void
44+
{
45+
$lookupData = 123;
46+
47+
$result = LookupRef\Unique::unique($lookupData);
48+
self::assertSame($lookupData, $result);
49+
}
50+
51+
public function uniqueTestProvider(): array
52+
{
53+
return [
54+
[
55+
[['Red'], ['Green'], ['Blue'], ['Orange']],
56+
[
57+
['Red'],
58+
['Green'],
59+
['Green'],
60+
['Blue'],
61+
['Blue'],
62+
['Orange'],
63+
['Green'],
64+
['Blue'],
65+
['Red'],
66+
],
67+
],
68+
[
69+
[['Red'], ['Green'], ['Blue'], ['Orange']],
70+
[
71+
['Red'],
72+
['Green'],
73+
['GrEEn'],
74+
['Blue'],
75+
['BLUE'],
76+
['Orange'],
77+
['GReeN'],
78+
['blue'],
79+
['RED'],
80+
],
81+
],
82+
[
83+
['Orange'],
84+
[
85+
['Red'],
86+
['Green'],
87+
['Green'],
88+
['Blue'],
89+
['Blue'],
90+
['Orange'],
91+
['Green'],
92+
['Blue'],
93+
['Red'],
94+
],
95+
false,
96+
true,
97+
],
98+
[
99+
['Andrew', 'Betty', 'Robert', 'David'],
100+
[['Andrew', 'Betty', 'Robert', 'Andrew', 'Betty', 'Robert', 'David', 'Andrew']],
101+
true,
102+
],
103+
[
104+
['David'],
105+
[['Andrew', 'Betty', 'Robert', 'Andrew', 'Betty', 'Robert', 'David', 'Andrew']],
106+
true,
107+
true,
108+
],
109+
[
110+
[1, 1, 2, 2, 3],
111+
[[1, 1, 2, 2, 3]],
112+
],
113+
[
114+
[1, 2, 3],
115+
[[1, 1, 2, 2, 3]],
116+
true,
117+
],
118+
[
119+
[
120+
[1, 1, 2, 3],
121+
[1, 2, 2, 3],
122+
],
123+
[
124+
[1, 1, 2, 2, 3],
125+
[1, 2, 2, 2, 3],
126+
],
127+
true,
128+
],
129+
[
130+
[
131+
['Andrew', 'Brown'],
132+
['Betty', 'Johnson'],
133+
['David', 'White'],
134+
],
135+
[
136+
['Andrew', 'Brown'],
137+
['Betty', 'Johnson'],
138+
['Betty', 'Johnson'],
139+
['Andrew', 'Brown'],
140+
['David', 'White'],
141+
['Andrew', 'Brown'],
142+
['David', 'White'],
143+
],
144+
],
145+
[
146+
[[1.2], [2.1], [2.2], [3.0]],
147+
[
148+
[1.2],
149+
[1.2],
150+
[2.1],
151+
[2.2],
152+
[3.0],
153+
],
154+
],
155+
];
156+
}
157+
}

0 commit comments

Comments
 (0)