Initial work implementing the new UNIQUE() Lookup/Reference array function
This commit is contained in:
parent
576fbc43c9
commit
c8cf193301
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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' => [
|
||||
|
|
|
|||
|
|
@ -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!';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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