Merge pull request #2692 from PHPOffice/Implementation-of-UNIQUE()-Function
Initial work implementing the new UNIQUE() Lookup/Reference array function
This commit is contained in:
commit
4847e05212
|
|
@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Implementation of the ISREF() information function.
|
- Implementation of the UNIQUE() Lookup/Reference (array) function
|
||||||
|
- Implementation of the ISREF() Information function.
|
||||||
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
|
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
|
||||||
|
|
||||||
(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.
|
(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.
|
||||||
|
|
|
||||||
|
|
@ -2583,7 +2583,7 @@ class Calculation
|
||||||
],
|
],
|
||||||
'UNIQUE' => [
|
'UNIQUE' => [
|
||||||
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
|
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
|
||||||
'functionCall' => [Functions::class, 'DUMMY'],
|
'functionCall' => [LookupRef\Unique::class, 'unique'],
|
||||||
'argumentCount' => '1+',
|
'argumentCount' => '1+',
|
||||||
],
|
],
|
||||||
'UPPER' => [
|
'UPPER' => [
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,20 @@ class ExcelError
|
||||||
/**
|
/**
|
||||||
* DIV0.
|
* DIV0.
|
||||||
*
|
*
|
||||||
* @return string #Not Yet Implemented
|
* @return string #DIV/0!
|
||||||
*/
|
*/
|
||||||
public static function DIV0()
|
public static function DIV0()
|
||||||
{
|
{
|
||||||
return self::$errorCodes['divisionbyzero'];
|
return self::$errorCodes['divisionbyzero'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CALC.
|
||||||
|
*
|
||||||
|
* @return string #Not Yet Implemented
|
||||||
|
*/
|
||||||
|
public static function CALC()
|
||||||
|
{
|
||||||
|
return '#CALC!';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||||
|
|
||||||
|
class Unique
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* UNIQUE
|
||||||
|
* The UNIQUE function searches for value either from a one-row or one-column range or from an array.
|
||||||
|
*
|
||||||
|
* @param mixed $lookupVector The range of cells being searched
|
||||||
|
* @param mixed $byColumn Whether the uniqueness should be determined by row (the default) or by column
|
||||||
|
* @param mixed $exactlyOnce Whether the function should return only entries that occur just once in the list
|
||||||
|
*
|
||||||
|
* @return mixed The unique values from the search range
|
||||||
|
*/
|
||||||
|
public static function unique($lookupVector, $byColumn = false, $exactlyOnce = false)
|
||||||
|
{
|
||||||
|
if (!is_array($lookupVector)) {
|
||||||
|
// Scalars are always returned "as is"
|
||||||
|
return $lookupVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
$byColumn = (bool) $byColumn;
|
||||||
|
$exactlyOnce = (bool) $exactlyOnce;
|
||||||
|
|
||||||
|
return ($byColumn === true)
|
||||||
|
? self::uniqueByColumn($lookupVector, $exactlyOnce)
|
||||||
|
: self::uniqueByRow($lookupVector, $exactlyOnce);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private static function uniqueByRow(array $lookupVector, bool $exactlyOnce)
|
||||||
|
{
|
||||||
|
// When not $byColumn, we count whole rows or values, not individual values
|
||||||
|
// so implode each row into a single string value
|
||||||
|
array_walk(
|
||||||
|
$lookupVector,
|
||||||
|
function (array &$value): void {
|
||||||
|
$value = implode(chr(0x00), $value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = self::countValuesCaseInsensitive($lookupVector);
|
||||||
|
|
||||||
|
if ($exactlyOnce === true) {
|
||||||
|
$result = self::exactlyOnceFilter($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($result) === 0) {
|
||||||
|
return ExcelError::CALC();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array_keys($result);
|
||||||
|
|
||||||
|
// restore rows from their strings
|
||||||
|
array_walk(
|
||||||
|
$result,
|
||||||
|
function (string &$value): void {
|
||||||
|
$value = explode(chr(0x00), $value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (count($result) === 1) ? array_pop($result) : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private static function uniqueByColumn(array $lookupVector, bool $exactlyOnce)
|
||||||
|
{
|
||||||
|
$flattenedLookupVector = Functions::flattenArray($lookupVector);
|
||||||
|
|
||||||
|
if (count($lookupVector, COUNT_RECURSIVE) > count($flattenedLookupVector, COUNT_RECURSIVE) + 1) {
|
||||||
|
// We're looking at a full column check (multiple rows)
|
||||||
|
$transpose = Matrix::transpose($lookupVector);
|
||||||
|
$result = self::uniqueByRow($transpose, $exactlyOnce);
|
||||||
|
|
||||||
|
return (is_array($result)) ? Matrix::transpose($result) : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = self::countValuesCaseInsensitive($flattenedLookupVector);
|
||||||
|
|
||||||
|
if ($exactlyOnce === true) {
|
||||||
|
$result = self::exactlyOnceFilter($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($result) === 0) {
|
||||||
|
return ExcelError::CALC();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array_keys($result);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array
|
||||||
|
{
|
||||||
|
$caseInsensitiveCounts = array_count_values(
|
||||||
|
array_map(
|
||||||
|
function (string $value) {
|
||||||
|
return StringHelper::strToUpper($value);
|
||||||
|
},
|
||||||
|
$caseSensitiveLookupValues
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$caseSensitiveCounts = [];
|
||||||
|
foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) {
|
||||||
|
if (is_numeric($caseInsensitiveKey)) {
|
||||||
|
$caseSensitiveCounts[$caseInsensitiveKey] = $count;
|
||||||
|
} else {
|
||||||
|
foreach ($caseSensitiveLookupValues as $caseSensitiveValue) {
|
||||||
|
if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) {
|
||||||
|
$caseSensitiveCounts[$caseSensitiveValue] = $count;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $caseSensitiveCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function exactlyOnceFilter(array $values): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$values,
|
||||||
|
function ($value) {
|
||||||
|
return $value === 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class UniqueTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider uniqueTestProvider
|
||||||
|
*/
|
||||||
|
public function testUnique(array $expectedResult, ...$args): void
|
||||||
|
{
|
||||||
|
$result = LookupRef\Unique::unique(...$args);
|
||||||
|
self::assertEquals($expectedResult, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueException(): void
|
||||||
|
{
|
||||||
|
$rowLookupData = [
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['Betty', 'Johnson'],
|
||||||
|
['Betty', 'Johnson'],
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['David', 'White'],
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['David', 'White'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$columnLookupData = [
|
||||||
|
['PHP', 'Rocks', 'php', 'rocks'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = LookupRef\Unique::unique($rowLookupData, false, true);
|
||||||
|
self::assertEquals(ExcelError::CALC(), $result);
|
||||||
|
|
||||||
|
$result = LookupRef\Unique::unique($columnLookupData, true, true);
|
||||||
|
self::assertEquals(ExcelError::CALC(), $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueWithScalar(): void
|
||||||
|
{
|
||||||
|
$lookupData = 123;
|
||||||
|
|
||||||
|
$result = LookupRef\Unique::unique($lookupData);
|
||||||
|
self::assertSame($lookupData, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uniqueTestProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
[['Red'], ['Green'], ['Blue'], ['Orange']],
|
||||||
|
[
|
||||||
|
['Red'],
|
||||||
|
['Green'],
|
||||||
|
['Green'],
|
||||||
|
['Blue'],
|
||||||
|
['Blue'],
|
||||||
|
['Orange'],
|
||||||
|
['Green'],
|
||||||
|
['Blue'],
|
||||||
|
['Red'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[['Red'], ['Green'], ['Blue'], ['Orange']],
|
||||||
|
[
|
||||||
|
['Red'],
|
||||||
|
['Green'],
|
||||||
|
['GrEEn'],
|
||||||
|
['Blue'],
|
||||||
|
['BLUE'],
|
||||||
|
['Orange'],
|
||||||
|
['GReeN'],
|
||||||
|
['blue'],
|
||||||
|
['RED'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['Orange'],
|
||||||
|
[
|
||||||
|
['Red'],
|
||||||
|
['Green'],
|
||||||
|
['Green'],
|
||||||
|
['Blue'],
|
||||||
|
['Blue'],
|
||||||
|
['Orange'],
|
||||||
|
['Green'],
|
||||||
|
['Blue'],
|
||||||
|
['Red'],
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['Andrew', 'Betty', 'Robert', 'David'],
|
||||||
|
[['Andrew', 'Betty', 'Robert', 'Andrew', 'Betty', 'Robert', 'David', 'Andrew']],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['David'],
|
||||||
|
[['Andrew', 'Betty', 'Robert', 'Andrew', 'Betty', 'Robert', 'David', 'Andrew']],
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[1, 1, 2, 2, 3],
|
||||||
|
[[1, 1, 2, 2, 3]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[1, 2, 3],
|
||||||
|
[[1, 1, 2, 2, 3]],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[1, 1, 2, 3],
|
||||||
|
[1, 2, 2, 3],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[1, 1, 2, 2, 3],
|
||||||
|
[1, 2, 2, 2, 3],
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['Betty', 'Johnson'],
|
||||||
|
['David', 'White'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['Betty', 'Johnson'],
|
||||||
|
['Betty', 'Johnson'],
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['David', 'White'],
|
||||||
|
['Andrew', 'Brown'],
|
||||||
|
['David', 'White'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[[1.2], [2.1], [2.2], [3.0]],
|
||||||
|
[
|
||||||
|
[1.2],
|
||||||
|
[1.2],
|
||||||
|
[2.1],
|
||||||
|
[2.2],
|
||||||
|
[3.0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue