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:
Mark Baker 2022-03-18 14:40:52 +01:00 committed by GitHub
commit 4847e05212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 312 additions and 3 deletions

View File

@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### 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.
(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.

View File

@ -2583,7 +2583,7 @@ class Calculation
],
'UNIQUE' => [
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
'functionCall' => [Functions::class, 'DUMMY'],
'functionCall' => [LookupRef\Unique::class, 'unique'],
'argumentCount' => '1+',
],
'UPPER' => [

View File

@ -127,10 +127,20 @@ class ExcelError
/**
* DIV0.
*
* @return string #Not Yet Implemented
* @return string #DIV/0!
*/
public static function DIV0()
{
return self::$errorCodes['divisionbyzero'];
}
/**
* CALC.
*
* @return string #Not Yet Implemented
*/
public static function CALC()
{
return '#CALC!';
}
}

View File

@ -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;
}
);
}
}

View File

@ -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],
],
],
];
}
}