mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-04 22:50:27 +00:00
Update Samples
... demonstrating how Excel sorts.
This commit is contained in:
@@ -44,7 +44,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
||||
@@ -47,4 +47,8 @@ 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. 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.
|
||||
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"`).
|
||||
For LibreOffice, numeric strings beginning with `-` will sort (in string order) before numeric strings beginning with `+` which will sort before unsigned numeric strings.
|
||||
There might be nuances I haven't thought of yet. PhpSpreadsheet will not necessarily duplicate Excel's/Libre'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.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Helper\Sample;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
|
||||
|
||||
require __DIR__ . '/../Header.php';
|
||||
/** @var Sample $helper */
|
||||
require_once __DIR__ . '/../templates/SortExcel.php';
|
||||
$helper->log('Emulating how Excel sorts different DataTypes');
|
||||
|
||||
/** @param mixed[] $original */
|
||||
function displaySorted(array $original, Sample $helper, bool $libreSemantics = false): void
|
||||
{
|
||||
$sorted = $original;
|
||||
$sortExcel = new SortExcel();
|
||||
$sortExcel->sortArray($sorted, libreSemantics: $libreSemantics);
|
||||
$outArray = [['Original', 'Sorted']];
|
||||
$count = count($original);
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$outArray[] = [$original[$i], $sorted[$i]];
|
||||
}
|
||||
$helper->displayGrid($outArray, TextGridRightAlign::floatOrInt);
|
||||
}
|
||||
|
||||
$helper->log('First example');
|
||||
$original = ['-3', '40', 'A', 'B', true, false, '+3', '1', '10', '2', '25', 1, 0, -1, '-2.5'];
|
||||
displaySorted($original, $helper);
|
||||
|
||||
$helper->log('First example with LibreOffice semantics');
|
||||
$original = ['-3', '40', 'A', 'B', true, false, '+3', '1', '10', '2', '25', 1, 0, -1, '-2.5'];
|
||||
displaySorted($original, $helper, true);
|
||||
|
||||
$helper->log('Second example');
|
||||
$original = ['a', 'A', null, 'x', 'X', true, false, -3, 1];
|
||||
displaySorted($original, $helper);
|
||||
@@ -2,97 +2,12 @@
|
||||
|
||||
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 */
|
||||
require_once __DIR__ . '/../templates/SortExcel.php';
|
||||
$helper->log('Emulating how Excel sorts different DataTypes by Column');
|
||||
|
||||
$array = [
|
||||
@@ -123,7 +38,7 @@ $array = [
|
||||
function displaySortedCols(array $original, Sample $helper): void
|
||||
{
|
||||
$sorted = $original;
|
||||
$sortExcelCols = new SortExcelCols();
|
||||
$sortExcelCols = new SortExcel();
|
||||
$helper->log('Sort by least significant column (descending)');
|
||||
$sortExcelCols->sortArray($sorted, arrayCol: 2, ascending: -1);
|
||||
$helper->log('Sort by middle column (ascending)');
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\Sample;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
|
||||
use Stringable;
|
||||
|
||||
class SortExcel
|
||||
@@ -16,10 +14,20 @@ class SortExcel
|
||||
|
||||
private int $ascending;
|
||||
|
||||
private bool $libreSemantics = false;
|
||||
|
||||
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 ($this->libreSemantics) {
|
||||
if (is_bool($a)) {
|
||||
$a = (int) $a;
|
||||
}
|
||||
if (is_bool($b)) {
|
||||
$b = (int) $b;
|
||||
}
|
||||
}
|
||||
if ($a instanceof Stringable) {
|
||||
$a = (string) $a;
|
||||
}
|
||||
@@ -67,10 +75,36 @@ class SortExcel
|
||||
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);
|
||||
/** @var string */
|
||||
$a2 = $a;
|
||||
/** @var string */
|
||||
$b2 = $b;
|
||||
if ($this->libreSemantics) {
|
||||
if (is_numeric($a2) && is_numeric($b2)) {
|
||||
if (str_starts_with($a2, '+')) {
|
||||
if (str_starts_with($b2, '+')) {
|
||||
} elseif (str_starts_with($b2, '-')) {
|
||||
return $this->ascending;
|
||||
} else {
|
||||
return -$this->ascending;
|
||||
}
|
||||
} elseif (str_starts_with($b2, '+')) {
|
||||
// $a2 can't start with + here
|
||||
if (str_starts_with($a2, '-')) {
|
||||
return -$this->ascending;
|
||||
}
|
||||
|
||||
return $this->ascending;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (is_numeric($a2) && str_starts_with($a2, '-')) {
|
||||
$a2 = substr($a2, 1);
|
||||
}
|
||||
if (is_numeric($b2) && str_starts_with($b2, '-')) {
|
||||
$b2 = substr($b2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// strings sort case-insensitive
|
||||
return $this->ascending * strcasecmp($a2, $b2);
|
||||
@@ -79,39 +113,14 @@ class SortExcel
|
||||
/**
|
||||
* @param mixed[] $array
|
||||
*/
|
||||
public function sortArray(array &$array, int $ascending = self::ASCENDING, int $arrayCol = 0): void
|
||||
public function sortArray(array &$array, int $ascending = self::ASCENDING, int $arrayCol = 0, bool $libreSemantics = false): void
|
||||
{
|
||||
if ($ascending !== 1 && $ascending !== -1) {
|
||||
throw new Exception('ascending must be 1 or -1');
|
||||
}
|
||||
$this->ascending = $ascending;
|
||||
$this->arrayCol = $arrayCol;
|
||||
$this->libreSemantics = $libreSemantics;
|
||||
usort($array, $this->cmp(...));
|
||||
}
|
||||
}
|
||||
|
||||
require __DIR__ . '/../Header.php';
|
||||
/** @var Sample $helper */
|
||||
$helper->log('Emulating how Excel sorts different DataTypes');
|
||||
|
||||
/** @param mixed[] $original */
|
||||
function displaySorted(array $original, Sample $helper): void
|
||||
{
|
||||
$sorted = $original;
|
||||
$sortExcel = new SortExcel();
|
||||
$sortExcel->sortArray($sorted);
|
||||
$outArray = [['Original', 'Sorted']];
|
||||
$count = count($original);
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$outArray[] = [$original[$i], $sorted[$i]];
|
||||
}
|
||||
$helper->displayGrid($outArray, TextGridRightAlign::floatOrInt);
|
||||
}
|
||||
|
||||
$helper->log('First example');
|
||||
$original = ['-3', '40', 'A', 'B', true, false, '+3', '1', '10', '2', '25', 1, 0, -1];
|
||||
displaySorted($original, $helper);
|
||||
|
||||
$helper->log('Second example');
|
||||
$original = ['a', 'A', null, 'x', 'X', true, false, -3, 1];
|
||||
displaySorted($original, $helper);
|
||||
Reference in New Issue
Block a user