init
This commit is contained in:
commit
72a26edcff
22092 changed files with 2101903 additions and 0 deletions
309
lib/PhpSpreadsheet/Calculation/LookupRef/Sort.php
Normal file
309
lib/PhpSpreadsheet/Calculation/LookupRef/Sort.php
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class Sort extends LookupRefValidations
|
||||
{
|
||||
public const ORDER_ASCENDING = 1;
|
||||
public const ORDER_DESCENDING = -1;
|
||||
|
||||
/**
|
||||
* SORT
|
||||
* The SORT function returns a sorted array of the elements in an array.
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
*
|
||||
* @param mixed $sortArray The range of cells being sorted
|
||||
* @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
|
||||
* Ascending = 1 (self::ORDER_ASCENDING)
|
||||
* Descending = -1 (self::ORDER_DESCENDING)
|
||||
* @param mixed $byColumn Whether the sort should be determined by row (the default) or by column
|
||||
*
|
||||
* @return mixed The sorted values from the sort range
|
||||
*/
|
||||
public static function sort(mixed $sortArray, mixed $sortIndex = 1, mixed $sortOrder = self::ORDER_ASCENDING, mixed $byColumn = false): mixed
|
||||
{
|
||||
if (!is_array($sortArray)) {
|
||||
// Scalars are always returned "as is"
|
||||
return $sortArray;
|
||||
}
|
||||
|
||||
$sortArray = self::enumerateArrayKeys($sortArray);
|
||||
|
||||
$byColumn = (bool) $byColumn;
|
||||
$lookupIndexSize = $byColumn ? count($sortArray) : count($sortArray[0]);
|
||||
|
||||
try {
|
||||
// If $sortIndex and $sortOrder are scalars, then convert them into arrays
|
||||
if (is_scalar($sortIndex)) {
|
||||
$sortIndex = [$sortIndex];
|
||||
$sortOrder = is_scalar($sortOrder) ? [$sortOrder] : $sortOrder;
|
||||
}
|
||||
// but the values of those array arguments still need validation
|
||||
$sortOrder = (empty($sortOrder) ? [self::ORDER_ASCENDING] : $sortOrder);
|
||||
self::validateArrayArgumentsForSort($sortIndex, $sortOrder, $lookupIndexSize);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
// We want a simple, enumrated array of arrays where we can reference column by its index number.
|
||||
$sortArray = array_values(array_map('array_values', $sortArray));
|
||||
|
||||
return ($byColumn === true)
|
||||
? self::sortByColumn($sortArray, $sortIndex, $sortOrder)
|
||||
: self::sortByRow($sortArray, $sortIndex, $sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* SORTBY
|
||||
* The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array.
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
*
|
||||
* @param mixed $sortArray The range of cells being sorted
|
||||
* @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
|
||||
*/
|
||||
public static function sortBy(mixed $sortArray, mixed ...$args): mixed
|
||||
{
|
||||
if (!is_array($sortArray)) {
|
||||
// Scalars are always returned "as is"
|
||||
return $sortArray;
|
||||
}
|
||||
|
||||
$sortArray = self::enumerateArrayKeys($sortArray);
|
||||
|
||||
$lookupArraySize = count($sortArray);
|
||||
$argumentCount = count($args);
|
||||
|
||||
try {
|
||||
$sortBy = $sortOrder = [];
|
||||
for ($i = 0; $i < $argumentCount; $i += 2) {
|
||||
$sortBy[] = self::validateSortVector($args[$i], $lookupArraySize);
|
||||
$sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static function validateScalarArgumentsForSort(mixed &$sortIndex, mixed &$sortOrder, int $sortArraySize): void
|
||||
{
|
||||
if (is_array($sortIndex) || is_array($sortOrder)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
$sortIndex = self::validatePositiveInt($sortIndex, false);
|
||||
|
||||
if ($sortIndex > $sortArraySize) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
$sortOrder = self::validateSortOrder($sortOrder);
|
||||
}
|
||||
|
||||
private static function validateSortVector(mixed $sortVector, int $sortArraySize): array
|
||||
{
|
||||
if (!is_array($sortVector)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
// It doesn't matter if it's a row or a column vectors, it works either way
|
||||
$sortVector = Functions::flattenArray($sortVector);
|
||||
if (count($sortVector) !== $sortArraySize) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
return $sortVector;
|
||||
}
|
||||
|
||||
private static function validateSortOrder(mixed $sortOrder): int
|
||||
{
|
||||
$sortOrder = self::validateInt($sortOrder);
|
||||
if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
return $sortOrder;
|
||||
}
|
||||
|
||||
private static function validateArrayArgumentsForSort(array &$sortIndex, mixed &$sortOrder, int $sortArraySize): void
|
||||
{
|
||||
// It doesn't matter if they're row or column vectors, it works either way
|
||||
$sortIndex = Functions::flattenArray($sortIndex);
|
||||
$sortOrder = Functions::flattenArray($sortOrder);
|
||||
|
||||
if (
|
||||
count($sortOrder) === 0 || count($sortOrder) > $sortArraySize
|
||||
|| (count($sortOrder) > count($sortIndex))
|
||||
) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
if (count($sortIndex) > count($sortOrder)) {
|
||||
// If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated.
|
||||
$sortOrder = array_merge(
|
||||
$sortOrder,
|
||||
array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder))
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($sortIndex as $key => &$value) {
|
||||
self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize);
|
||||
}
|
||||
}
|
||||
|
||||
private static function prepareSortVectorValues(array $sortVector): array
|
||||
{
|
||||
// Strings should be sorted case-insensitive; with booleans converted to locale-strings
|
||||
return array_map(
|
||||
function ($value) {
|
||||
if (is_bool($value)) {
|
||||
return ($value) ? Calculation::getTRUE() : Calculation::getFALSE();
|
||||
} elseif (is_string($value)) {
|
||||
return StringHelper::strToLower($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
$sortVector
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $sortIndex
|
||||
* @param int[] $sortOrder
|
||||
*/
|
||||
private static function processSortBy(array $sortArray, array $sortIndex, array $sortOrder): array
|
||||
{
|
||||
$sortArguments = [];
|
||||
$sortData = [];
|
||||
foreach ($sortIndex as $index => $sortValues) {
|
||||
$sortData[] = $sortValues;
|
||||
$sortArguments[] = self::prepareSortVectorValues($sortValues);
|
||||
$sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
|
||||
}
|
||||
|
||||
$sortVector = self::executeVectorSortQuery($sortData, $sortArguments);
|
||||
|
||||
return self::sortLookupArrayFromVector($sortArray, $sortVector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $sortIndex
|
||||
* @param int[] $sortOrder
|
||||
*/
|
||||
private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array
|
||||
{
|
||||
$sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder);
|
||||
|
||||
return self::sortLookupArrayFromVector($sortArray, $sortVector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $sortIndex
|
||||
* @param int[] $sortOrder
|
||||
*/
|
||||
private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array
|
||||
{
|
||||
$sortArray = Matrix::transpose($sortArray);
|
||||
$result = self::sortByRow($sortArray, $sortIndex, $sortOrder);
|
||||
|
||||
return Matrix::transpose($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $sortIndex
|
||||
* @param int[] $sortOrder
|
||||
*/
|
||||
private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array
|
||||
{
|
||||
$sortArguments = [];
|
||||
$sortData = [];
|
||||
foreach ($sortIndex as $index => $sortIndexValue) {
|
||||
$sortValues = array_column($sortArray, $sortIndexValue - 1);
|
||||
$sortData[] = $sortValues;
|
||||
$sortArguments[] = self::prepareSortVectorValues($sortValues);
|
||||
$sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
|
||||
}
|
||||
|
||||
$sortData = self::executeVectorSortQuery($sortData, $sortArguments);
|
||||
|
||||
return $sortData;
|
||||
}
|
||||
|
||||
private static function executeVectorSortQuery(array $sortData, array $sortArguments): array
|
||||
{
|
||||
$sortData = Matrix::transpose($sortData);
|
||||
|
||||
// We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys.
|
||||
$sortDataIndexed = [];
|
||||
foreach ($sortData as $key => $value) {
|
||||
$sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value;
|
||||
}
|
||||
unset($sortData);
|
||||
|
||||
$sortArguments[] = &$sortDataIndexed;
|
||||
|
||||
array_multisort(...$sortArguments);
|
||||
|
||||
// After the sort, we restore the numeric keys that will now be in the correct, sorted order
|
||||
$sortedData = [];
|
||||
foreach (array_keys($sortDataIndexed) as $key) {
|
||||
$sortedData[] = Coordinate::columnIndexFromString($key) - 1;
|
||||
}
|
||||
|
||||
return $sortedData;
|
||||
}
|
||||
|
||||
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
|
||||
$sortedArray = [];
|
||||
foreach ($sortVector as $index) {
|
||||
$sortedArray[] = $sortArray[$index];
|
||||
}
|
||||
|
||||
return $sortedArray;
|
||||
|
||||
// uksort(
|
||||
// $lookupArray,
|
||||
// function (int $a, int $b) use (array $sortVector) {
|
||||
// return $sortVector[$a] <=> $sortVector[$b];
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// return $lookupArray;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue