Add Sample for Sort By Column

This commit is contained in:
oleibman
2025-12-13 10:24:07 -08:00
parent d727455173
commit 11d60f6ace
3 changed files with 198 additions and 31 deletions
+1 -1
View File
@@ -47,4 +47,4 @@ In Excel, COUNTIF appears to ignore text cells, behavior which doesn't seem to b
## SORT on Different DataTypes
Excel appears to sort so that numbers are lowest in sort order, strings are next, booleans are next (LibreOffice treats booleans as ints), and null is highest (and converted to int 0). In addition, if your sort includes a numeric string with a leading plus or minus sign, the plus sign will be considered part of the string (so that `"+1"` will sort before `"0"`), but the minus sign will be ignored (so that `"-3"` will sort between `"25"` and `"40"`). There might be nuances I haven't thought of yet. PhpSpreadsheet will not necessarily duplicate Excel's behavior. The best advice we can offer is to make sure that arrays you wish to sort consist of a single datatype, and don't contain numeric strings. Sample samples/LookupRef/SortExcel is added to give an idea of how you might emulate Excel's behavior. I am not yet convinced that there is a use case for adding it as a class member in the src tree.
Excel appears to sort so that numbers are lowest in sort order, strings are next, booleans are next (LibreOffice treats booleans as ints), and null is highest. In addition, if your sort includes a numeric string with a leading plus or minus sign, the plus sign will be considered part of the string (so that `"+1"` will sort before `"0"`), but the minus sign will be ignored (so that `"-3"` will sort between `"25"` and `"40"`). There might be nuances I haven't thought of yet. PhpSpreadsheet will not necessarily duplicate Excel's behavior. The best advice we can offer is to make sure that arrays you wish to sort consist of a single datatype, and don't contain numeric strings. Samples samples/LookupRef/SortExcel and SortExcelCols are added to give an idea of how you might emulate Excel's behavior. I am not yet convinced that there is a use case for adding it as a class member in the src tree.
+48 -30
View File
@@ -2,19 +2,39 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use Exception;
use PhpOffice\PhpSpreadsheet\Helper\Sample;
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
use Stringable;
class SortExcel
{
private static function cmp(mixed $a, mixed $b): int
public const ASCENDING = 1;
public const DESCENDING = -1;
private int $arrayCol;
private int $ascending;
private function cmp(mixed $rowA, mixed $rowB): int
{
// null sorts highest
if (!is_scalar($a)) {
return (is_scalar($b)) ? 1 : 0;
$a = is_array($rowA) ? $rowA[$this->arrayCol] : $rowA;
$b = is_array($rowB) ? $rowB[$this->arrayCol] : $rowB;
if ($a instanceof Stringable) {
$a = (string) $a;
}
if (!is_scalar($b)) {
return -1;
if ($b instanceof Stringable) {
$b = (string) $b;
}
if (is_array($a) || is_object($a) || is_resource($a) || is_array($b) || is_object($b) || is_resource($b)) {
throw new Exception('Invalid datatype');
}
// null sorts highest
if ($a === null) {
return ($b === null) ? 0 : $this->ascending;
}
if ($b === null) {
return -$this->ascending;
}
// int|float sorts lowest
$numericA = is_int($a) || is_float($a);
@@ -24,52 +44,49 @@ class SortExcel
return 0;
}
return ($a < $b) ? -1 : 1;
return ($a < $b) ? -$this->ascending : $this->ascending;
}
if ($numericA) {
return -1;
return -$this->ascending;
}
if ($numericB) {
return 1;
return $this->ascending;
}
// bool sorts higher than string
if (is_bool($a)) {
if (!is_bool($b)) {
return 1;
return $this->ascending;
}
if ($a) {
return $b ? 0 : 1;
return $b ? 0 : $this->ascending;
}
return $b ? -1 : 0;
return $b ? -$this->ascending : 0;
}
if (is_bool($b)) {
return -1;
return -$this->ascending;
}
// special handling for numeric strings starting with -
$a = (string) preg_replace('/^-(\d)+$/', '$1', $a);
$b = (string) preg_replace('/^-(\d)+$/', '$1', $b);
/** @var string $a */
$a2 = (string) preg_replace('/^-(\d)+$/', '$1', $a);
/** @var string $b */
$b2 = (string) preg_replace('/^-(\d)+$/', '$1', $b);
// strings sort case-insensitive
return strcasecmp($a, $b);
return $this->ascending * strcasecmp($a2, $b2);
}
/**
* Sort a one-dimensional array in the same order as Excel.
*
* @param array<int, null|bool|float|int|string> $array
* @param mixed[] $array
*/
public static function sortArray(array &$array): void
public function sortArray(array &$array, int $ascending = self::ASCENDING, int $arrayCol = 0): void
{
usort($array, self::cmp(...));
$j = count($array);
while ($j > 0) {
--$j;
if ($array[$j] !== null) {
break;
}
$array[$j] = 0;
if ($ascending !== 1 && $ascending !== -1) {
throw new Exception('ascending must be 1 or -1');
}
$this->ascending = $ascending;
$this->arrayCol = $arrayCol;
usort($array, $this->cmp(...));
}
}
@@ -77,11 +94,12 @@ require __DIR__ . '/../Header.php';
/** @var Sample $helper */
$helper->log('Emulating how Excel sorts different DataTypes');
/** @param array<int, null|bool|float|int|string> $original */
/** @param mixed[] $original */
function displaySorted(array $original, Sample $helper): void
{
$sorted = $original;
SortExcel::sortArray($sorted);
$sortExcel = new SortExcel();
$sortExcel->sortArray($sorted);
$outArray = [['Original', 'Sorted']];
$count = count($original);
for ($i = 0; $i < $count; ++$i) {
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use Exception;
use PhpOffice\PhpSpreadsheet\Helper\Sample;
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
use Stringable;
// this is the same class as in sortExcel
class SortExcelCols
{
public const ASCENDING = 1;
public const DESCENDING = -1;
private int $arrayCol;
private int $ascending;
private function cmp(mixed $rowA, mixed $rowB): int
{
$a = is_array($rowA) ? $rowA[$this->arrayCol] : $rowA;
$b = is_array($rowB) ? $rowB[$this->arrayCol] : $rowB;
if ($a instanceof Stringable) {
$a = (string) $a;
}
if ($b instanceof Stringable) {
$b = (string) $b;
}
if (is_array($a) || is_object($a) || is_resource($a) || is_array($b) || is_object($b) || is_resource($b)) {
throw new Exception('Invalid datatype');
}
// null sorts highest
if ($a === null) {
return ($b === null) ? 0 : $this->ascending;
}
if ($b === null) {
return -$this->ascending;
}
// int|float sorts lowest
$numericA = is_int($a) || is_float($a);
$numericB = is_int($b) || is_float($b);
if ($numericA && $numericB) {
if ($a == $b) {
return 0;
}
return ($a < $b) ? -$this->ascending : $this->ascending;
}
if ($numericA) {
return -$this->ascending;
}
if ($numericB) {
return $this->ascending;
}
// bool sorts higher than string
if (is_bool($a)) {
if (!is_bool($b)) {
return $this->ascending;
}
if ($a) {
return $b ? 0 : $this->ascending;
}
return $b ? -$this->ascending : 0;
}
if (is_bool($b)) {
return -$this->ascending;
}
// special handling for numeric strings starting with -
/** @var string $a */
$a2 = (string) preg_replace('/^-(\d)+$/', '$1', $a);
/** @var string $b */
$b2 = (string) preg_replace('/^-(\d)+$/', '$1', $b);
// strings sort case-insensitive
return $this->ascending * strcasecmp($a2, $b2);
}
/**
* @param mixed[] $array
*/
public function sortArray(array &$array, int $ascending = self::ASCENDING, int $arrayCol = 0): void
{
if ($ascending !== 1 && $ascending !== -1) {
throw new Exception('ascending must be 1 or -1');
}
$this->ascending = $ascending;
$this->arrayCol = $arrayCol;
usort($array, $this->cmp(...));
}
}
require __DIR__ . '/../Header.php';
/** @var Sample $helper */
$helper->log('Emulating how Excel sorts different DataTypes by Column');
$array = [
['a', 'a', 'a'],
['a', 'a', 'b'],
['a', null, 'c'],
['b', 'b', 1],
['b', 'c', 2],
['b', 'c', true],
['c', 1, false],
['c', 1, 'a'],
['c', 2, 'b'],
[1, 2, 'c'],
[1, true, 1],
[1, true, 2],
[2, false, true],
[2, false, false],
[2, 'a', false],
[true, 'b', true],
[true, 'c', 2],
[true, 1, 1],
[false, 2, 'a'],
[false, true, 'b'],
[false, false, 'c'],
];
/** @param array<int, array<int, mixed>> $original */
function displaySortedCols(array $original, Sample $helper): void
{
$sorted = $original;
$sortExcelCols = new SortExcelCols();
$helper->log('Sort by least significant column (descending)');
$sortExcelCols->sortArray($sorted, arrayCol: 2, ascending: -1);
$helper->log('Sort by middle column (ascending)');
$sortExcelCols->sortArray($sorted, arrayCol: 1, ascending: 1);
$helper->log('Sort by most significant column (descending)');
$sortExcelCols->sortArray($sorted, arrayCol: 0, ascending: -1);
$outArray = [['Original', '', '', 'Sorted', '', '']];
$count = count($original);
/** @var string[][] $sorted */
for ($i = 0; $i < $count; ++$i) {
$outArray[] = [
$original[$i][0],
$original[$i][1],
$original[$i][2],
$sorted[$i][0],
$sorted[$i][1],
$sorted[$i][2],
];
}
$helper->displayGrid($outArray, TextGridRightAlign::floatOrInt);
}
displaySortedCols($array, $helper);