Additional unit testing

And a quick bugfix for cell ranges applied to both sort functions and to FILTER()
This commit is contained in:
MarkBaker 2022-03-24 17:08:22 +01:00
parent 7f00049fe8
commit 9019523efc
3 changed files with 126 additions and 39 deletions

View File

@ -19,6 +19,8 @@ class Filter
return ExcelError::VALUE(); return ExcelError::VALUE();
} }
$matchArray = self::enumerateArrayKeys($matchArray);
$result = (Matrix::isColumnVector($matchArray)) $result = (Matrix::isColumnVector($matchArray))
? self::filterByRow($lookupArray, $matchArray) ? self::filterByRow($lookupArray, $matchArray)
: self::filterByColumn($lookupArray, $matchArray); : self::filterByColumn($lookupArray, $matchArray);
@ -30,6 +32,20 @@ class Filter
return array_values($result); return array_values($result);
} }
private static function enumerateArrayKeys(array $sortArray): array
{
array_walk(
$sortArray,
function (&$columns): void {
if (is_array($columns)) {
$columns = array_values($columns);
}
}
);
return array_values($sortArray);
}
private static function filterByRow(array $lookupArray, array $matchArray): array private static function filterByRow(array $lookupArray, array $matchArray): array
{ {
$matchArray = array_values(array_column($matchArray, 0)); $matchArray = array_values(array_column($matchArray, 0));

View File

@ -21,7 +21,7 @@ class Sort extends LookupRefValidations
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting. * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
* *
* @param mixed $sortArray The range of cells being sorted * @param mixed $sortArray The range of cells being sorted
* @param mixed $sortIndex Whether the uniqueness should be determined by row (the default) or by column * @param mixed $sortIndex The column or row number within the sortArray to sort on
* @param mixed $sortOrder Flag indicating whether to sort ascending or descending * @param mixed $sortOrder Flag indicating whether to sort ascending or descending
* Ascending = 1 (self::ORDER_ASCENDING) * Ascending = 1 (self::ORDER_ASCENDING)
* Descending = -1 (self::ORDER_DESCENDING) * Descending = -1 (self::ORDER_DESCENDING)
@ -29,13 +29,15 @@ class Sort extends LookupRefValidations
* *
* @return mixed The sorted values from the sort range * @return mixed The sorted values from the sort range
*/ */
public static function sort($sortArray, $sortIndex = [1], $sortOrder = self::ORDER_ASCENDING, $byColumn = false) public static function sort($sortArray, $sortIndex = 1, $sortOrder = self::ORDER_ASCENDING, $byColumn = false)
{ {
if (!is_array($sortArray)) { if (!is_array($sortArray)) {
// Scalars are always returned "as is" // Scalars are always returned "as is"
return $sortArray; return $sortArray;
} }
$sortArray = self::enumerateArrayKeys($sortArray);
$byColumn = (bool) $byColumn; $byColumn = (bool) $byColumn;
$lookupIndexSize = $byColumn ? count($sortArray) : count($sortArray[0]); $lookupIndexSize = $byColumn ? count($sortArray) : count($sortArray[0]);
@ -68,6 +70,12 @@ class Sort extends LookupRefValidations
* *
* @param mixed $sortArray The range of cells being sorted * @param mixed $sortArray The range of cells being sorted
* @param mixed $args * @param mixed $args
* At least one additional argument must be provided, The vector or range to sort on
* After that, arguments are passed as pairs:
* sort order: ascending or descending
* Ascending = 1 (self::ORDER_ASCENDING)
* Descending = -1 (self::ORDER_DESCENDING)
* additional arrays or ranges for multi-level sorting
* *
* @return mixed The sorted values from the sort range * @return mixed The sorted values from the sort range
*/ */
@ -78,6 +86,8 @@ class Sort extends LookupRefValidations
return $sortArray; return $sortArray;
} }
$sortArray = self::enumerateArrayKeys($sortArray);
$lookupArraySize = count($sortArray); $lookupArraySize = count($sortArray);
$argumentCount = count($args); $argumentCount = count($args);
@ -94,11 +104,25 @@ class Sort extends LookupRefValidations
return self::processSortBy($sortArray, $sortBy, $sortOrder); return self::processSortBy($sortArray, $sortBy, $sortOrder);
} }
private static function enumerateArrayKeys(array $sortArray): array
{
array_walk(
$sortArray,
function (&$columns): void {
if (is_array($columns)) {
$columns = array_values($columns);
}
}
);
return array_values($sortArray);
}
/** /**
* @param mixed $sortIndex * @param mixed $sortIndex
* @param mixed $sortOrder * @param mixed $sortOrder
*/ */
private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $lookupIndexSize): void private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void
{ {
if (is_array($sortIndex) || is_array($sortOrder)) { if (is_array($sortIndex) || is_array($sortOrder)) {
throw new Exception(ExcelError::VALUE()); throw new Exception(ExcelError::VALUE());
@ -106,7 +130,7 @@ class Sort extends LookupRefValidations
$sortIndex = self::validatePositiveInt($sortIndex, false); $sortIndex = self::validatePositiveInt($sortIndex, false);
if ($sortIndex > $lookupIndexSize) { if ($sortIndex > $sortArraySize) {
throw new Exception(ExcelError::VALUE()); throw new Exception(ExcelError::VALUE());
} }
@ -116,7 +140,7 @@ class Sort extends LookupRefValidations
/** /**
* @param mixed $sortVector * @param mixed $sortVector
*/ */
private static function validateSortVector($sortVector, int $lookupArraySize): array private static function validateSortVector($sortVector, int $sortArraySize): array
{ {
if (!is_array($sortVector)) { if (!is_array($sortVector)) {
throw new Exception(ExcelError::VALUE()); throw new Exception(ExcelError::VALUE());
@ -124,7 +148,7 @@ class Sort extends LookupRefValidations
// It doesn't matter if it's a row or a column vectors, it works either way // It doesn't matter if it's a row or a column vectors, it works either way
$sortVector = Functions::flattenArray($sortVector); $sortVector = Functions::flattenArray($sortVector);
if (count($sortVector) !== $lookupArraySize) { if (count($sortVector) !== $sortArraySize) {
throw new Exception(ExcelError::VALUE()); throw new Exception(ExcelError::VALUE());
} }
@ -148,14 +172,14 @@ class Sort extends LookupRefValidations
* @param array $sortIndex * @param array $sortIndex
* @param mixed $sortOrder * @param mixed $sortOrder
*/ */
private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $lookupIndexSize): void private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void
{ {
// It doesn't matter if they're row or column vectors, it works either way // It doesn't matter if they're row or column vectors, it works either way
$sortIndex = Functions::flattenArray($sortIndex); $sortIndex = Functions::flattenArray($sortIndex);
$sortOrder = Functions::flattenArray($sortOrder); $sortOrder = Functions::flattenArray($sortOrder);
if ( if (
count($sortOrder) === 0 || count($sortOrder) > $lookupIndexSize || count($sortOrder) === 0 || count($sortOrder) > $sortArraySize ||
(count($sortOrder) > count($sortIndex)) (count($sortOrder) > count($sortIndex))
) { ) {
throw new Exception(ExcelError::VALUE()); throw new Exception(ExcelError::VALUE());
@ -170,7 +194,7 @@ class Sort extends LookupRefValidations
} }
foreach ($sortIndex as $key => &$value) { foreach ($sortIndex as $key => &$value) {
self::validateScalarArgumentsForSort($value, $sortOrder[$key], $lookupIndexSize); self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize);
} }
} }
@ -195,7 +219,7 @@ class Sort extends LookupRefValidations
* @param array[] $sortIndex * @param array[] $sortIndex
* @param int[] $sortOrder * @param int[] $sortOrder
*/ */
private static function processSortBy(array $lookupArray, array $sortIndex, $sortOrder): array private static function processSortBy(array $sortArray, array $sortIndex, $sortOrder): array
{ {
$sortArguments = []; $sortArguments = [];
$sortData = []; $sortData = [];
@ -204,32 +228,32 @@ class Sort extends LookupRefValidations
$sortArguments[] = self::prepareSortVectorValues($sortValues); $sortArguments[] = self::prepareSortVectorValues($sortValues);
$sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
} }
$sortArguments = self::applyPHP7Patch($lookupArray, $sortArguments); $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments);
$sortVector = self::executeVectorSortQuery($sortData, $sortArguments); $sortVector = self::executeVectorSortQuery($sortData, $sortArguments);
return self::sortLookupArrayFromVector($lookupArray, $sortVector); return self::sortLookupArrayFromVector($sortArray, $sortVector);
} }
/** /**
* @param int[] $sortIndex * @param int[] $sortIndex
* @param int[] $sortOrder * @param int[] $sortOrder
*/ */
private static function sortByRow(array $lookupArray, array $sortIndex, array $sortOrder): array private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array
{ {
$sortVector = self::buildVectorForSort($lookupArray, $sortIndex, $sortOrder); $sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder);
return self::sortLookupArrayFromVector($lookupArray, $sortVector); return self::sortLookupArrayFromVector($sortArray, $sortVector);
} }
/** /**
* @param int[] $sortIndex * @param int[] $sortIndex
* @param int[] $sortOrder * @param int[] $sortOrder
*/ */
private static function sortByColumn(array $lookupArray, array $sortIndex, array $sortOrder): array private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array
{ {
$lookupArray = Matrix::transpose($lookupArray); $sortArray = Matrix::transpose($sortArray);
$result = self::sortByRow($lookupArray, $sortIndex, $sortOrder); $result = self::sortByRow($sortArray, $sortIndex, $sortOrder);
return Matrix::transpose($result); return Matrix::transpose($result);
} }
@ -238,17 +262,17 @@ class Sort extends LookupRefValidations
* @param int[] $sortIndex * @param int[] $sortIndex
* @param int[] $sortOrder * @param int[] $sortOrder
*/ */
private static function buildVectorForSort(array $lookupArray, array $sortIndex, array $sortOrder): array private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array
{ {
$sortArguments = []; $sortArguments = [];
$sortData = []; $sortData = [];
foreach ($sortIndex as $index => $sortIndexValue) { foreach ($sortIndex as $index => $sortIndexValue) {
$sortValues = array_column($lookupArray, $sortIndexValue - 1); $sortValues = array_column($sortArray, $sortIndexValue - 1);
$sortData[] = $sortValues; $sortData[] = $sortValues;
$sortArguments[] = self::prepareSortVectorValues($sortValues); $sortArguments[] = self::prepareSortVectorValues($sortValues);
$sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
} }
$sortArguments = self::applyPHP7Patch($lookupArray, $sortArguments); $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments);
$sortData = self::executeVectorSortQuery($sortData, $sortArguments); $sortData = self::executeVectorSortQuery($sortData, $sortArguments);
@ -279,12 +303,12 @@ class Sort extends LookupRefValidations
return $sortedData; return $sortedData;
} }
private static function sortLookupArrayFromVector(array $lookupArray, array $sortVector): array private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array
{ {
// Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays
$sortedArray = []; $sortedArray = [];
foreach ($sortVector as $index) { foreach ($sortVector as $index) {
$sortedArray[] = $lookupArray[$index]; $sortedArray[] = $sortArray[$index];
} }
return $sortedArray; return $sortedArray;
@ -306,10 +330,10 @@ class Sort extends LookupRefValidations
* MS Excel replicates the PHP 8.0.0 behaviour, retaining the original order of matching elements. * MS Excel replicates the PHP 8.0.0 behaviour, retaining the original order of matching elements.
* To replicate that behaviour with PHP 7, we add an extra sort based on the row index. * To replicate that behaviour with PHP 7, we add an extra sort based on the row index.
*/ */
private static function applyPHP7Patch(array $lookupArray, array $sortArguments): array private static function applyPHP7Patch(array $sortArray, array $sortArguments): array
{ {
if (PHP_VERSION_ID < 80000) { if (PHP_VERSION_ID < 80000) {
$sortArguments[] = range(1, count($lookupArray)); $sortArguments[] = range(1, count($sortArray));
$sortArguments[] = SORT_ASC; $sortArguments[] = SORT_ASC;
} }

View File

@ -12,7 +12,7 @@ class SortByTest extends TestCase
{ {
$value = 'NON-ARRAY'; $value = 'NON-ARRAY';
$result = Sort::sort($value, [1]); $result = Sort::sortBy($value);
self::assertSame($value, $result); self::assertSame($value, $result);
} }
@ -45,16 +45,16 @@ class SortByTest extends TestCase
/** /**
* @dataProvider providerSortByRow * @dataProvider providerSortByRow
*/ */
public function testSortByRow(array $expectedResult, array $matrix, array $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void public function testSortByRow(array $expectedResult, array $matrix, ...$args): void
{ {
$result = Sort::sortBy($matrix, $sortIndex, $sortOrder); $result = Sort::sortBy($matrix, ...$args);
self::assertSame($expectedResult, $result); self::assertSame($expectedResult, $result);
} }
public function providerSortByRow(): array public function providerSortByRow(): array
{ {
return [ return [
[ 'Simple sort by age' => [
[ [
['Fritz', 19], ['Fritz', 19],
['Xi', 19], ['Xi', 19],
@ -65,10 +65,10 @@ class SortByTest extends TestCase
['Hector', 66], ['Hector', 66],
['Sal', 73], ['Sal', 73],
], ],
$this->sampleDataForRow(), $this->sampleDataForSimpleSort(),
array_column($this->sampleDataForRow(), 1), array_column($this->sampleDataForSimpleSort(), 1),
], ],
[ 'Simple sort by name' => [
[ [
['Amy', 22], ['Amy', 22],
['Fred', 65], ['Fred', 65],
@ -79,10 +79,10 @@ class SortByTest extends TestCase
['Tom', 52], ['Tom', 52],
['Xi', 19], ['Xi', 19],
], ],
$this->sampleDataForRow(), $this->sampleDataForSimpleSort(),
array_column($this->sampleDataForRow(), 0), array_column($this->sampleDataForSimpleSort(), 0),
], ],
[ 'Row vector' => [
[ [
['Amy', 22], ['Amy', 22],
['Fred', 65], ['Fred', 65],
@ -93,10 +93,10 @@ class SortByTest extends TestCase
['Tom', 52], ['Tom', 52],
['Xi', 19], ['Xi', 19],
], ],
$this->sampleDataForRow(), $this->sampleDataForSimpleSort(),
['Tom', 'Fred', 'Amy', 'Sal', 'Fritz', 'Srivan', 'Xi', 'Hector'], ['Tom', 'Fred', 'Amy', 'Sal', 'Fritz', 'Srivan', 'Xi', 'Hector'],
], ],
[ 'Column vector' => [
[ [
['Amy', 22], ['Amy', 22],
['Fred', 65], ['Fred', 65],
@ -107,13 +107,46 @@ class SortByTest extends TestCase
['Tom', 52], ['Tom', 52],
['Xi', 19], ['Xi', 19],
], ],
$this->sampleDataForRow(), $this->sampleDataForSimpleSort(),
[['Tom'], ['Fred'], ['Amy'], ['Sal'], ['Fritz'], ['Srivan'], ['Xi'], ['Hector']], [['Tom'], ['Fred'], ['Amy'], ['Sal'], ['Fritz'], ['Srivan'], ['Xi'], ['Hector']],
], ],
'Sort by region asc, name asc' => [
[
['East', 'Fritz', 19],
['East', 'Tom', 52],
['North', 'Amy', 22],
['North', 'Xi', 19],
['South', 'Hector', 66],
['South', 'Sal', 73],
['West', 'Fred', 65],
['West', 'Srivan', 39],
],
$this->sampleDataForMultiSort(),
array_column($this->sampleDataForMultiSort(), 0),
Sort::ORDER_ASCENDING,
array_column($this->sampleDataForMultiSort(), 1),
],
'Sort by region asc, age desc' => [
[
['East', 'Tom', 52],
['East', 'Fritz', 19],
['North', 'Amy', 22],
['North', 'Xi', 19],
['South', 'Sal', 73],
['South', 'Hector', 66],
['West', 'Fred', 65],
['West', 'Srivan', 39],
],
$this->sampleDataForMultiSort(),
array_column($this->sampleDataForMultiSort(), 0),
Sort::ORDER_ASCENDING,
array_column($this->sampleDataForMultiSort(), 2),
Sort::ORDER_DESCENDING,
],
]; ];
} }
private function sampleDataForRow(): array private function sampleDataForSimpleSort(): array
{ {
return [ return [
['Tom', 52], ['Tom', 52],
@ -126,4 +159,18 @@ class SortByTest extends TestCase
['Hector', 66], ['Hector', 66],
]; ];
} }
private function sampleDataForMultiSort(): array
{
return [
['North', 'Amy', 22],
['West', 'Fred', 65],
['East', 'Fritz', 19],
['South', 'Hector', 66],
['South', 'Sal', 73],
['West', 'Srivan', 39],
['East', 'Tom', 52],
['North', 'Xi', 19],
];
}
} }